Skip to content

Commit 49c3b0a

Browse files
colesburyvstinner
andauthored
gh-142095: Use thread local frame info in py-bt and py-bt-full when available (gh-143371)
In optimized and `-Og` builds, arguments and local variables are frequently unavailable in gdb. This makes `py-bt` fail to print anything useful. Use the `PyThreadState*` pointers `_Py_tss_gilstate` and `Py_tss_tstate` to find the interpreter frame if we can't get the frame from the `_PyEval_EvalFrameDefault` call. Co-authored-by: Victor Stinner <vstinner@python.org>
1 parent 5462002 commit 49c3b0a

File tree

2 files changed

+109
-76
lines changed

2 files changed

+109
-76
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Make gdb 'py-bt' command use frame from thread local state when available.
2+
Patch by Sam Gross and Victor Stinner.

Tools/gdb/libpython.py

Lines changed: 107 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@ def write(self, data):
152152
def getvalue(self):
153153
return self._val
154154

155+
156+
def _PyStackRef_AsPyObjectBorrow(gdbval):
157+
return gdb.Value(int(gdbval['bits']) & ~USED_TAGS)
158+
159+
155160
class PyObjectPtr(object):
156161
"""
157162
Class wrapping a gdb.Value that's either a (PyObject*) within the
@@ -170,7 +175,7 @@ def __init__(self, gdbval, cast_to=None):
170175
if gdbval.type.name == '_PyStackRef':
171176
if cast_to is None:
172177
cast_to = gdb.lookup_type('PyObject').pointer()
173-
self._gdbval = gdb.Value(int(gdbval['bits']) & ~USED_TAGS).cast(cast_to)
178+
self._gdbval = _PyStackRef_AsPyObjectBorrow(gdbval).cast(cast_to)
174179
elif cast_to:
175180
self._gdbval = gdbval.cast(cast_to)
176181
else:
@@ -1034,30 +1039,49 @@ def write_repr(self, out, visited):
10341039
return
10351040
return self._frame.write_repr(out, visited)
10361041

1037-
def print_traceback(self):
1038-
if self.is_optimized_out():
1039-
sys.stdout.write(' %s\n' % FRAME_INFO_OPTIMIZED_OUT)
1040-
return
1041-
return self._frame.print_traceback()
1042-
10431042
class PyFramePtr:
10441043

10451044
def __init__(self, gdbval):
10461045
self._gdbval = gdbval
1046+
if self.is_optimized_out():
1047+
return
1048+
self.co = self._f_code()
1049+
if self.is_shim():
1050+
return
1051+
self.co_name = self.co.pyop_field('co_name')
1052+
self.co_filename = self.co.pyop_field('co_filename')
10471053

1048-
if not self.is_optimized_out():
1054+
self.f_lasti = self._f_lasti()
1055+
self.co_nlocals = int_from_int(self.co.field('co_nlocals'))
1056+
pnames = self.co.field('co_localsplusnames')
1057+
self.co_localsplusnames = PyTupleObjectPtr.from_pyobject_ptr(pnames)
1058+
1059+
@staticmethod
1060+
def get_thread_state():
1061+
exprs = [
1062+
'_Py_tss_gilstate', # 3.15+
1063+
'_Py_tss_tstate', # 3.12+ (and not when GIL is released)
1064+
'pthread_getspecific(_PyRuntime.autoTSSkey._key)', # only live programs
1065+
'((struct pthread*)$fs_base)->specific_1stblock[_PyRuntime.autoTSSkey._key].data' # x86-64
1066+
]
1067+
for expr in exprs:
10491068
try:
1050-
self.co = self._f_code()
1051-
self.co_name = self.co.pyop_field('co_name')
1052-
self.co_filename = self.co.pyop_field('co_filename')
1053-
1054-
self.f_lasti = self._f_lasti()
1055-
self.co_nlocals = int_from_int(self.co.field('co_nlocals'))
1056-
pnames = self.co.field('co_localsplusnames')
1057-
self.co_localsplusnames = PyTupleObjectPtr.from_pyobject_ptr(pnames)
1058-
self._is_code = True
1059-
except:
1060-
self._is_code = False
1069+
val = gdb.parse_and_eval(f'(PyThreadState*)({expr})')
1070+
except gdb.error:
1071+
continue
1072+
if int(val) != 0:
1073+
return val
1074+
return None
1075+
1076+
@staticmethod
1077+
def get_thread_local_frame():
1078+
thread_state = PyFramePtr.get_thread_state()
1079+
if thread_state is None:
1080+
return None
1081+
current_frame = thread_state['current_frame']
1082+
if int(current_frame) == 0:
1083+
return None
1084+
return PyFramePtr(current_frame)
10611085

10621086
def is_optimized_out(self):
10631087
return self._gdbval.is_optimized_out
@@ -1115,6 +1139,8 @@ def is_shim(self):
11151139
return self._f_special("owner", int) == FRAME_OWNED_BY_INTERPRETER
11161140

11171141
def previous(self):
1142+
if int(self._gdbval['previous']) == 0:
1143+
return None
11181144
return self._f_special("previous", PyFramePtr)
11191145

11201146
def iter_globals(self):
@@ -1243,6 +1269,27 @@ def print_traceback(self):
12431269
lineno,
12441270
self.co_name.proxyval(visited)))
12451271

1272+
def print_traceback_until_shim(self, frame_index=None):
1273+
# Print traceback for _PyInterpreterFrame and return previous frame
1274+
interp_frame = self
1275+
while True:
1276+
if not interp_frame:
1277+
sys.stdout.write(' (unable to read python frame information)\n')
1278+
return None
1279+
if interp_frame.is_shim():
1280+
return interp_frame.previous()
1281+
1282+
if frame_index is not None:
1283+
line = interp_frame.get_truncated_repr(MAX_OUTPUT_LEN)
1284+
sys.stdout.write('#%i %s\n' % (frame_index, line))
1285+
else:
1286+
interp_frame.print_traceback()
1287+
if not interp_frame.is_optimized_out():
1288+
line = interp_frame.current_line()
1289+
if line is not None:
1290+
sys.stdout.write(' %s\n' % line.strip())
1291+
interp_frame = interp_frame.previous()
1292+
12461293
def get_truncated_repr(self, maxlen):
12471294
'''
12481295
Get a repr-like string for the data, but truncate it at "maxlen" bytes
@@ -1855,50 +1902,17 @@ def get_selected_bytecode_frame(cls):
18551902
def print_summary(self):
18561903
if self.is_evalframe():
18571904
interp_frame = self.get_pyop()
1858-
while True:
1859-
if interp_frame:
1860-
if interp_frame.is_shim():
1861-
break
1862-
line = interp_frame.get_truncated_repr(MAX_OUTPUT_LEN)
1863-
sys.stdout.write('#%i %s\n' % (self.get_index(), line))
1864-
if not interp_frame.is_optimized_out():
1865-
line = interp_frame.current_line()
1866-
if line is not None:
1867-
sys.stdout.write(' %s\n' % line.strip())
1868-
else:
1869-
sys.stdout.write('#%i (unable to read python frame information)\n' % self.get_index())
1870-
break
1871-
interp_frame = interp_frame.previous()
1905+
if interp_frame:
1906+
interp_frame.print_traceback_until_shim(self.get_index())
1907+
else:
1908+
sys.stdout.write('#%i (unable to read python frame information)\n' % self.get_index())
18721909
else:
18731910
info = self.is_other_python_frame()
18741911
if info:
18751912
sys.stdout.write('#%i %s\n' % (self.get_index(), info))
18761913
else:
18771914
sys.stdout.write('#%i\n' % self.get_index())
18781915

1879-
def print_traceback(self):
1880-
if self.is_evalframe():
1881-
interp_frame = self.get_pyop()
1882-
while True:
1883-
if interp_frame:
1884-
if interp_frame.is_shim():
1885-
break
1886-
interp_frame.print_traceback()
1887-
if not interp_frame.is_optimized_out():
1888-
line = interp_frame.current_line()
1889-
if line is not None:
1890-
sys.stdout.write(' %s\n' % line.strip())
1891-
else:
1892-
sys.stdout.write(' (unable to read python frame information)\n')
1893-
break
1894-
interp_frame = interp_frame.previous()
1895-
else:
1896-
info = self.is_other_python_frame()
1897-
if info:
1898-
sys.stdout.write(' %s\n' % info)
1899-
else:
1900-
sys.stdout.write(' (not a python frame)\n')
1901-
19021916
class PyList(gdb.Command):
19031917
'''List the current Python source code, if any
19041918
@@ -2042,6 +2056,41 @@ def invoke(self, args, from_tty):
20422056
PyUp()
20432057
PyDown()
20442058

2059+
2060+
def print_traceback_helper(full_info):
2061+
frame = Frame.get_selected_python_frame()
2062+
interp_frame = PyFramePtr.get_thread_local_frame()
2063+
if not frame and not interp_frame:
2064+
print('Unable to locate python frame')
2065+
return
2066+
2067+
sys.stdout.write('Traceback (most recent call first):\n')
2068+
if frame:
2069+
while frame:
2070+
frame_index = frame.get_index() if full_info else None
2071+
if frame.is_evalframe():
2072+
pyop = frame.get_pyop()
2073+
if pyop is not None:
2074+
# Use the _PyInterpreterFrame from the gdb frame
2075+
interp_frame = pyop
2076+
if interp_frame:
2077+
interp_frame = interp_frame.print_traceback_until_shim(frame_index)
2078+
else:
2079+
sys.stdout.write(' (unable to read python frame information)\n')
2080+
else:
2081+
info = frame.is_other_python_frame()
2082+
if full_info:
2083+
if info:
2084+
sys.stdout.write('#%i %s\n' % (frame_index, info))
2085+
elif info:
2086+
sys.stdout.write(' %s\n' % info)
2087+
frame = frame.older()
2088+
else:
2089+
# Fall back to just using the thread-local frame
2090+
while interp_frame:
2091+
interp_frame = interp_frame.print_traceback_until_shim()
2092+
2093+
20452094
class PyBacktraceFull(gdb.Command):
20462095
'Display the current python frame and all the frames within its call stack (if any)'
20472096
def __init__(self):
@@ -2052,15 +2101,7 @@ def __init__(self):
20522101

20532102

20542103
def invoke(self, args, from_tty):
2055-
frame = Frame.get_selected_python_frame()
2056-
if not frame:
2057-
print('Unable to locate python frame')
2058-
return
2059-
2060-
while frame:
2061-
if frame.is_python_frame():
2062-
frame.print_summary()
2063-
frame = frame.older()
2104+
print_traceback_helper(full_info=True)
20642105

20652106
PyBacktraceFull()
20662107

@@ -2072,18 +2113,8 @@ def __init__(self):
20722113
gdb.COMMAND_STACK,
20732114
gdb.COMPLETE_NONE)
20742115

2075-
20762116
def invoke(self, args, from_tty):
2077-
frame = Frame.get_selected_python_frame()
2078-
if not frame:
2079-
print('Unable to locate python frame')
2080-
return
2081-
2082-
sys.stdout.write('Traceback (most recent call first):\n')
2083-
while frame:
2084-
if frame.is_python_frame():
2085-
frame.print_traceback()
2086-
frame = frame.older()
2117+
print_traceback_helper(full_info=False)
20872118

20882119
PyBacktrace()
20892120

0 commit comments

Comments
 (0)