-
-
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 13 commits
5cd7ade
669dfb9
255c0b3
ac018d6
b0bab8c
a0dff1f
2b52588
e44170e
2e40f1d
d890add
965a543
17be0a2
f47d763
67ae6cb
a18cb96
bdc8d12
6357698
0b07c57
93bbf99
3eeddb9
a9c6315
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,28 @@ | ||
| #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?
Contributor
Author
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. yes, I've seen now the other headers files. |
||
| #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.
Contributor
Author
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. for now, I'll add the bare minimum to address this but I already in mind some refactoring to do with another PR. Let's land this first and then I will refactor the code in light of adding libcc (for gnu backtrace)
Contributor
Author
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 solution won't be the best, but it will be improved in subsequent PRs. I don't want to keep changing this PR. |
||
|
|
||
| #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); | ||
|
|
||
| #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,116 @@ | ||
| 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 executor. | ||
| # That gives a deterministic backtrace starting with py::jit_executor:<jit>. | ||
| # | ||
| # builtin_id() sits only a few helper frames above the executor 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 shim and set a temporary breakpoint on | ||
| # the resulting JIT entry address. | ||
| MAX_SHIM_SETUP_STEPS = 20 | ||
| # After landing on the executor frame, single-step a little further into the | ||
| # blob so the backtrace is taken from executor code itself rather than the | ||
| # immediate helper-return site. | ||
| EXECUTOR_SINGLE_STEPS = 2 | ||
| EVAL_FRAME_RE = r"(_PyEval_EvalFrameDefault|_PyEval_Vector)" | ||
|
|
||
| FINISH_TO_JIT_EXECUTOR = ( | ||
| "python exec(\"import gdb\\n" | ||
| "target = 'py::jit_executor:<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_SHIM = ( | ||
| "python exec(\"import gdb\\n" | ||
| "lazy = int(gdb.parse_and_eval('(void*)_Py_LazyJitShim'))\\n" | ||
| f"for _ in range({MAX_SHIM_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 shim 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_shim(self): | ||
| gdb_output = self.get_stack_trace( | ||
| script=JIT_SAMPLE_SCRIPT, | ||
| breakpoint="_Py_LazyJitShim", | ||
| cmds_after_breakpoint=[ | ||
| BREAK_IN_COMPILED_SHIM, | ||
| "continue", | ||
| "bt", | ||
| ], | ||
| PYTHON_JIT="1", | ||
| ) | ||
| self.assertRegex( | ||
| gdb_output, | ||
| re.compile( | ||
| rf"#0\s+py::jit_shim:<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_executor:<jit>.*{EVAL_FRAME_RE}", | ||
| re.DOTALL, | ||
| ), | ||
| ) | ||
|
|
||
| def test_bt_unwinds_from_inside_jit_executor(self): | ||
| gdb_output = self.get_stack_trace( | ||
| script=JIT_SAMPLE_SCRIPT, | ||
| cmds_after_breakpoint=[ | ||
| FINISH_TO_JIT_EXECUTOR, | ||
| *(["si"] * EXECUTOR_SINGLE_STEPS), | ||
| "bt", | ||
| ], | ||
| PYTHON_JIT="1", | ||
| ) | ||
| # Once the selected PC is inside the executor, 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_executor:<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.
Contributor
Author
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. ok, after some thinking on how to solve this I decided to change all the api to use |
||
|
|
@@ -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
Contributor
Author
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'm measuring it.. it might take some time. |
||
| 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_executor", | ||
| "<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_shim", | ||
| "<jit>"); | ||
| return (_PyJitEntryFuncPtr)memory; | ||
| } | ||
|
|
||
|
|
||
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