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 + +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): ) +@support.requires_gil_enabled("test requires the GIL enabled") +@unittest.skipIf(support.is_wasi, "test not supported on WASI") +@unittest.skipUnless(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 fc44399434fe95..7a31db5372a660 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 a07675bb66d8cc..dc0cfcbf3a55a9 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -47,6 +47,9 @@ #if defined(HAVE_DLADDR) && !defined(__wasi__) # include #endif +#if defined(HAVE_EXECINFO_H) +# include +#endif #ifdef MS_WINDOWS # include # include @@ -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) @@ -2915,6 +2968,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 953973a2ad32df..a8126d44c708b6 100644 --- a/PCbuild/_freeze_module.vcxproj +++ b/PCbuild/_freeze_module.vcxproj @@ -237,6 +237,7 @@ + diff --git a/PCbuild/_freeze_module.vcxproj.filters b/PCbuild/_freeze_module.vcxproj.filters index 13db4d93f54518..c84c32ccd181fd 100644 --- a/PCbuild/_freeze_module.vcxproj.filters +++ b/PCbuild/_freeze_module.vcxproj.filters @@ -265,6 +265,9 @@ Source Files + + Source Files + Source Files diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj index fae4a90b4536fc..1c4a468d389288 100644 --- a/PCbuild/pythoncore.vcxproj +++ b/PCbuild/pythoncore.vcxproj @@ -648,6 +648,7 @@ + diff --git a/PCbuild/pythoncore.vcxproj.filters b/PCbuild/pythoncore.vcxproj.filters index 04b6641ae30e7f..a631d42fe5b457 100644 --- a/PCbuild/pythoncore.vcxproj.filters +++ b/PCbuild/pythoncore.vcxproj.filters @@ -1481,6 +1481,9 @@ Python + + Python + Source Files 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..bbe27056094ff1 --- /dev/null +++ b/Python/jit_publish.c @@ -0,0 +1,141 @@ +#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 { +# if defined(PY_HAVE_JIT_GDB_UNWIND) + void *gdb_handle; +# endif +# if defined(PY_HAVE_JIT_GNU_BACKTRACE_UNWIND) + void *gnu_backtrace_handle; +# endif +}; +#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 #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 #include @@ -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 11658fca0da595..bd5c969613483c 100644 --- a/Python/optimizer.c +++ b/Python/optimizer.c @@ -1448,7 +1448,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 734aa3a6a721d1..d978397a9ff67e 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 c8cb1686d55c07..473502a285da1d 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