-
-
Notifications
You must be signed in to change notification settings - Fork 34.4k
GH-126910: Add gdb support for unwinding JIT frames #146071
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5cd7ade
669dfb9
255c0b3
ac018d6
b0bab8c
a0dff1f
2b52588
e44170e
2e40f1d
d890add
965a543
17be0a2
f47d763
67ae6cb
a18cb96
bdc8d12
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| #ifndef Py_CORE_JIT_UNWIND_H | ||
| #define Py_CORE_JIT_UNWIND_H | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is missing Py_BUILD_CORE guard no? |
||
| #ifdef PY_HAVE_PERF_TRAMPOLINE | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The entire file is gated on PY_HAVE_PERF_TRAMPOLINE, but the GDB JIT interface is conceptually independent of perf no?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops yea you're right. |
||
|
|
||
| #include <stddef.h> | ||
|
|
||
| /* Return the size of the generated .eh_frame data for the given encoding. */ | ||
| size_t _PyJitUnwind_EhFrameSize(int absolute_addr); | ||
|
|
||
| /* | ||
| * Build DWARF .eh_frame data for JIT code; returns size written or 0 on error. | ||
| * absolute_addr selects the FDE address encoding: | ||
| * - 0: PC-relative offsets (perf jitdump synthesized DSO). | ||
| * - nonzero: absolute addresses (GDB JIT in-memory ELF). | ||
| */ | ||
| size_t _PyJitUnwind_BuildEhFrame(uint8_t *buffer, size_t buffer_size, | ||
| const void *code_addr, size_t code_size, | ||
| int absolute_addr); | ||
|
|
||
| void _PyJitUnwind_GdbRegisterCode(const void *code_addr, | ||
| size_t code_size, | ||
| const char *entry, | ||
| const char *filename); | ||
|
|
||
| void _PyJitUnwind_GdbUnregisterCode(const void *code_addr); | ||
|
|
||
| #endif // PY_HAVE_PERF_TRAMPOLINE | ||
|
|
||
| #endif // Py_CORE_JIT_UNWIND_H | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| # Sample script for use by test_gdb.test_jit | ||
|
|
||
| import _testinternalcapi | ||
| import operator | ||
|
|
||
|
|
||
| WARMUP_ITERATIONS = _testinternalcapi.TIER2_THRESHOLD + 10 | ||
|
|
||
|
|
||
| def jit_bt_hot(depth, warming_up_caller=False): | ||
| if depth == 0: | ||
| if not warming_up_caller: | ||
| id(42) | ||
| return | ||
|
|
||
| for iteration in range(WARMUP_ITERATIONS): | ||
| operator.call( | ||
| jit_bt_hot, | ||
| depth - 1, | ||
| warming_up_caller or iteration + 1 != WARMUP_ITERATIONS, | ||
| ) | ||
|
|
||
|
|
||
| # Warm the shared shim once without hitting builtin_id so the real run uses | ||
| # the steady-state shim path when GDB breaks inside id(42). | ||
| jit_bt_hot(1, warming_up_caller=True) | ||
| jit_bt_hot(1) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| import os | ||
| import re | ||
| import sys | ||
| import unittest | ||
|
|
||
| from .util import setup_module, DebuggerTests | ||
|
|
||
|
|
||
| JIT_SAMPLE_SCRIPT = os.path.join(os.path.dirname(__file__), "gdb_jit_sample.py") | ||
| # In batch GDB, break in builtin_id() while it is running under JIT, | ||
| # then repeatedly "finish" until the selected frame is the JIT entry. | ||
| # That gives a deterministic backtrace starting with py::jit_entry:<jit>. | ||
| # | ||
| # builtin_id() sits only a few helper frames above the JIT entry on this path. | ||
| # This bound is just a generous upper limit so the test fails clearly if the | ||
| # expected stack shape changes. | ||
| MAX_FINISH_STEPS = 20 | ||
| # Break directly on the lazy shim entry in the binary, then single-step just | ||
| # enough to let it install the compiled JIT entry and set a temporary | ||
| # breakpoint on the resulting address. | ||
| MAX_ENTRY_SETUP_STEPS = 20 | ||
| # After landing on the JIT entry frame, single-step a little further into the | ||
| # blob so the backtrace is taken from JIT code itself rather than the | ||
| # immediate helper-return site. | ||
| JIT_ENTRY_SINGLE_STEPS = 2 | ||
| EVAL_FRAME_RE = r"(_PyEval_EvalFrameDefault|_PyEval_Vector)" | ||
|
|
||
| FINISH_TO_JIT_ENTRY = ( | ||
| "python exec(\"import gdb\\n" | ||
| "target = 'py::jit_entry:<jit>'\\n" | ||
| f"for _ in range({MAX_FINISH_STEPS}):\\n" | ||
| " frame = gdb.selected_frame()\\n" | ||
| " if frame is not None and frame.name() == target:\\n" | ||
| " break\\n" | ||
| " gdb.execute('finish')\\n" | ||
| "else:\\n" | ||
| " raise RuntimeError('did not reach %s' % target)\\n\")" | ||
| ) | ||
| BREAK_IN_COMPILED_JIT_ENTRY = ( | ||
| "python exec(\"import gdb\\n" | ||
| "lazy = int(gdb.parse_and_eval('(void*)_Py_LazyJitShim'))\\n" | ||
| f"for _ in range({MAX_ENTRY_SETUP_STEPS}):\\n" | ||
| " entry = int(gdb.parse_and_eval('(void*)_Py_jit_entry'))\\n" | ||
| " if entry != lazy:\\n" | ||
| " gdb.execute('tbreak *0x%x' % entry)\\n" | ||
| " break\\n" | ||
| " gdb.execute('next')\\n" | ||
| "else:\\n" | ||
| " raise RuntimeError('compiled JIT entry was not installed')\\n\")" | ||
| ) | ||
|
|
||
|
|
||
| def setUpModule(): | ||
| setup_module() | ||
|
|
||
|
|
||
| @unittest.skipUnless( | ||
| hasattr(sys, "_jit") and sys._jit.is_available(), | ||
| "requires a JIT-enabled build", | ||
| ) | ||
| class JitBacktraceTests(DebuggerTests): | ||
| def test_bt_shows_compiled_jit_entry(self): | ||
| gdb_output = self.get_stack_trace( | ||
| script=JIT_SAMPLE_SCRIPT, | ||
| breakpoint="_Py_LazyJitShim", | ||
| cmds_after_breakpoint=[ | ||
| BREAK_IN_COMPILED_JIT_ENTRY, | ||
| "continue", | ||
| "bt", | ||
| ], | ||
| PYTHON_JIT="1", | ||
| ) | ||
| # GDB registers the compiled JIT entry and per-trace JIT regions under | ||
| # the same synthetic symbol name. | ||
| self.assertRegex( | ||
| gdb_output, | ||
| re.compile( | ||
| rf"#0\s+py::jit_entry:<jit>.*{EVAL_FRAME_RE}", | ||
| re.DOTALL, | ||
| ), | ||
| ) | ||
|
|
||
| def test_bt_unwinds_through_jit_frames(self): | ||
| gdb_output = self.get_stack_trace( | ||
| script=JIT_SAMPLE_SCRIPT, | ||
| cmds_after_breakpoint=["bt"], | ||
| PYTHON_JIT="1", | ||
| ) | ||
| # The executor should appear as a named JIT frame and unwind back into | ||
| # the eval loop. Whether GDB also materializes a separate shim frame is | ||
| # an implementation detail of the synthetic executor CFI. | ||
| self.assertRegex( | ||
| gdb_output, | ||
| re.compile( | ||
| rf"py::jit_entry:<jit>.*{EVAL_FRAME_RE}", | ||
| re.DOTALL, | ||
| ), | ||
| ) | ||
|
|
||
| def test_bt_unwinds_from_inside_jit_entry(self): | ||
| gdb_output = self.get_stack_trace( | ||
| script=JIT_SAMPLE_SCRIPT, | ||
| cmds_after_breakpoint=[ | ||
| FINISH_TO_JIT_ENTRY, | ||
| *(["si"] * JIT_ENTRY_SINGLE_STEPS), | ||
| "bt", | ||
| ], | ||
| PYTHON_JIT="1", | ||
| ) | ||
| # Once the selected PC is inside the JIT entry, we only require that | ||
| # GDB can identify the JIT frame and keep unwinding into _PyEval_*. | ||
| self.assertRegex( | ||
| gdb_output, | ||
| re.compile( | ||
| rf"#0\s+py::jit_entry:<jit>.*{EVAL_FRAME_RE}", | ||
| re.DOTALL, | ||
| ), | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Add support for unwinding JIT frames using GDB. Patch by Diego Russo |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1210,7 +1210,7 @@ write_perf_map_entry(PyObject *self, PyObject *args) | |
| { | ||
| PyObject *code_addr_v; | ||
| const void *code_addr; | ||
| unsigned int code_size; | ||
| size_t code_size; | ||
| const char *entry_name; | ||
|
|
||
| if (!PyArg_ParseTuple(args, "OIs", &code_addr_v, &code_size, &entry_name)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
@@ -1220,7 +1220,7 @@ write_perf_map_entry(PyObject *self, PyObject *args) | |
| return NULL; | ||
| } | ||
|
|
||
| int ret = PyUnstable_WritePerfMapEntry(code_addr, code_size, entry_name); | ||
| int ret = PyUnstable_WritePerfMapEntry(code_addr, (unsigned int)code_size, entry_name); | ||
| if (ret < 0) { | ||
| PyErr_SetFromErrno(PyExc_OSError); | ||
| return NULL; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |
| #include "pycore_interpframe.h" | ||
| #include "pycore_interpolation.h" | ||
| #include "pycore_intrinsics.h" | ||
| #include "pycore_jit_unwind.h" | ||
| #include "pycore_lazyimportobject.h" | ||
| #include "pycore_list.h" | ||
| #include "pycore_long.h" | ||
|
|
@@ -60,6 +61,28 @@ jit_error(const char *message) | |
| PyErr_Format(PyExc_RuntimeWarning, "JIT %s (%d)", message, hint); | ||
| } | ||
|
|
||
| static void | ||
| jit_record_code(const void *code_addr, size_t code_size, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will leave this for the future but as this is unconditionally active I assume will have a perf cost we probably want top measure |
||
| 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; | ||
| } | ||
| _PyJitUnwind_GdbRegisterCode( | ||
| code_addr, code_size, entry, filename); | ||
| #else | ||
| (void)code_addr; | ||
| (void)code_size; | ||
| (void)entry; | ||
| (void)filename; | ||
| #endif | ||
| } | ||
|
|
||
| static size_t _Py_jit_shim_size = 0; | ||
|
|
||
| static int | ||
|
|
@@ -731,6 +754,10 @@ _PyJIT_Compile(_PyExecutorObject *executor, const _PyUOpInstruction trace[], siz | |
| } | ||
| executor->jit_code = memory; | ||
| executor->jit_size = total_size; | ||
| jit_record_code(memory, | ||
| code_size + state.trampolines.size, | ||
| "jit_entry", | ||
| "<jit>"); | ||
| return 0; | ||
| } | ||
|
|
||
|
|
@@ -781,6 +808,10 @@ compile_shim(void) | |
| return NULL; | ||
| } | ||
| _Py_jit_shim_size = total_size; | ||
| jit_record_code(memory, | ||
| code_size + state.trampolines.size, | ||
| "jit_entry", | ||
| "<jit>"); | ||
| return (_PyJitEntryFuncPtr)memory; | ||
| } | ||
|
|
||
|
|
@@ -812,6 +843,9 @@ _PyJIT_Free(_PyExecutorObject *executor) | |
| if (memory) { | ||
| executor->jit_code = NULL; | ||
| executor->jit_size = 0; | ||
| #ifdef PY_HAVE_PERF_TRAMPOLINE | ||
| _PyJitUnwind_GdbUnregisterCode(memory); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Registration is conditional but unregistration is not. |
||
| #endif | ||
| if (jit_free(memory, size)) { | ||
| PyErr_FormatUnraisable("Exception ignored while " | ||
| "freeing JIT memory"); | ||
|
|
@@ -829,6 +863,9 @@ _PyJIT_Fini(void) | |
| if (size) { | ||
| _Py_jit_entry = _Py_LazyJitShim; | ||
| _Py_jit_shim_size = 0; | ||
| #ifdef PY_HAVE_PERF_TRAMPOLINE | ||
| _PyJitUnwind_GdbUnregisterCode(memory); | ||
| #endif | ||
| if (jit_free(memory, size)) { | ||
| PyErr_FormatUnraisable("Exception ignored while " | ||
| "freeing JIT entry code"); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be Py_INTERNAL_JIT_UNWIND_H