Skip to content

Commit 445ab53

Browse files
committed
Relax JIT GDB test backtrace shape
Accept either JIT backtrace shape in test_jit.py: - py::jit:executor -> _PyEval_* - py::jit:executor -> _PyJIT_Entry -> _PyEval_* This avoids baking in architecture-specific unwind details while still checking that GDB gets out of the JIT region and back into the eval loop.
1 parent d4c0113 commit 445ab53

7 files changed

Lines changed: 67 additions & 80 deletions

File tree

Include/internal/pycore_uop_metadata.h

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_gdb/test_jit.py

Lines changed: 51 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
# JIT region, instead of asserting against a misleading backtrace.
2525
MAX_JIT_ENTRY_STEPS = 4
2626
EVAL_FRAME_RE = r"(_PyEval_EvalFrameDefault|_PyEval_Vector)"
27-
JIT_ENTRY_RE = r"_PyJIT_Entry"
2827
JIT_EXECUTOR_FRAME = "py::jit:executor"
28+
JIT_ENTRY_SYMBOL = "_PyJIT_Entry"
2929
BACKTRACE_FRAME_RE = re.compile(r"^#\d+\s+.*$", re.MULTILINE)
3030

3131
FINISH_TO_JIT_EXECUTOR = (
@@ -92,14 +92,12 @@ def _assert_jit_backtrace_shape(self, gdb_output, *, anchor_at_top):
9292
# py::jit:executor frame would mean the unwinder is
9393
# materializing two native frames for a single logical JIT
9494
# region, or failing to unwind out of the region entirely.
95-
# 2. The linked shim frame appears exactly once after the
96-
# synthetic JIT frame and before the eval loop.
97-
# 3. At least one _PyEval_EvalFrameDefault / _PyEval_Vector
98-
# frame appears after the JIT frame, proving the unwinder
99-
# climbs back out of the JIT region into the eval loop.
100-
# Helper frames from inside the JITted region may still
101-
# appear above the synthetic JIT frame in the backtrace.
102-
# 4. For tests that assert a specific entry PC, the JIT frame
95+
# 2. The unwinder must climb back out of the JIT region into
96+
# the eval loop. Some platforms materialize a real
97+
# _PyJIT_Entry frame between the synthetic executor frame
98+
# and _PyEval_*, while others unwind directly from the
99+
# executor into _PyEval_*. Accept both shapes.
100+
# 3. For tests that assert a specific entry PC, the JIT frame
103101
# is also at #0.
104102
frames = self._extract_backtrace_frames(gdb_output)
105103
backtrace = "\n".join(frames)
@@ -111,13 +109,6 @@ def _assert_jit_backtrace_shape(self, gdb_output, *, anchor_at_top):
111109
f"expected exactly 1 {JIT_EXECUTOR_FRAME} frame, got {jit_count}\n"
112110
f"backtrace:\n{backtrace}",
113111
)
114-
jit_entry_frames = [frame for frame in frames if re.search(JIT_ENTRY_RE, frame)]
115-
jit_entry_count = len(jit_entry_frames)
116-
self.assertEqual(
117-
jit_entry_count, 1,
118-
f"expected exactly 1 _PyJIT_Entry frame, got {jit_entry_count}\n"
119-
f"backtrace:\n{backtrace}",
120-
)
121112
eval_frames = [frame for frame in frames if re.search(EVAL_FRAME_RE, frame)]
122113
eval_count = len(eval_frames)
123114
self.assertGreaterEqual(
@@ -128,38 +119,45 @@ def _assert_jit_backtrace_shape(self, gdb_output, *, anchor_at_top):
128119
jit_frame_index = next(
129120
i for i, frame in enumerate(frames) if JIT_EXECUTOR_FRAME in frame
130121
)
131-
jit_entry_index = next(
132-
i for i, frame in enumerate(frames) if re.search(JIT_ENTRY_RE, frame)
133-
)
134-
self.assertGreater(
135-
jit_entry_index, jit_frame_index,
136-
"expected _PyJIT_Entry after the synthetic JIT frame\n"
137-
f"backtrace:\n{backtrace}",
138-
)
139-
eval_after_jit = any(
140-
re.search(EVAL_FRAME_RE, frame)
141-
for frame in frames[jit_frame_index + 1:]
122+
frames_after_jit = frames[jit_frame_index + 1:]
123+
first_eval_offset = next(
124+
(
125+
i for i, frame in enumerate(frames_after_jit)
126+
if re.search(EVAL_FRAME_RE, frame)
127+
),
128+
None,
142129
)
143-
self.assertTrue(
144-
eval_after_jit,
130+
self.assertIsNotNone(
131+
first_eval_offset,
145132
f"expected an eval frame after the JIT frame\n"
146133
f"backtrace:\n{backtrace}",
147134
)
148-
eval_after_entry = any(
149-
re.search(EVAL_FRAME_RE, frame)
150-
for frame in frames[jit_entry_index + 1:]
135+
between_jit_and_eval = frames_after_jit[:first_eval_offset]
136+
jit_entry_frames = [
137+
frame for frame in between_jit_and_eval
138+
if JIT_ENTRY_SYMBOL in frame
139+
]
140+
self.assertLessEqual(
141+
len(jit_entry_frames), 1,
142+
f"expected at most one {JIT_ENTRY_SYMBOL} frame between the "
143+
f"executor and eval frames\nbacktrace:\n{backtrace}",
151144
)
152-
self.assertTrue(
153-
eval_after_entry,
154-
"expected an eval frame after _PyJIT_Entry\n"
145+
unexpected_between = [
146+
frame for frame in between_jit_and_eval
147+
if JIT_ENTRY_SYMBOL not in frame
148+
]
149+
self.assertFalse(
150+
unexpected_between,
151+
"expected only an optional _PyJIT_Entry frame between the "
152+
"executor and eval frames\n"
155153
f"backtrace:\n{backtrace}",
156154
)
157155
relevant_end = max(
158156
i
159157
for i, frame in enumerate(frames)
160158
if (
161159
JIT_EXECUTOR_FRAME in frame
162-
or re.search(JIT_ENTRY_RE, frame)
160+
or JIT_ENTRY_SYMBOL in frame
163161
or re.search(EVAL_FRAME_RE, frame)
164162
)
165163
)
@@ -188,6 +186,23 @@ def test_bt_unwinds_through_jit_frames(self):
188186
# the eval loop.
189187
self._assert_jit_backtrace_shape(gdb_output, anchor_at_top=False)
190188

189+
def test_bt_handoff_from_jit_entry_to_executor(self):
190+
gdb_output = self.get_stack_trace(
191+
script=JIT_SAMPLE_SCRIPT,
192+
breakpoint=JIT_ENTRY_SYMBOL,
193+
cmds_after_breakpoint=[
194+
"delete 1",
195+
"tbreak builtin_id",
196+
"continue",
197+
"bt",
198+
],
199+
PYTHON_JIT="1",
200+
)
201+
# If we stop first in the shim and then continue into the real JIT
202+
# workload, the final backtrace should match the architecture's
203+
# executor unwind contract.
204+
self._assert_jit_backtrace_shape(gdb_output, anchor_at_top=False)
205+
191206
def test_bt_unwinds_from_inside_jit_executor(self):
192207
gdb_output = self.get_stack_trace(
193208
script=JIT_SAMPLE_SCRIPT,

Python/bytecodes.c

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6118,11 +6118,6 @@ dummy_func(
61186118
#ifndef _Py_JIT
61196119
assert(current_executor == (_PyExecutorObject*)executor);
61206120
#endif
6121-
// Keep the return-to-_PyJIT_Entry PC in a reserved
6122-
// callee-saved register so the executor-wide GDB FDE can
6123-
// always materialize _PyJIT_Entry as the immediate caller
6124-
// frame.
6125-
_Py_JIT_CAPTURE_CALLER_PC();
61266121
assert(tstate->jit_exit == NULL || tstate->jit_exit->executor == current_executor);
61276122
tstate->current_executor = (PyObject *)current_executor;
61286123
if (!current_executor->vm_data.valid) {

Python/ceval_macros.h

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -168,21 +168,6 @@
168168
#define STOP_TRACING() ((void)(0));
169169
#endif
170170

171-
/*
172-
* The executor runs inside the frame established by _PyJIT_Entry. On AArch64,
173-
* helper-calling stencils would otherwise leave the return-to-_PyJIT_Entry PC
174-
* in different places at different executor PCs. Capture the entry x30 into
175-
* reserved x28 once so the executor-wide GDB FDE in Python/jit_unwind.c can
176-
* always materialize _PyJIT_Entry as the immediate caller frame.
177-
*/
178-
#if defined(_Py_JIT) && defined(__aarch64__) && defined(__AARCH64EL__) && !defined(__ILP32__)
179-
# define _Py_JIT_CAPTURE_CALLER_PC() \
180-
__asm__ volatile("mov x28, x30")
181-
#else
182-
# define _Py_JIT_CAPTURE_CALLER_PC() ((void)0)
183-
#endif
184-
185-
186171
/* PRE_DISPATCH_GOTO() does lltrace if enabled. Normally a no-op */
187172
#ifdef Py_DEBUG
188173
#define PRE_DISPATCH_GOTO() if (frame->lltrace >= 5) { \

Python/executor_cases.c.h

Lines changed: 0 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/jit_unwind.c

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
enum {
3737
DWRF_CFA_nop = 0x0, // No operation
3838
DWRF_CFA_offset_extended = 0x5, // Extended offset instruction
39-
DWRF_CFA_register = 0x9, // Register contains saved value
4039
DWRF_CFA_def_cfa = 0xc, // Define CFA rule
4140
DWRF_CFA_def_cfa_register = 0xd, // Define CFA register
4241
DWRF_CFA_def_cfa_offset = 0xe, // Define CFA offset
@@ -75,7 +74,6 @@ enum {
7574
DWRF_REG_RA, // Return address (RIP)
7675
#elif defined(__aarch64__) && defined(__AARCH64EL__) && !defined(__ILP32__)
7776
/* AArch64 register numbering */
78-
DWRF_REG_JIT_RA = 28, // Reserved x28 holds the return PC into _PyJIT_Entry
7977
DWRF_REG_FP = 29, // Frame Pointer
8078
DWRF_REG_RA = 30, // Link register (return address)
8179
DWRF_REG_SP = 31, // Stack pointer
@@ -620,13 +618,16 @@ static void elf_init_ehframe_perf(ELFObjectContext* ctx) {
620618
* executor region:
621619
*
622620
* x86_64: CFA = %rbp + 16, return-to-_PyJIT_Entry PC at cfa-72
623-
* AArch64: CFA = x29 + 96, return-to-_PyJIT_Entry PC in reserved x28
621+
* AArch64: CFA = x29 + 96, caller x29 at cfa-96, caller x30 at cfa-88
624622
*
625-
* Executor stencils never touch the frame pointer — enforced by
626-
* Tools/jit/_optimizers.py _validate() and -mframe-pointer=reserved — so
627-
* that rule is valid at every PC and the FDE body is empty. On AArch64,
628-
* _START_EXECUTOR copies the entry x30 into reserved x28 once and the
629-
* executor stencils preserve x28 thereafter.
623+
* The executor runs inside the frame established by _PyJIT_Entry. On AArch64
624+
* we collapse that state into a single synthetic executor frame that unwinds
625+
* directly into _PyEval_*. On x86_64 the normal call into the executor leaves
626+
* a real return slot back into _PyJIT_Entry, so the executor FDE materializes
627+
* _PyJIT_Entry as the caller frame. Executor stencils never touch the frame
628+
* pointer — enforced by Tools/jit/_optimizers.py _validate() and
629+
* -mframe-pointer=reserved — so the steady-state rule is valid at every PC
630+
* and the FDE body is empty.
630631
*/
631632
static void elf_init_ehframe_gdb(ELFObjectContext* ctx) {
632633
int fde_ptr_enc = DWRF_EH_PE_absptr;
@@ -658,9 +659,10 @@ static void elf_init_ehframe_gdb(ELFObjectContext* ctx) {
658659
DWRF_U8(DWRF_CFA_def_cfa); // CFA = x29 + 96
659660
DWRF_UV(DWRF_REG_FP);
660661
DWRF_UV(96);
661-
DWRF_U8(DWRF_CFA_register); // Return PC lives in reserved x28
662-
DWRF_UV(DWRF_REG_RA);
663-
DWRF_UV(DWRF_REG_JIT_RA);
662+
DWRF_U8(DWRF_CFA_offset | DWRF_REG_FP);
663+
DWRF_UV(12); // caller x29 at cfa-96
664+
DWRF_U8(DWRF_CFA_offset | DWRF_REG_RA);
665+
DWRF_UV(11); // caller x30 at cfa-88
664666
#else
665667
# error "Unsupported target architecture"
666668
#endif

Tools/jit/_targets.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,6 @@ async def _build_stencil_group(
200200
]
201201
if self.frame_pointers:
202202
args_s += ["-Xclang", "-mframe-pointer=reserved"]
203-
if self.triple.startswith("aarch64-"):
204-
# Keep x28 free so _START_EXECUTOR can stash the return-to-_PyJIT_Entry
205-
# PC there for a single executor-wide unwind rule.
206-
args_s += ["-ffixed-x28"]
207203
args_s += self._compile_args()
208204
# Allow user-provided CFLAGS to override any defaults
209205
args_s += shlex.split(self.cflags)
@@ -238,7 +234,7 @@ async def _build_shim_object(self, output: pathlib.Path) -> None:
238234
args_o += self._shim_compile_args()
239235
args_o += [
240236
"-c",
241-
# The linked shim is a real function in the final binary, so
237+
# The shim is a real function in the final binary, so
242238
# keep unwind info for debuggers and stack walkers.
243239
"-fasynchronous-unwind-tables",
244240
]
@@ -336,7 +332,7 @@ class _COFF(
336332
_Target[_schema.COFFSection, _schema.COFFRelocation]
337333
): # pylint: disable = too-few-public-methods
338334
def _shim_compile_args(self) -> list[str]:
339-
# The linked shim is part of pythoncore, not a shared extension.
335+
# The shim is part of pythoncore, not a shared extension.
340336
# On Windows, Py_BUILD_CORE_MODULE makes public APIs import from
341337
# pythonXY.lib, which creates a self-dependency when linking
342338
# pythoncore.dll. Build the shim with builtin/core semantics.

0 commit comments

Comments
 (0)