https://github.com/python/cpython/commit/0f02dbe4ab5605dafef150000bdc5a94ad44b3cf
commit: 0f02dbe4ab5605dafef150000bdc5a94ad44b3cf
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: colesbury <[email protected]>
date: 2026-01-08T12:35:31-05:00
summary:

[3.14] gh-142095: Use thread local frame info in `py-bt` and `py-bt-full` when 
available (gh-143371) (#143566)

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.
(cherry picked from commit 49c3b0a67a77bb42e736cea7dcbc1aa8fa704074)

Co-authored-by: Sam Gross <[email protected]>
Co-authored-by: Victor Stinner <[email protected]>

files:
A Misc/NEWS.d/next/Tools-Demos/2026-01-02-11-44-56.gh-issue-142095.4ssgnM.rst
M Tools/gdb/libpython.py

diff --git 
a/Misc/NEWS.d/next/Tools-Demos/2026-01-02-11-44-56.gh-issue-142095.4ssgnM.rst 
b/Misc/NEWS.d/next/Tools-Demos/2026-01-02-11-44-56.gh-issue-142095.4ssgnM.rst
new file mode 100644
index 00000000000000..196b27dfd66302
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Tools-Demos/2026-01-02-11-44-56.gh-issue-142095.4ssgnM.rst
@@ -0,0 +1,2 @@
+Make gdb 'py-bt' command use frame from thread local state when available.
+Patch by Sam Gross and Victor Stinner.
diff --git a/Tools/gdb/libpython.py b/Tools/gdb/libpython.py
index 27aa6b0cc266d3..a85195dcd1016a 100755
--- a/Tools/gdb/libpython.py
+++ b/Tools/gdb/libpython.py
@@ -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')
-
-                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
+                val = gdb.parse_and_eval(f'(PyThreadState*)({expr})')
+            except gdb.error:
+                continue
+            if int(val) != 0:
+                return val
+        return None
+
+    @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()
 

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to