Skip to content

Commit 1e5d942

Browse files
GH-126910: Add GNU backtrace support for unwinding JIT frames (#149104)
Co-authored-by: Pablo Galindo Salgado <pablogsal@gmail.com>
1 parent ddfdf9b commit 1e5d942

18 files changed

Lines changed: 522 additions & 67 deletions
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#ifndef Py_INTERNAL_JIT_PUBLISH_H
2+
#define Py_INTERNAL_JIT_PUBLISH_H
3+
4+
#ifndef Py_BUILD_CORE
5+
# error "this header requires Py_BUILD_CORE define"
6+
#endif
7+
8+
#include <stddef.h>
9+
10+
typedef struct _PyJitCodeRegistration _PyJitCodeRegistration;
11+
12+
#ifdef _Py_JIT
13+
14+
/* Publish JIT code to optional tooling backends.
15+
*
16+
* The return value is a backend-specific deregistration handle, not a
17+
* success/failure indicator. NULL means there is nothing to unregister later:
18+
* perf does not need a handle, and GDB/GNU backtrace registration failures
19+
* are intentionally non-fatal because tooling support must not make JIT
20+
* compilation fail.
21+
*/
22+
_PyJitCodeRegistration *_PyJit_RegisterCode(const void *code_addr,
23+
size_t code_size,
24+
const char *entry,
25+
const char *filename);
26+
27+
void _PyJit_UnregisterCode(_PyJitCodeRegistration *registration);
28+
29+
#endif // _Py_JIT
30+
31+
#endif // Py_INTERNAL_JIT_PUBLISH_H

Include/internal/pycore_jit_unwind.h

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@
1010

1111
#if defined(_Py_JIT) && defined(__linux__) && defined(__ELF__)
1212
# define PY_HAVE_JIT_GDB_UNWIND
13+
# if defined(HAVE_EXECINFO_H) && defined(HAVE_BACKTRACE) && \
14+
defined(HAVE_LIBGCC_EH_FRAME_REGISTRATION)
15+
# define PY_HAVE_JIT_GNU_BACKTRACE_UNWIND
16+
# endif
1317
#endif
1418

15-
#if defined(PY_HAVE_PERF_TRAMPOLINE) || defined(PY_HAVE_JIT_GDB_UNWIND)
19+
#if defined(PY_HAVE_PERF_TRAMPOLINE) \
20+
|| defined(PY_HAVE_JIT_GDB_UNWIND) \
21+
|| defined(PY_HAVE_JIT_GNU_BACKTRACE_UNWIND)
1622

1723
#if defined(PY_HAVE_JIT_GDB_UNWIND)
1824
extern PyMutex _Py_jit_debug_mutex;
@@ -63,6 +69,13 @@ void *_PyJitUnwind_GdbRegisterCode(const void *code_addr,
6369

6470
void _PyJitUnwind_GdbUnregisterCode(void *handle);
6571

66-
#endif // defined(PY_HAVE_PERF_TRAMPOLINE) || defined(PY_HAVE_JIT_GDB_UNWIND)
72+
#if defined(PY_HAVE_JIT_GNU_BACKTRACE_UNWIND)
73+
void *_PyJitUnwind_GnuBacktraceRegisterCode(const void *code_addr,
74+
size_t code_size);
75+
76+
void _PyJitUnwind_GnuBacktraceUnregisterCode(void *handle);
77+
#endif
78+
79+
#endif // JIT unwind support
6780

6881
#endif // Py_INTERNAL_JIT_UNWIND_H

Include/internal/pycore_optimizer.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ extern "C" {
99
#endif
1010

1111
#include "pycore_typedefs.h" // _PyInterpreterFrame
12+
#include "pycore_jit_publish.h"
1213
#include "pycore_uop.h" // _PyUOpInstruction
1314
#include "pycore_uop_ids.h"
1415
#include "pycore_stackref.h" // _PyStackRef
@@ -198,7 +199,7 @@ typedef struct _PyExecutorObject {
198199
uint32_t code_size;
199200
size_t jit_size;
200201
void *jit_code;
201-
void *jit_gdb_handle;
202+
_PyJitCodeRegistration *jit_registration;
202203
_PyExitData exits[1];
203204
} _PyExecutorObject;
204205

Lib/test/test_frame_pointer_unwind.py

Lines changed: 126 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
raise unittest.SkipTest("test requires subprocess support")
1818

1919

20+
STACK_DEPTH = 10
21+
22+
2023
def _frame_pointers_expected(machine):
2124
cflags = " ".join(
2225
value for value in (
@@ -70,7 +73,7 @@ def _frame_pointers_expected(machine):
7073
return None
7174

7275

73-
def _build_stack_and_unwind():
76+
def _build_stack_and_unwind(unwinder):
7477
import operator
7578

7679
def build_stack(n, unwinder, warming_up_caller=False):
@@ -89,7 +92,7 @@ def build_stack(n, unwinder, warming_up_caller=False):
8992
result = operator.call(build_stack, n - 1, unwinder, warming_up)
9093
return result
9194

92-
stack = build_stack(10, _testinternalcapi.manual_frame_pointer_unwind)
95+
stack = build_stack(STACK_DEPTH, unwinder)
9396
return stack
9497

9598

@@ -112,8 +115,7 @@ def _classify_stack(stack, jit_enabled):
112115
return annotated, python_frames, jit_frames, other_frames
113116

114117

115-
def _annotate_unwind():
116-
stack = _build_stack_and_unwind()
118+
def _summarize_unwind(stack, unwinder_name):
117119
jit_enabled = hasattr(sys, "_jit") and sys._jit.is_enabled()
118120
jit_backend = _testinternalcapi.get_jit_backend()
119121
ranges = _testinternalcapi.get_jit_code_ranges() if jit_enabled else []
@@ -126,19 +128,44 @@ def _annotate_unwind():
126128
)
127129
for idx, addr, tag in annotated:
128130
print(f"#{idx:02d} {addr:#x} -> {tag}")
129-
return json.dumps({
131+
return {
130132
"length": len(stack),
131133
"python_frames": python_frames,
132134
"jit_frames": jit_frames,
133135
"other_frames": other_frames,
134136
"jit_backend": jit_backend,
137+
"unwinder": unwinder_name,
138+
}
139+
140+
141+
def _annotate_unwind(unwinder_name="manual_frame_pointer_unwind"):
142+
unwinder = getattr(_testinternalcapi, unwinder_name)
143+
stack = _build_stack_and_unwind(unwinder)
144+
return json.dumps(_summarize_unwind(stack, unwinder_name))
145+
146+
147+
def _annotate_unwind_after_executor_free(unwinder_name="gnu_backtrace_unwind"):
148+
# The first unwind runs at the bottom of _build_stack_and_unwind(), while
149+
# the recursive helper may be executing in JIT code. After it returns, this
150+
# helper is back in normal test code; clearing executor caches should remove
151+
# the old JIT ranges, so the second unwind must not report stale JIT frames.
152+
live = json.loads(_annotate_unwind(unwinder_name))
153+
154+
sys._clear_internal_caches()
155+
_testinternalcapi.clear_executor_deletion_list()
156+
157+
unwinder = getattr(_testinternalcapi, unwinder_name)
158+
after_free = _summarize_unwind(unwinder(), unwinder_name)
159+
return json.dumps({
160+
"live": live,
161+
"after_free": after_free,
135162
})
136163

137164

138-
def _manual_unwind_length(**env):
165+
def _run_unwind_helper(helper_name, unwinder_name, **env):
139166
code = (
140-
"from test.test_frame_pointer_unwind import _annotate_unwind; "
141-
"print(_annotate_unwind());"
167+
f"from test.test_frame_pointer_unwind import {helper_name}; "
168+
f"print({helper_name}({unwinder_name!r}));"
142169
)
143170
run_env = os.environ.copy()
144171
run_env.update(env)
@@ -166,6 +193,15 @@ def _manual_unwind_length(**env):
166193
) from exc
167194

168195

196+
def _unwind_result(unwinder_name, **env):
197+
return _run_unwind_helper("_annotate_unwind", unwinder_name, **env)
198+
199+
200+
def _unwind_after_executor_free_result(unwinder_name, **env):
201+
return _run_unwind_helper(
202+
"_annotate_unwind_after_executor_free", unwinder_name, **env)
203+
204+
169205
@support.requires_gil_enabled("test requires the GIL enabled")
170206
@unittest.skipIf(support.is_wasi, "test not supported on WASI")
171207
class FramePointerUnwindTests(unittest.TestCase):
@@ -197,14 +233,14 @@ def test_manual_unwind_respects_frame_pointers(self):
197233

198234
for env, using_jit in envs:
199235
with self.subTest(env=env):
200-
result = _manual_unwind_length(**env)
236+
result = _unwind_result("manual_frame_pointer_unwind", **env)
201237
jit_frames = result["jit_frames"]
202238
python_frames = result.get("python_frames", 0)
203239
jit_backend = result.get("jit_backend")
204240
if self.frame_pointers_expected:
205-
self.assertGreater(
241+
self.assertGreaterEqual(
206242
python_frames,
207-
0,
243+
STACK_DEPTH,
208244
f"expected to find Python frames on {self.machine} with env {env}",
209245
)
210246
if using_jit:
@@ -240,5 +276,84 @@ def test_manual_unwind_respects_frame_pointers(self):
240276
)
241277

242278

279+
@support.requires_gil_enabled("test requires the GIL enabled")
280+
@unittest.skipIf(support.is_wasi, "test not supported on WASI")
281+
@unittest.skipUnless(sys.platform == "linux", "GNU backtrace unwinding test requires Linux")
282+
class GnuBacktraceUnwindTests(unittest.TestCase):
283+
284+
def setUp(self):
285+
super().setUp()
286+
try:
287+
_testinternalcapi.gnu_backtrace_unwind()
288+
except RuntimeError as exc:
289+
if "not supported" in str(exc):
290+
self.skipTest("gnu backtrace unwinding not supported on this platform")
291+
raise
292+
293+
def test_gnu_backtrace_unwinds_through_jit_frames(self):
294+
jit_available = hasattr(sys, "_jit") and sys._jit.is_available()
295+
envs = [({"PYTHON_JIT": "0"}, False)]
296+
if jit_available:
297+
envs.append(({"PYTHON_JIT": "1"}, True))
298+
299+
for env, using_jit in envs:
300+
with self.subTest(env=env):
301+
result = _unwind_result("gnu_backtrace_unwind", **env)
302+
python_frames = result.get("python_frames", 0)
303+
jit_frames = result.get("jit_frames", 0)
304+
jit_backend = result.get("jit_backend")
305+
306+
self.assertGreaterEqual(
307+
python_frames,
308+
STACK_DEPTH,
309+
f"expected to find Python frames in GNU backtrace with env {env}",
310+
)
311+
if using_jit and jit_backend == "jit":
312+
self.assertGreater(
313+
jit_frames,
314+
0,
315+
f"expected GNU backtrace to include JIT frames with env {env}",
316+
)
317+
else:
318+
self.assertEqual(
319+
jit_frames,
320+
0,
321+
f"unexpected JIT frames counted in GNU backtrace with env {env}",
322+
)
323+
324+
def test_gnu_backtrace_jit_frames_disappear_after_executor_free(self):
325+
if not (hasattr(sys, "_jit") and sys._jit.is_available()):
326+
self.skipTest("JIT is not available")
327+
328+
result = _unwind_after_executor_free_result(
329+
"gnu_backtrace_unwind", PYTHON_JIT="1")
330+
live = result["live"]
331+
if live.get("jit_backend") != "jit":
332+
self.skipTest("JIT backend is not active")
333+
334+
self.assertGreaterEqual(
335+
live.get("python_frames", 0),
336+
STACK_DEPTH,
337+
"expected live GNU backtrace to include recursive Python frames",
338+
)
339+
self.assertGreater(
340+
live.get("jit_frames", 0),
341+
0,
342+
"expected live GNU backtrace to include JIT frames",
343+
)
344+
345+
after_free = result["after_free"]
346+
self.assertGreater(
347+
after_free.get("python_frames", 0),
348+
0,
349+
"expected GNU backtrace after executor free to include Python frames",
350+
)
351+
self.assertEqual(
352+
after_free.get("jit_frames", 0),
353+
0,
354+
"unexpected JIT frames in GNU backtrace after executor free",
355+
)
356+
357+
243358
if __name__ == "__main__":
244359
unittest.main()

Makefile.pre.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ PYTHON_OBJS= \
470470
Python/instruction_sequence.o \
471471
Python/intrinsics.o \
472472
Python/jit.o \
473+
Python/jit_publish.o \
473474
$(JIT_OBJS) \
474475
Python/legacy_tracing.o \
475476
Python/lock.o \
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for unwinding JIT frames using GNU backtrace. Patch by Diego Russo and Pablo Galindo

0 commit comments

Comments
 (0)