https://github.com/python/cpython/commit/1e5d94274d5f49c5a1fb1bbabbe07e92edeb0028
commit: 1e5d94274d5f49c5a1fb1bbabbe07e92edeb0028
branch: main
author: Diego Russo <[email protected]>
committer: diegorusso <[email protected]>
date: 2026-05-05T09:29:07+01:00
summary:

GH-126910: Add GNU backtrace support for unwinding JIT frames (#149104)


Co-authored-by: Pablo Galindo Salgado <[email protected]>

files:
A Include/internal/pycore_jit_publish.h
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-05-02-18-02-41.gh-issue-126910.nqDVrp.rst
A Python/jit_publish.c
M Include/internal/pycore_jit_unwind.h
M Include/internal/pycore_optimizer.h
M Lib/test/test_frame_pointer_unwind.py
M Makefile.pre.in
M Modules/_testinternalcapi.c
M PCbuild/_freeze_module.vcxproj
M PCbuild/_freeze_module.vcxproj.filters
M PCbuild/pythoncore.vcxproj
M PCbuild/pythoncore.vcxproj.filters
M Python/jit.c
M Python/jit_unwind.c
M Python/optimizer.c
M configure
M configure.ac
M pyconfig.h.in

diff --git a/Include/internal/pycore_jit_publish.h 
b/Include/internal/pycore_jit_publish.h
new file mode 100644
index 00000000000000..7ee77f5c996faf
--- /dev/null
+++ b/Include/internal/pycore_jit_publish.h
@@ -0,0 +1,31 @@
+#ifndef Py_INTERNAL_JIT_PUBLISH_H
+#define Py_INTERNAL_JIT_PUBLISH_H
+
+#ifndef Py_BUILD_CORE
+#  error "this header requires Py_BUILD_CORE define"
+#endif
+
+#include <stddef.h>
+
+typedef struct _PyJitCodeRegistration _PyJitCodeRegistration;
+
+#ifdef _Py_JIT
+
+/* Publish JIT code to optional tooling backends.
+ *
+ * The return value is a backend-specific deregistration handle, not a
+ * success/failure indicator. NULL means there is nothing to unregister later:
+ * perf does not need a handle, and GDB/GNU backtrace registration failures
+ * are intentionally non-fatal because tooling support must not make JIT
+ * compilation fail.
+ */
+_PyJitCodeRegistration *_PyJit_RegisterCode(const void *code_addr,
+                                            size_t code_size,
+                                            const char *entry,
+                                            const char *filename);
+
+void _PyJit_UnregisterCode(_PyJitCodeRegistration *registration);
+
+#endif  // _Py_JIT
+
+#endif  // Py_INTERNAL_JIT_PUBLISH_H
diff --git a/Include/internal/pycore_jit_unwind.h 
b/Include/internal/pycore_jit_unwind.h
index 2d325ad9562286..508caee97c43ab 100644
--- a/Include/internal/pycore_jit_unwind.h
+++ b/Include/internal/pycore_jit_unwind.h
@@ -10,9 +10,15 @@
 
 #if defined(_Py_JIT) && defined(__linux__) && defined(__ELF__)
 #  define PY_HAVE_JIT_GDB_UNWIND
+#  if defined(HAVE_EXECINFO_H) && defined(HAVE_BACKTRACE) && \
+          defined(HAVE_LIBGCC_EH_FRAME_REGISTRATION)
+#    define PY_HAVE_JIT_GNU_BACKTRACE_UNWIND
+#  endif
 #endif
 
-#if defined(PY_HAVE_PERF_TRAMPOLINE) || defined(PY_HAVE_JIT_GDB_UNWIND)
+#if defined(PY_HAVE_PERF_TRAMPOLINE) \
+    || defined(PY_HAVE_JIT_GDB_UNWIND) \
+    || defined(PY_HAVE_JIT_GNU_BACKTRACE_UNWIND)
 
 #if defined(PY_HAVE_JIT_GDB_UNWIND)
 extern PyMutex _Py_jit_debug_mutex;
@@ -63,6 +69,13 @@ void *_PyJitUnwind_GdbRegisterCode(const void *code_addr,
 
 void _PyJitUnwind_GdbUnregisterCode(void *handle);
 
-#endif  // defined(PY_HAVE_PERF_TRAMPOLINE) || defined(PY_HAVE_JIT_GDB_UNWIND)
+#if defined(PY_HAVE_JIT_GNU_BACKTRACE_UNWIND)
+void *_PyJitUnwind_GnuBacktraceRegisterCode(const void *code_addr,
+                                            size_t code_size);
+
+void _PyJitUnwind_GnuBacktraceUnregisterCode(void *handle);
+#endif
+
+#endif  // JIT unwind support
 
 #endif  // Py_INTERNAL_JIT_UNWIND_H
diff --git a/Include/internal/pycore_optimizer.h 
b/Include/internal/pycore_optimizer.h
index a0727c045e51ec..69f913ec9c3038 100644
--- a/Include/internal/pycore_optimizer.h
+++ b/Include/internal/pycore_optimizer.h
@@ -9,6 +9,7 @@ extern "C" {
 #endif
 
 #include "pycore_typedefs.h"      // _PyInterpreterFrame
+#include "pycore_jit_publish.h"
 #include "pycore_uop.h"           // _PyUOpInstruction
 #include "pycore_uop_ids.h"
 #include "pycore_stackref.h"      // _PyStackRef
@@ -198,7 +199,7 @@ typedef struct _PyExecutorObject {
     uint32_t code_size;
     size_t jit_size;
     void *jit_code;
-    void *jit_gdb_handle;
+    _PyJitCodeRegistration *jit_registration;
     _PyExitData exits[1];
 } _PyExecutorObject;
 
diff --git a/Lib/test/test_frame_pointer_unwind.py 
b/Lib/test/test_frame_pointer_unwind.py
index 2f9ce2bf049f58..4081e1cbd8aaac 100644
--- a/Lib/test/test_frame_pointer_unwind.py
+++ b/Lib/test/test_frame_pointer_unwind.py
@@ -17,6 +17,9 @@
     raise unittest.SkipTest("test requires subprocess support")
 
 
+STACK_DEPTH = 10
+
+
 def _frame_pointers_expected(machine):
     cflags = " ".join(
         value for value in (
@@ -70,7 +73,7 @@ def _frame_pointers_expected(machine):
     return None
 
 
-def _build_stack_and_unwind():
+def _build_stack_and_unwind(unwinder):
     import operator
 
     def build_stack(n, unwinder, warming_up_caller=False):
@@ -89,7 +92,7 @@ def build_stack(n, unwinder, warming_up_caller=False):
             result = operator.call(build_stack, n - 1, unwinder, warming_up)
         return result
 
-    stack = build_stack(10, _testinternalcapi.manual_frame_pointer_unwind)
+    stack = build_stack(STACK_DEPTH, unwinder)
     return stack
 
 
@@ -112,8 +115,7 @@ def _classify_stack(stack, jit_enabled):
     return annotated, python_frames, jit_frames, other_frames
 
 
-def _annotate_unwind():
-    stack = _build_stack_and_unwind()
+def _summarize_unwind(stack, unwinder_name):
     jit_enabled = hasattr(sys, "_jit") and sys._jit.is_enabled()
     jit_backend = _testinternalcapi.get_jit_backend()
     ranges = _testinternalcapi.get_jit_code_ranges() if jit_enabled else []
@@ -126,19 +128,44 @@ def _annotate_unwind():
     )
     for idx, addr, tag in annotated:
         print(f"#{idx:02d} {addr:#x} -> {tag}")
-    return json.dumps({
+    return {
         "length": len(stack),
         "python_frames": python_frames,
         "jit_frames": jit_frames,
         "other_frames": other_frames,
         "jit_backend": jit_backend,
+        "unwinder": unwinder_name,
+    }
+
+
+def _annotate_unwind(unwinder_name="manual_frame_pointer_unwind"):
+    unwinder = getattr(_testinternalcapi, unwinder_name)
+    stack = _build_stack_and_unwind(unwinder)
+    return json.dumps(_summarize_unwind(stack, unwinder_name))
+
+
+def _annotate_unwind_after_executor_free(unwinder_name="gnu_backtrace_unwind"):
+    # The first unwind runs at the bottom of _build_stack_and_unwind(), while
+    # the recursive helper may be executing in JIT code. After it returns, this
+    # helper is back in normal test code; clearing executor caches should 
remove
+    # the old JIT ranges, so the second unwind must not report stale JIT 
frames.
+    live = json.loads(_annotate_unwind(unwinder_name))
+
+    sys._clear_internal_caches()
+    _testinternalcapi.clear_executor_deletion_list()
+
+    unwinder = getattr(_testinternalcapi, unwinder_name)
+    after_free = _summarize_unwind(unwinder(), unwinder_name)
+    return json.dumps({
+        "live": live,
+        "after_free": after_free,
     })
 
 
-def _manual_unwind_length(**env):
+def _run_unwind_helper(helper_name, unwinder_name, **env):
     code = (
-        "from test.test_frame_pointer_unwind import _annotate_unwind; "
-        "print(_annotate_unwind());"
+        f"from test.test_frame_pointer_unwind import {helper_name}; "
+        f"print({helper_name}({unwinder_name!r}));"
     )
     run_env = os.environ.copy()
     run_env.update(env)
@@ -166,6 +193,15 @@ def _manual_unwind_length(**env):
         ) from exc
 
 
+def _unwind_result(unwinder_name, **env):
+    return _run_unwind_helper("_annotate_unwind", unwinder_name, **env)
+
+
+def _unwind_after_executor_free_result(unwinder_name, **env):
+    return _run_unwind_helper(
+        "_annotate_unwind_after_executor_free", unwinder_name, **env)
+
+
 @support.requires_gil_enabled("test requires the GIL enabled")
 @unittest.skipIf(support.is_wasi, "test not supported on WASI")
 class FramePointerUnwindTests(unittest.TestCase):
@@ -197,14 +233,14 @@ def test_manual_unwind_respects_frame_pointers(self):
 
         for env, using_jit in envs:
             with self.subTest(env=env):
-                result = _manual_unwind_length(**env)
+                result = _unwind_result("manual_frame_pointer_unwind", **env)
                 jit_frames = result["jit_frames"]
                 python_frames = result.get("python_frames", 0)
                 jit_backend = result.get("jit_backend")
                 if self.frame_pointers_expected:
-                    self.assertGreater(
+                    self.assertGreaterEqual(
                         python_frames,
-                        0,
+                        STACK_DEPTH,
                         f"expected to find Python frames on {self.machine} 
with env {env}",
                     )
                     if using_jit:
@@ -240,5 +276,84 @@ def test_manual_unwind_respects_frame_pointers(self):
                     )
 
 
[email protected]_gil_enabled("test requires the GIL enabled")
[email protected](support.is_wasi, "test not supported on WASI")
[email protected](sys.platform == "linux", "GNU backtrace unwinding test 
requires Linux")
+class GnuBacktraceUnwindTests(unittest.TestCase):
+
+    def setUp(self):
+        super().setUp()
+        try:
+            _testinternalcapi.gnu_backtrace_unwind()
+        except RuntimeError as exc:
+            if "not supported" in str(exc):
+                self.skipTest("gnu backtrace unwinding not supported on this 
platform")
+            raise
+
+    def test_gnu_backtrace_unwinds_through_jit_frames(self):
+        jit_available = hasattr(sys, "_jit") and sys._jit.is_available()
+        envs = [({"PYTHON_JIT": "0"}, False)]
+        if jit_available:
+            envs.append(({"PYTHON_JIT": "1"}, True))
+
+        for env, using_jit in envs:
+            with self.subTest(env=env):
+                result = _unwind_result("gnu_backtrace_unwind", **env)
+                python_frames = result.get("python_frames", 0)
+                jit_frames = result.get("jit_frames", 0)
+                jit_backend = result.get("jit_backend")
+
+                self.assertGreaterEqual(
+                    python_frames,
+                    STACK_DEPTH,
+                    f"expected to find Python frames in GNU backtrace with env 
{env}",
+                )
+                if using_jit and jit_backend == "jit":
+                    self.assertGreater(
+                        jit_frames,
+                        0,
+                        f"expected GNU backtrace to include JIT frames with 
env {env}",
+                    )
+                else:
+                    self.assertEqual(
+                        jit_frames,
+                        0,
+                        f"unexpected JIT frames counted in GNU backtrace with 
env {env}",
+                    )
+
+    def test_gnu_backtrace_jit_frames_disappear_after_executor_free(self):
+        if not (hasattr(sys, "_jit") and sys._jit.is_available()):
+            self.skipTest("JIT is not available")
+
+        result = _unwind_after_executor_free_result(
+            "gnu_backtrace_unwind", PYTHON_JIT="1")
+        live = result["live"]
+        if live.get("jit_backend") != "jit":
+            self.skipTest("JIT backend is not active")
+
+        self.assertGreaterEqual(
+            live.get("python_frames", 0),
+            STACK_DEPTH,
+            "expected live GNU backtrace to include recursive Python frames",
+        )
+        self.assertGreater(
+            live.get("jit_frames", 0),
+            0,
+            "expected live GNU backtrace to include JIT frames",
+        )
+
+        after_free = result["after_free"]
+        self.assertGreater(
+            after_free.get("python_frames", 0),
+            0,
+            "expected GNU backtrace after executor free to include Python 
frames",
+        )
+        self.assertEqual(
+            after_free.get("jit_frames", 0),
+            0,
+            "unexpected JIT frames in GNU backtrace after executor free",
+        )
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/Makefile.pre.in b/Makefile.pre.in
index 2834226ff0c31f..fe16c73204dcf8 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -470,6 +470,7 @@ PYTHON_OBJS=        \
                Python/instruction_sequence.o \
                Python/intrinsics.o \
                Python/jit.o \
+               Python/jit_publish.o \
                $(JIT_OBJS) \
                Python/legacy_tracing.o \
                Python/lock.o \
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-02-18-02-41.gh-issue-126910.nqDVrp.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-02-18-02-41.gh-issue-126910.nqDVrp.rst
new file mode 100644
index 00000000000000..7103c7e9356042
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-02-18-02-41.gh-issue-126910.nqDVrp.rst
@@ -0,0 +1 @@
+Add support for unwinding JIT frames using GNU backtrace. Patch by Diego Russo 
and Pablo Galindo
diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c
index d85b9eb5f7da89..23ef4f13be3b5f 100644
--- a/Modules/_testinternalcapi.c
+++ b/Modules/_testinternalcapi.c
@@ -47,6 +47,9 @@
 #if defined(HAVE_DLADDR) && !defined(__wasi__)
 #  include <dlfcn.h>
 #endif
+#if defined(HAVE_EXECINFO_H)
+#  include <execinfo.h>
+#endif
 #ifdef MS_WINDOWS
 #  include <windows.h>
 #  include <intrin.h>
@@ -58,6 +61,7 @@
 
 
 static const uintptr_t min_frame_pointer_addr = 0x1000;
+#define MAX_UNWIND_FRAMES 200
 
 
 static PyObject *
@@ -328,7 +332,6 @@ get_jit_backend(PyObject *self, PyObject *Py_UNUSED(args))
 static PyObject *
 manual_unwind_from_fp(uintptr_t *frame_pointer)
 {
-    Py_ssize_t max_depth = 200;
     int stack_grows_down = _Py_STACK_GROWS_DOWN;
 
     if (frame_pointer == NULL) {
@@ -340,14 +343,20 @@ manual_unwind_from_fp(uintptr_t *frame_pointer)
         return NULL;
     }
 
-    for (Py_ssize_t depth = 0;
-         depth < max_depth && frame_pointer != NULL;
-         depth++)
-    {
+    Py_ssize_t depth = 0;
+    while (frame_pointer != NULL) {
         uintptr_t fp_addr = (uintptr_t)frame_pointer;
         if ((fp_addr % sizeof(uintptr_t)) != 0) {
             break;
         }
+        if (depth >= MAX_UNWIND_FRAMES) {
+            Py_DECREF(result);
+            PyErr_Format(
+                PyExc_RuntimeError,
+                "manual frame pointer unwind returned more than %d frames",
+                MAX_UNWIND_FRAMES);
+            return NULL;
+        }
         uintptr_t return_addr = frame_pointer[1];
 
         PyObject *addr_obj = PyLong_FromUnsignedLongLong(return_addr);
@@ -361,6 +370,7 @@ manual_unwind_from_fp(uintptr_t *frame_pointer)
             return NULL;
         }
         Py_DECREF(addr_obj);
+        depth++;
 
         uintptr_t *next_fp = (uintptr_t *)frame_pointer[0];
         // Stop if the frame pointer is extremely low.
@@ -383,6 +393,49 @@ manual_unwind_from_fp(uintptr_t *frame_pointer)
 
     return result;
 }
+
+#if defined(HAVE_EXECINFO_H) && defined(HAVE_BACKTRACE)
+static PyObject *
+gnu_backtrace_unwind(PyObject *self, PyObject *Py_UNUSED(args))
+{
+    void *addresses[MAX_UNWIND_FRAMES + 1];
+    int frame_count = backtrace(addresses, (int)Py_ARRAY_LENGTH(addresses));
+    if (frame_count < 0) {
+        PyErr_SetString(PyExc_RuntimeError, "backtrace() failed");
+        return NULL;
+    }
+    if (frame_count > MAX_UNWIND_FRAMES) {
+        PyErr_Format(
+            PyExc_RuntimeError,
+            "backtrace() returned more than %d frames",
+            MAX_UNWIND_FRAMES);
+        return NULL;
+    }
+
+    PyObject *result = PyList_New(frame_count);
+    if (result == NULL) {
+        return NULL;
+    }
+    for (int i = 0; i < frame_count; i++) {
+        PyObject *addr_obj = 
PyLong_FromUnsignedLongLong((uintptr_t)addresses[i]);
+        if (addr_obj == NULL) {
+            Py_DECREF(result);
+            return NULL;
+        }
+        PyList_SET_ITEM(result, i, addr_obj);
+    }
+    return result;
+}
+#else
+static PyObject *
+gnu_backtrace_unwind(PyObject *self, PyObject *Py_UNUSED(args))
+{
+    PyErr_SetString(PyExc_RuntimeError,
+                    "gnu_backtrace_unwind is not supported on this platform");
+    return NULL;
+}
+#endif
+
 #if defined(__GNUC__) || defined(__clang__)
 static PyObject *
 manual_frame_pointer_unwind(PyObject *self, PyObject *args)
@@ -2914,6 +2967,7 @@ static PyMethodDef module_functions[] = {
     {"classify_stack_addresses", classify_stack_addresses, METH_VARARGS},
     {"get_jit_code_ranges", get_jit_code_ranges, METH_NOARGS},
     {"get_jit_backend", get_jit_backend, METH_NOARGS},
+    {"gnu_backtrace_unwind", gnu_backtrace_unwind, METH_NOARGS},
     {"manual_frame_pointer_unwind", manual_frame_pointer_unwind, METH_NOARGS},
     {"test_bswap", test_bswap, METH_NOARGS},
     {"test_popcount", test_popcount, METH_NOARGS},
diff --git a/PCbuild/_freeze_module.vcxproj b/PCbuild/_freeze_module.vcxproj
index aae8e59faad755..17b98c9d9ec345 100644
--- a/PCbuild/_freeze_module.vcxproj
+++ b/PCbuild/_freeze_module.vcxproj
@@ -237,6 +237,7 @@
     <ClCompile Include="..\Python\intrinsics.c" />
     <ClCompile Include="..\Python\instrumentation.c" />
     <ClCompile Include="..\Python\jit.c" />
+    <ClCompile Include="..\Python\jit_publish.c" />
     <ClCompile Include="..\Python\legacy_tracing.c" />
     <ClCompile Include="..\Python\lock.c" />
     <ClCompile Include="..\Python\marshal.c" />
diff --git a/PCbuild/_freeze_module.vcxproj.filters 
b/PCbuild/_freeze_module.vcxproj.filters
index e76acb5d323858..af3fded0dabf2d 100644
--- a/PCbuild/_freeze_module.vcxproj.filters
+++ b/PCbuild/_freeze_module.vcxproj.filters
@@ -265,6 +265,9 @@
     <ClCompile Include="..\Python\jit.c">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\Python\jit_publish.c">
+      <Filter>Source Files</Filter>
+    </ClCompile>
     <ClCompile Include="..\Objects\lazyimportobject.c">
       <Filter>Source Files</Filter>
     </ClCompile>
diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj
index e306fe9a9615ad..e255ed5af19125 100644
--- a/PCbuild/pythoncore.vcxproj
+++ b/PCbuild/pythoncore.vcxproj
@@ -655,6 +655,7 @@
     <ClCompile Include="..\Python\instruction_sequence.c" />
     <ClCompile Include="..\Python\instrumentation.c" />
     <ClCompile Include="..\Python\jit.c" />
+    <ClCompile Include="..\Python\jit_publish.c" />
     <ClCompile Include="..\Python\legacy_tracing.c" />
     <ClCompile Include="..\Python\lock.c" />
     <ClCompile Include="..\Python\marshal.c" />
diff --git a/PCbuild/pythoncore.vcxproj.filters 
b/PCbuild/pythoncore.vcxproj.filters
index acabcdc8575d48..649ee1859ff996 100644
--- a/PCbuild/pythoncore.vcxproj.filters
+++ b/PCbuild/pythoncore.vcxproj.filters
@@ -1493,6 +1493,9 @@
     <ClCompile Include="..\Python\jit.c">
       <Filter>Python</Filter>
     </ClCompile>
+    <ClCompile Include="..\Python\jit_publish.c">
+      <Filter>Python</Filter>
+    </ClCompile>
     <ClCompile Include="..\Python\legacy_tracing.c">
       <Filter>Source Files</Filter>
     </ClCompile>
diff --git a/Python/jit.c b/Python/jit.c
index 8b555105129a9f..5c8f87857731fc 100644
--- a/Python/jit.c
+++ b/Python/jit.c
@@ -15,7 +15,7 @@
 #include "pycore_interpframe.h"
 #include "pycore_interpolation.h"
 #include "pycore_intrinsics.h"
-#include "pycore_jit_unwind.h"
+#include "pycore_jit_publish.h"
 #include "pycore_lazyimportobject.h"
 #include "pycore_list.h"
 #include "pycore_long.h"
@@ -61,40 +61,6 @@ jit_error(const char *message)
     PyErr_Format(PyExc_RuntimeWarning, "JIT %s (%d)", message, hint);
 }
 
-/*
- * Publish JIT code to optional tooling backends.
- *
- * The return value is a backend-specific deregistration handle, not a
- * success/failure indicator. NULL means there is nothing to unregister later:
- * perf does not need a handle, and GDB registration failures are intentionally
- * non-fatal because tooling support must not make JIT compilation fail.
- */
-static void *
-jit_record_code(const void *code_addr, size_t code_size,
-                const char *entry, const char *filename)
-{
-#ifdef PY_HAVE_PERF_TRAMPOLINE
-    _PyPerf_Callbacks callbacks;
-    _PyPerfTrampoline_GetCallbacks(&callbacks);
-    if (callbacks.write_state == _Py_perfmap_jit_callbacks.write_state) {
-        _PyPerfJit_WriteNamedCode(
-            code_addr, code_size, entry, filename);
-        return NULL;
-    }
-#endif
-
-#if defined(PY_HAVE_JIT_GDB_UNWIND)
-    return _PyJitUnwind_GdbRegisterCode(
-        code_addr, code_size, entry, filename);
-#else
-    (void)code_addr;
-    (void)code_size;
-    (void)entry;
-    (void)filename;
-    return NULL;
-#endif
-}
-
 static int
 address_in_executor_array(_PyExecutorObject **ptrs, size_t count, uintptr_t 
addr)
 {
@@ -750,10 +716,11 @@ _PyJIT_Compile(_PyExecutorObject *executor, const 
_PyUOpInstruction trace[], siz
     }
     executor->jit_code = memory;
     executor->jit_size = total_size;
-    executor->jit_gdb_handle = jit_record_code(memory,
-                    code_size + state.trampolines.size,
-                    "jit",
-                    "executor");
+    executor->jit_registration = _PyJit_RegisterCode(
+        memory,
+        code_size + state.trampolines.size,
+        "jit",
+        "executor");
     return 0;
 }
 
@@ -766,12 +733,8 @@ _PyJIT_Free(_PyExecutorObject *executor)
     if (memory) {
         executor->jit_code = NULL;
         executor->jit_size = 0;
-#if defined(PY_HAVE_JIT_GDB_UNWIND)
-        if (executor->jit_gdb_handle != NULL) {
-            _PyJitUnwind_GdbUnregisterCode(executor->jit_gdb_handle);
-            executor->jit_gdb_handle = NULL;
-        }
-#endif
+        _PyJit_UnregisterCode(executor->jit_registration);
+        executor->jit_registration = NULL;
         if (jit_free(memory, size)) {
             PyErr_FormatUnraisable("Exception ignored while "
                                    "freeing JIT memory");
diff --git a/Python/jit_publish.c b/Python/jit_publish.c
new file mode 100644
index 00000000000000..fd068076a7709e
--- /dev/null
+++ b/Python/jit_publish.c
@@ -0,0 +1,137 @@
+#include "Python.h"
+
+#include "pycore_ceval.h"
+#include "pycore_jit_publish.h"
+#include "pycore_jit_unwind.h"
+
+#ifdef _Py_JIT
+
+#if defined(PY_HAVE_JIT_GDB_UNWIND) \
+    || defined(PY_HAVE_JIT_GNU_BACKTRACE_UNWIND)
+struct _PyJitCodeRegistration {
+    void *gdb_handle;
+    void *gnu_backtrace_handle;
+};
+#endif
+
+static void
+jit_register_perf_code(const void *code_addr, size_t code_size,
+                       const char *entry, const char *filename)
+{
+#ifdef PY_HAVE_PERF_TRAMPOLINE
+    _PyPerf_Callbacks callbacks;
+    _PyPerfTrampoline_GetCallbacks(&callbacks);
+    if (callbacks.write_state == _Py_perfmap_jit_callbacks.write_state) {
+        _PyPerfJit_WriteNamedCode(
+            code_addr, code_size, entry, filename);
+    }
+#else
+    (void)code_addr;
+    (void)code_size;
+    (void)entry;
+    (void)filename;
+#endif
+}
+
+#if defined(PY_HAVE_JIT_GDB_UNWIND)
+static void
+jit_register_gdb_code(_PyJitCodeRegistration *registration,
+                      const void *code_addr, size_t code_size,
+                      const char *entry, const char *filename)
+{
+    registration->gdb_handle = _PyJitUnwind_GdbRegisterCode(
+        code_addr, code_size, entry, filename);
+}
+
+static void
+jit_unregister_gdb_code(_PyJitCodeRegistration *registration)
+{
+    if (registration->gdb_handle != NULL) {
+        _PyJitUnwind_GdbUnregisterCode(registration->gdb_handle);
+        registration->gdb_handle = NULL;
+    }
+}
+#endif
+
+#if defined(PY_HAVE_JIT_GNU_BACKTRACE_UNWIND)
+static void
+jit_register_gnu_backtrace_code(_PyJitCodeRegistration *registration,
+                                const void *code_addr, size_t code_size)
+{
+    registration->gnu_backtrace_handle =
+        _PyJitUnwind_GnuBacktraceRegisterCode(code_addr, code_size);
+}
+
+static void
+jit_unregister_gnu_backtrace_code(_PyJitCodeRegistration *registration)
+{
+    if (registration->gnu_backtrace_handle != NULL) {
+        _PyJitUnwind_GnuBacktraceUnregisterCode(
+            registration->gnu_backtrace_handle);
+        registration->gnu_backtrace_handle = NULL;
+    }
+}
+#endif
+
+_PyJitCodeRegistration *
+_PyJit_RegisterCode(const void *code_addr, size_t code_size,
+                    const char *entry, const char *filename)
+{
+    jit_register_perf_code(code_addr, code_size, entry, filename);
+    // Perf publication has no teardown handle, so it is intentionally
+    // not counted below.
+
+#if !defined(PY_HAVE_JIT_GDB_UNWIND) \
+    && !defined(PY_HAVE_JIT_GNU_BACKTRACE_UNWIND)
+    return NULL;
+#else
+    _PyJitCodeRegistration *registration = PyMem_RawCalloc(
+        1, sizeof(*registration));
+    if (registration == NULL) {
+        return NULL;
+    }
+
+    // Partial failures are non-fatal: the JIT code can still execute, but
+    // unavailable tooling may not be able to unwind it.
+    int any_registered = 0;
+#  if defined(PY_HAVE_JIT_GDB_UNWIND)
+    jit_register_gdb_code(
+        registration, code_addr, code_size, entry, filename);
+    any_registered |= registration->gdb_handle != NULL;
+#  endif
+#  if defined(PY_HAVE_JIT_GNU_BACKTRACE_UNWIND)
+    jit_register_gnu_backtrace_code(
+        registration, code_addr, code_size);
+    any_registered |= registration->gnu_backtrace_handle != NULL;
+#  endif
+    if (!any_registered) {
+        PyMem_RawFree(registration);
+        return NULL;
+    }
+    return registration;
+#endif
+}
+
+void
+_PyJit_UnregisterCode(_PyJitCodeRegistration *registration)
+{
+#if !defined(PY_HAVE_JIT_GDB_UNWIND) \
+    && !defined(PY_HAVE_JIT_GNU_BACKTRACE_UNWIND)
+    assert(registration == NULL);
+    (void)registration;
+#else
+    if (registration == NULL) {
+        return;
+    }
+
+#  if defined(PY_HAVE_JIT_GNU_BACKTRACE_UNWIND)
+    jit_unregister_gnu_backtrace_code(registration);
+#  endif
+#  if defined(PY_HAVE_JIT_GDB_UNWIND)
+    jit_unregister_gdb_code(registration);
+#  endif
+    PyMem_RawFree(registration);
+#endif
+}
+
+#endif  // _Py_JIT
diff --git a/Python/jit_unwind.c b/Python/jit_unwind.c
index 09002feafec17c..646106f0a9655c 100644
--- a/Python/jit_unwind.c
+++ b/Python/jit_unwind.c
@@ -16,11 +16,22 @@
 #  endif
 #endif
 
-#if defined(PY_HAVE_PERF_TRAMPOLINE) || defined(PY_HAVE_JIT_GDB_UNWIND)
+#if defined(PY_HAVE_PERF_TRAMPOLINE) \
+    || defined(PY_HAVE_JIT_GDB_UNWIND) \
+    || defined(PY_HAVE_JIT_GNU_BACKTRACE_UNWIND)
 
 #if defined(PY_HAVE_JIT_GDB_UNWIND)
 #  include <elf.h>
 #endif
+#if defined(PY_HAVE_JIT_GNU_BACKTRACE_UNWIND)
+/*
+ * libgcc exposes frame registration entry points, but GCC's public headers
+ * on some distributions do not declare them even though the symbols are
+ * available in libgcc_s.
+ */
+void __register_frame(const void *);
+void __deregister_frame(const void *);
+#endif
 #include <stdio.h>
 #include <string.h>
 
@@ -983,4 +994,56 @@ _PyJitUnwind_GdbUnregisterCode(void *handle)
 #endif
 }
 
-#endif  // defined(PY_HAVE_PERF_TRAMPOLINE) || defined(PY_HAVE_JIT_GDB_UNWIND)
+#if defined(PY_HAVE_JIT_GNU_BACKTRACE_UNWIND)
+void *
+_PyJitUnwind_GnuBacktraceRegisterCode(const void *code_addr, size_t code_size)
+{
+    if (code_addr == NULL || code_size == 0) {
+        return NULL;
+    }
+
+    size_t eh_frame_size = _PyJitUnwind_EhFrameSize(1);
+    if (eh_frame_size == 0) {
+        return NULL;
+    }
+    size_t total_size = eh_frame_size + sizeof(uint32_t);
+    if (total_size < eh_frame_size) {
+        return NULL;
+    }
+
+    /*
+     * libgcc's __register_frame walks a .eh_frame section until it finds a
+     * zero-length terminator entry, so keep an extra zeroed word after the
+     * generated CIE/FDE pair.
+     *
+     * See GCC's libgcc/unwind-dw2-fde.c (__register_frame) and
+     * libgcc/unwind-dw2-fde.h (last_fde/next_fde):
+     * https://github.com/gcc-mirror/gcc/blob/master/libgcc/unwind-dw2-fde.c
+     * https://github.com/gcc-mirror/gcc/blob/master/libgcc/unwind-dw2-fde.h
+     */
+    uint8_t *eh_frame = PyMem_RawCalloc(1, total_size);
+    if (eh_frame == NULL) {
+        return NULL;
+    }
+    if (_PyJitUnwind_BuildEhFrame(
+            eh_frame, eh_frame_size, code_addr, code_size, 1) == 0) {
+        PyMem_RawFree(eh_frame);
+        return NULL;
+    }
+
+    __register_frame(eh_frame);
+    return eh_frame;
+}
+
+void
+_PyJitUnwind_GnuBacktraceUnregisterCode(void *handle)
+{
+    if (handle == NULL) {
+        return;
+    }
+    __deregister_frame(handle);
+    PyMem_RawFree(handle);
+}
+#endif  // defined(PY_HAVE_JIT_GNU_BACKTRACE_UNWIND)
+
+#endif  // JIT unwind support
diff --git a/Python/optimizer.c b/Python/optimizer.c
index 765bab9726088d..c8ce4396b426e4 100644
--- a/Python/optimizer.c
+++ b/Python/optimizer.c
@@ -1449,7 +1449,7 @@ allocate_executor(int exit_count, int length)
     res->trace = (_PyUOpInstruction *)(res->exits + exit_count);
     res->code_size = length;
     res->exit_count = exit_count;
-    res->jit_gdb_handle = NULL;
+    res->jit_registration = NULL;
     return res;
 }
 
diff --git a/configure b/configure
index 44ba9a211fefcc..627f5ee4888a1f 100755
--- a/configure
+++ b/configure
@@ -14424,6 +14424,52 @@ fi
 
 done
 
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for libgcc frame 
registration functions" >&5
+printf %s "checking for libgcc frame registration functions... " >&6; }
+if test ${ac_cv_have_libgcc_eh_frame_registration+y}
+then :
+  printf %s "(cached) " >&6
+else case e in #(
+  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+void __register_frame(const void *);
+void __deregister_frame(const void *);
+
+int
+main (void)
+{
+
+__register_frame(0);
+__deregister_frame(0);
+
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+  ac_cv_have_libgcc_eh_frame_registration=yes
+else case e in #(
+  e) ac_cv_have_libgcc_eh_frame_registration=no ;;
+esac
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+    conftest$ac_exeext conftest.$ac_ext
+   ;;
+esac
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: 
$ac_cv_have_libgcc_eh_frame_registration" >&5
+printf "%s\n" "$ac_cv_have_libgcc_eh_frame_registration" >&6; }
+if test "x$ac_cv_have_libgcc_eh_frame_registration" = xyes
+then :
+
+
+printf "%s\n" "#define HAVE_LIBGCC_EH_FRAME_REGISTRATION 1" >>confdefs.h
+
+
+fi
+
 if test "x$ac_cv_require_ldl" = xyes
 then :
 
diff --git a/configure.ac b/configure.ac
index b5a32d4b090175..4f968b25d91566 100644
--- a/configure.ac
+++ b/configure.ac
@@ -3828,6 +3828,24 @@ AC_CHECK_HEADERS([execinfo.h link.h dlfcn.h], [
   ])
 ])
 
+dnl for JIT GNU backtrace unwind registration
+AC_CACHE_CHECK([for libgcc frame registration functions],
+  [ac_cv_have_libgcc_eh_frame_registration],
+  [AC_LINK_IFELSE([AC_LANG_PROGRAM([[
+void __register_frame(const void *);
+void __deregister_frame(const void *);
+]], [[
+__register_frame(0);
+__deregister_frame(0);
+]])],
+    [ac_cv_have_libgcc_eh_frame_registration=yes],
+    [ac_cv_have_libgcc_eh_frame_registration=no])
+  ])
+AS_VAR_IF([ac_cv_have_libgcc_eh_frame_registration], [yes], [
+  AC_DEFINE([HAVE_LIBGCC_EH_FRAME_REGISTRATION], [1],
+    [Define to 1 if libgcc __register_frame and __deregister_frame are 
linkable.])
+])
+
 dnl only add -ldl to LDFLAGS if it isn't already part of LIBS (GH-133081)
 AS_VAR_IF([ac_cv_require_ldl], [yes], [
   AS_VAR_IF([ac_cv_lib_dl_dlopen], [yes], [], [
diff --git a/pyconfig.h.in b/pyconfig.h.in
index 9da33c954a52f8..4eeec330466441 100644
--- a/pyconfig.h.in
+++ b/pyconfig.h.in
@@ -701,6 +701,10 @@
 /* Define to 1 if you have the 'dld' library (-ldld). */
 #undef HAVE_LIBDLD
 
+/* Define to 1 if libgcc __register_frame and __deregister_frame are linkable.
+   */
+#undef HAVE_LIBGCC_EH_FRAME_REGISTRATION
+
 /* Define to 1 if you have the 'ieee' library (-lieee). */
 #undef HAVE_LIBIEEE
 

_______________________________________________
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