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>
This commit is contained in:
Sam Gross 2026-01-08 09:18:24 -05:00 committed by GitHub
parent 5462002bbe
commit 49c3b0a67a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 108 additions and 75 deletions

View file

@ -0,0 +1,2 @@
Make gdb 'py-bt' command use frame from thread local state when available.
Patch by Sam Gross and Victor Stinner.

View file

@ -152,6 +152,11 @@ def write(self, data):
def getvalue(self):
return self._val
def _PyStackRef_AsPyObjectBorrow(gdbval):
return gdb.Value(int(gdbval['bits']) & ~USED_TAGS)
class PyObjectPtr(object):
"""
Class wrapping a gdb.Value that's either a (PyObject*) within the
@ -170,7 +175,7 @@ def __init__(self, gdbval, cast_to=None):
if gdbval.type.name == '_PyStackRef':
if cast_to is None:
cast_to = gdb.lookup_type('PyObject').pointer()
self._gdbval = gdb.Value(int(gdbval['bits']) & ~USED_TAGS).cast(cast_to)
self._gdbval = _PyStackRef_AsPyObjectBorrow(gdbval).cast(cast_to)
elif cast_to:
self._gdbval = gdbval.cast(cast_to)
else:
@ -1034,30 +1039,49 @@ def write_repr(self, out, visited):
return
return self._frame.write_repr(out, visited)
def print_traceback(self):
if self.is_optimized_out():
sys.stdout.write(' %s\n' % FRAME_INFO_OPTIMIZED_OUT)
return
return self._frame.print_traceback()
class PyFramePtr:
def __init__(self, gdbval):
self._gdbval = gdbval
if self.is_optimized_out():
return
self.co = self._f_code()
if self.is_shim():
return
self.co_name = self.co.pyop_field('co_name')
self.co_filename = self.co.pyop_field('co_filename')
if not self.is_optimized_out():
self.f_lasti = self._f_lasti()
self.co_nlocals = int_from_int(self.co.field('co_nlocals'))
pnames = self.co.field('co_localsplusnames')
self.co_localsplusnames = PyTupleObjectPtr.from_pyobject_ptr(pnames)
@staticmethod
def get_thread_state():
exprs = [
'_Py_tss_gilstate', # 3.15+
'_Py_tss_tstate', # 3.12+ (and not when GIL is released)
'pthread_getspecific(_PyRuntime.autoTSSkey._key)', # only live programs
'((struct pthread*)$fs_base)->specific_1stblock[_PyRuntime.autoTSSkey._key].data' # x86-64
]
for expr in exprs:
try:
self.co = self._f_code()
self.co_name = self.co.pyop_field('co_name')
self.co_filename = self.co.pyop_field('co_filename')
val = gdb.parse_and_eval(f'(PyThreadState*)({expr})')
except gdb.error:
continue
if int(val) != 0:
return val
return None
self.f_lasti = self._f_lasti()
self.co_nlocals = int_from_int(self.co.field('co_nlocals'))
pnames = self.co.field('co_localsplusnames')
self.co_localsplusnames = PyTupleObjectPtr.from_pyobject_ptr(pnames)
self._is_code = True
except:
self._is_code = False
@staticmethod
def get_thread_local_frame():
thread_state = PyFramePtr.get_thread_state()
if thread_state is None:
return None
current_frame = thread_state['current_frame']
if int(current_frame) == 0:
return None
return PyFramePtr(current_frame)
def is_optimized_out(self):
return self._gdbval.is_optimized_out
@ -1115,6 +1139,8 @@ def is_shim(self):
return self._f_special("owner", int) == FRAME_OWNED_BY_INTERPRETER
def previous(self):
if int(self._gdbval['previous']) == 0:
return None
return self._f_special("previous", PyFramePtr)
def iter_globals(self):
@ -1243,6 +1269,27 @@ def print_traceback(self):
lineno,
self.co_name.proxyval(visited)))
def print_traceback_until_shim(self, frame_index=None):
# Print traceback for _PyInterpreterFrame and return previous frame
interp_frame = self
while True:
if not interp_frame:
sys.stdout.write(' (unable to read python frame information)\n')
return None
if interp_frame.is_shim():
return interp_frame.previous()
if frame_index is not None:
line = interp_frame.get_truncated_repr(MAX_OUTPUT_LEN)
sys.stdout.write('#%i %s\n' % (frame_index, line))
else:
interp_frame.print_traceback()
if not interp_frame.is_optimized_out():
line = interp_frame.current_line()
if line is not None:
sys.stdout.write(' %s\n' % line.strip())
interp_frame = interp_frame.previous()
def get_truncated_repr(self, maxlen):
'''
Get a repr-like string for the data, but truncate it at "maxlen" bytes
@ -1855,20 +1902,10 @@ def get_selected_bytecode_frame(cls):
def print_summary(self):
if self.is_evalframe():
interp_frame = self.get_pyop()
while True:
if interp_frame:
if interp_frame.is_shim():
break
line = interp_frame.get_truncated_repr(MAX_OUTPUT_LEN)
sys.stdout.write('#%i %s\n' % (self.get_index(), line))
if not interp_frame.is_optimized_out():
line = interp_frame.current_line()
if line is not None:
sys.stdout.write(' %s\n' % line.strip())
else:
sys.stdout.write('#%i (unable to read python frame information)\n' % self.get_index())
break
interp_frame = interp_frame.previous()
if interp_frame:
interp_frame.print_traceback_until_shim(self.get_index())
else:
sys.stdout.write('#%i (unable to read python frame information)\n' % self.get_index())
else:
info = self.is_other_python_frame()
if info:
@ -1876,29 +1913,6 @@ def print_summary(self):
else:
sys.stdout.write('#%i\n' % self.get_index())
def print_traceback(self):
if self.is_evalframe():
interp_frame = self.get_pyop()
while True:
if interp_frame:
if interp_frame.is_shim():
break
interp_frame.print_traceback()
if not interp_frame.is_optimized_out():
line = interp_frame.current_line()
if line is not None:
sys.stdout.write(' %s\n' % line.strip())
else:
sys.stdout.write(' (unable to read python frame information)\n')
break
interp_frame = interp_frame.previous()
else:
info = self.is_other_python_frame()
if info:
sys.stdout.write(' %s\n' % info)
else:
sys.stdout.write(' (not a python frame)\n')
class PyList(gdb.Command):
'''List the current Python source code, if any
@ -2042,6 +2056,41 @@ def invoke(self, args, from_tty):
PyUp()
PyDown()
def print_traceback_helper(full_info):
frame = Frame.get_selected_python_frame()
interp_frame = PyFramePtr.get_thread_local_frame()
if not frame and not interp_frame:
print('Unable to locate python frame')
return
sys.stdout.write('Traceback (most recent call first):\n')
if frame:
while frame:
frame_index = frame.get_index() if full_info else None
if frame.is_evalframe():
pyop = frame.get_pyop()
if pyop is not None:
# Use the _PyInterpreterFrame from the gdb frame
interp_frame = pyop
if interp_frame:
interp_frame = interp_frame.print_traceback_until_shim(frame_index)
else:
sys.stdout.write(' (unable to read python frame information)\n')
else:
info = frame.is_other_python_frame()
if full_info:
if info:
sys.stdout.write('#%i %s\n' % (frame_index, info))
elif info:
sys.stdout.write(' %s\n' % info)
frame = frame.older()
else:
# Fall back to just using the thread-local frame
while interp_frame:
interp_frame = interp_frame.print_traceback_until_shim()
class PyBacktraceFull(gdb.Command):
'Display the current python frame and all the frames within its call stack (if any)'
def __init__(self):
@ -2052,15 +2101,7 @@ def __init__(self):
def invoke(self, args, from_tty):
frame = Frame.get_selected_python_frame()
if not frame:
print('Unable to locate python frame')
return
while frame:
if frame.is_python_frame():
frame.print_summary()
frame = frame.older()
print_traceback_helper(full_info=True)
PyBacktraceFull()
@ -2072,18 +2113,8 @@ def __init__(self):
gdb.COMMAND_STACK,
gdb.COMPLETE_NONE)
def invoke(self, args, from_tty):
frame = Frame.get_selected_python_frame()
if not frame:
print('Unable to locate python frame')
return
sys.stdout.write('Traceback (most recent call first):\n')
while frame:
if frame.is_python_frame():
frame.print_traceback()
frame = frame.older()
print_traceback_helper(full_info=False)
PyBacktrace()