GH-126910: Add GNU backtrace support for unwinding JIT frames

This commit is contained in:
Diego Russo 2026-04-24 14:47:34 +01:00
parent efcac6f281
commit db967dca64
14 changed files with 366 additions and 63 deletions

View file

@ -0,0 +1,27 @@
#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
/* Return a teardown handle when any backend stores registration state.
* A NULL result is valid when publication succeeded only through backends
* with no unregister step, such as perf map output.
*/
_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

View file

@ -10,9 +10,12 @@
#if defined(_Py_JIT) && defined(__linux__) && defined(__ELF__)
# define PY_HAVE_JIT_GDB_UNWIND
# define PY_HAVE_JIT_GNU_BACKTRACE_UNWIND
#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 +66,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

View file

@ -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;

View file

@ -70,7 +70,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 +89,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(10, unwinder)
return stack
@ -112,8 +112,9 @@ def _classify_stack(stack, jit_enabled):
return annotated, python_frames, jit_frames, other_frames
def _annotate_unwind():
stack = _build_stack_and_unwind()
def _annotate_unwind(unwinder_name="manual_frame_pointer_unwind"):
unwinder = getattr(_testinternalcapi, unwinder_name)
stack = _build_stack_and_unwind(unwinder)
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 []
@ -132,13 +133,14 @@ def _annotate_unwind():
"jit_frames": jit_frames,
"other_frames": other_frames,
"jit_backend": jit_backend,
"unwinder": unwinder_name,
})
def _manual_unwind_length(**env):
def _unwind_result(unwinder_name, **env):
code = (
"from test.test_frame_pointer_unwind import _annotate_unwind; "
"print(_annotate_unwind());"
f"print(_annotate_unwind({unwinder_name!r}));"
)
run_env = os.environ.copy()
run_env.update(env)
@ -197,7 +199,7 @@ 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")
@ -240,5 +242,51 @@ 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.assertGreater(
python_frames,
0,
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}",
)
if __name__ == "__main__":
unittest.main()

View file

@ -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 \

View file

@ -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)
@ -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},

View file

@ -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" />

View file

@ -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>

View file

@ -648,6 +648,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" />

View file

@ -1481,6 +1481,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>

View file

@ -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");

128
Python/jit_publish.c Normal file
View file

@ -0,0 +1,128 @@
#include "Python.h"
#include "pycore_ceval.h"
#include "pycore_jit_publish.h"
#include "pycore_jit_unwind.h"
#ifdef _Py_JIT
struct _PyJitCodeRegistration {
void *gdb_handle;
void *gnu_backtrace_handle;
};
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
}
static void
jit_register_gdb_code(_PyJitCodeRegistration *registration,
const void *code_addr, size_t code_size,
const char *entry, const char *filename)
{
#if defined(PY_HAVE_JIT_GDB_UNWIND)
registration->gdb_handle = _PyJitUnwind_GdbRegisterCode(
code_addr, code_size, entry, filename);
#else
(void)registration;
(void)code_addr;
(void)code_size;
(void)entry;
(void)filename;
#endif
}
static void
jit_unregister_gdb_code(_PyJitCodeRegistration *registration)
{
#if defined(PY_HAVE_JIT_GDB_UNWIND)
if (registration->gdb_handle != NULL) {
_PyJitUnwind_GdbUnregisterCode(registration->gdb_handle);
registration->gdb_handle = NULL;
}
#else
(void)registration;
#endif
}
static void
jit_register_gnu_backtrace_code(_PyJitCodeRegistration *registration,
const void *code_addr, size_t code_size)
{
#if defined(PY_HAVE_JIT_GNU_BACKTRACE_UNWIND)
registration->gnu_backtrace_handle =
_PyJitUnwind_GnuBacktraceRegisterCode(code_addr, code_size);
#else
(void)registration;
(void)code_addr;
(void)code_size;
#endif
}
static void
jit_unregister_gnu_backtrace_code(_PyJitCodeRegistration *registration)
{
#if defined(PY_HAVE_JIT_GNU_BACKTRACE_UNWIND)
if (registration->gnu_backtrace_handle != NULL) {
_PyJitUnwind_GnuBacktraceUnregisterCode(
registration->gnu_backtrace_handle);
registration->gnu_backtrace_handle = NULL;
}
#else
(void)registration;
#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);
_PyJitCodeRegistration *registration = PyMem_RawCalloc(
1, sizeof(*registration));
if (registration == NULL) {
return NULL;
}
jit_register_gdb_code(
registration, code_addr, code_size, entry, filename);
jit_register_gnu_backtrace_code(
registration, code_addr, code_size);
if (registration->gdb_handle == NULL &&
registration->gnu_backtrace_handle == NULL)
{
PyMem_RawFree(registration);
return NULL;
}
return registration;
}
void
_PyJit_UnregisterCode(_PyJitCodeRegistration *registration)
{
if (registration == NULL) {
return;
}
jit_unregister_gnu_backtrace_code(registration);
jit_unregister_gdb_code(registration);
PyMem_RawFree(registration);
}
#endif // _Py_JIT

View file

@ -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

View file

@ -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;
}