Skip to content

Commit d4c0113

Browse files
committed
Drop all shim related changes.
Link the JIT shim into the binary and remove the old runtime shim-specific unwind machinery. Rename _PyJIT to _PyJIT_Entry and the synthetic executor frame to py::jit:executor. Make executor unwinding materialize _PyJIT_Entry beneath py::jit:executor. Update the GDB tests to require both the executor frame and _PyJIT_Entry.
1 parent 88a1bd8 commit d4c0113

16 files changed

Lines changed: 126 additions & 333 deletions

Include/internal/pycore_jit.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ typedef _Py_CODEUNIT *(*jit_func)(
2323
_PyStackRef _tos_cache0, _PyStackRef _tos_cache1, _PyStackRef _tos_cache2
2424
);
2525

26-
_Py_CODEUNIT *_PyJIT(
26+
_Py_CODEUNIT *_PyJIT_Entry(
2727
_PyExecutorObject *executor, _PyInterpreterFrame *frame,
2828
_PyStackRef *stack_pointer, PyThreadState *tstate
2929
);

Include/internal/pycore_jit_unwind.h

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,6 @@
88
#include <stddef.h>
99
#include <stdint.h>
1010

11-
/*
12-
* Compiler-emitted CFI for the shim region (GDB path only).
13-
*
14-
* Captured at build time by Tools/jit from the shim's compiled .eh_frame
15-
* so the runtime CIE/FDE can describe whatever prologue the compiler
16-
* chose, without hand-rolling DWARF. Executors pass NULL and fall back
17-
* to the invariant-based steady-state rule that the CIE emits by hand.
18-
*
19-
* The struct is defined unconditionally so jit_record_code() in Python/jit.c
20-
* has a valid pointer type on every platform — callers on non-(Linux+ELF)
21-
* always pass NULL, matching jit_record_code()'s internal #ifdef.
22-
*/
23-
typedef struct {
24-
const uint8_t *cie_init_cfi;
25-
size_t cie_init_cfi_size;
26-
const uint8_t *fde_cfi;
27-
size_t fde_cfi_size;
28-
uint32_t code_align;
29-
int32_t data_align;
30-
uint32_t ra_column;
31-
} _PyJitUnwind_ShimCfi;
32-
3311
#if defined(PY_HAVE_PERF_TRAMPOLINE) || (defined(__linux__) && defined(__ELF__))
3412

3513
/* DWARF exception-handling pointer encodings shared by JIT unwind users. */
@@ -58,30 +36,22 @@ enum {
5836
};
5937

6038
/* Return the size of the generated .eh_frame data for the given encoding. */
61-
size_t _PyJitUnwind_EhFrameSize(int absolute_addr,
62-
const _PyJitUnwind_ShimCfi *shim_cfi);
39+
size_t _PyJitUnwind_EhFrameSize(int absolute_addr);
6340

6441
/*
6542
* Build DWARF .eh_frame data for JIT code; returns size written or 0 on error.
6643
* absolute_addr selects the FDE address encoding:
6744
* - 0: PC-relative offsets (perf jitdump synthesized DSO).
6845
* - nonzero: absolute addresses (GDB JIT in-memory ELF).
69-
*
70-
* shim_cfi selects which JIT region the CFI describes (GDB path only):
71-
* - NULL: executor trace; steady-state rule in the CIE applies at every PC.
72-
* - non-NULL: compile_shim() output; the captured CIE/FDE CFI bytes are
73-
* spliced in so unwinding is valid at every PC in the shim.
7446
*/
7547
size_t _PyJitUnwind_BuildEhFrame(uint8_t *buffer, size_t buffer_size,
7648
const void *code_addr, size_t code_size,
77-
int absolute_addr,
78-
const _PyJitUnwind_ShimCfi *shim_cfi);
49+
int absolute_addr);
7950

8051
void *_PyJitUnwind_GdbRegisterCode(const void *code_addr,
8152
size_t code_size,
8253
const char *entry,
83-
const char *filename,
84-
const _PyJitUnwind_ShimCfi *shim_cfi);
54+
const char *filename);
8555

8656
void _PyJitUnwind_GdbUnregisterCode(void *handle);
8757

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: 50 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,13 @@
99

1010
JIT_SAMPLE_SCRIPT = os.path.join(os.path.dirname(__file__), "gdb_jit_sample.py")
1111
# In batch GDB, break in builtin_id() while it is running under JIT,
12-
# then repeatedly "finish" until the selected frame is the JIT entry.
13-
# That gives a deterministic backtrace starting with py::jit_entry:<jit>.
12+
# then repeatedly "finish" until the selected frame is the JIT executor.
13+
# That gives a deterministic backtrace starting with py::jit:executor.
1414
#
1515
# builtin_id() sits only a few helper frames above the JIT entry on this path.
1616
# This bound is just a generous upper limit so the test fails clearly if the
1717
# expected stack shape changes.
1818
MAX_FINISH_STEPS = 20
19-
# Break directly on the lazy shim entry in the binary, then single-step just
20-
# enough to let it install the compiled JIT entry and set a temporary
21-
# breakpoint on the resulting address.
22-
MAX_ENTRY_SETUP_STEPS = 20
2319
# After landing on the JIT entry frame, single-step a bounded number of
2420
# instructions further into the blob so the backtrace is taken from JIT code
2521
# itself rather than the immediate helper-return site. The exact number of
@@ -28,11 +24,13 @@
2824
# JIT region, instead of asserting against a misleading backtrace.
2925
MAX_JIT_ENTRY_STEPS = 4
3026
EVAL_FRAME_RE = r"(_PyEval_EvalFrameDefault|_PyEval_Vector)"
27+
JIT_ENTRY_RE = r"_PyJIT_Entry"
28+
JIT_EXECUTOR_FRAME = "py::jit:executor"
3129
BACKTRACE_FRAME_RE = re.compile(r"^#\d+\s+.*$", re.MULTILINE)
3230

33-
FINISH_TO_JIT_ENTRY = (
31+
FINISH_TO_JIT_EXECUTOR = (
3432
"python exec(\"import gdb\\n"
35-
"target = 'py::jit_entry:<jit>'\\n"
33+
f"target = {JIT_EXECUTOR_FRAME!r}\\n"
3634
f"for _ in range({MAX_FINISH_STEPS}):\\n"
3735
" frame = gdb.selected_frame()\\n"
3836
" if frame is not None and frame.name() == target:\\n"
@@ -41,21 +39,9 @@
4139
"else:\\n"
4240
" raise RuntimeError('did not reach %s' % target)\\n\")"
4341
)
44-
BREAK_IN_COMPILED_JIT_ENTRY = (
42+
STEP_INSIDE_JIT_EXECUTOR = (
4543
"python exec(\"import gdb\\n"
46-
"lazy = int(gdb.parse_and_eval('(void*)_Py_LazyJitShim'))\\n"
47-
f"for _ in range({MAX_ENTRY_SETUP_STEPS}):\\n"
48-
" entry = int(gdb.parse_and_eval('(void*)_Py_jit_entry'))\\n"
49-
" if entry != lazy:\\n"
50-
" gdb.execute('tbreak *0x%x' % entry)\\n"
51-
" break\\n"
52-
" gdb.execute('next')\\n"
53-
"else:\\n"
54-
" raise RuntimeError('compiled JIT entry was not installed')\\n\")"
55-
)
56-
STEP_INSIDE_JIT_ENTRY = (
57-
"python exec(\"import gdb\\n"
58-
"target = 'py::jit_entry:<jit>'\\n"
44+
f"target = {JIT_EXECUTOR_FRAME!r}\\n"
5945
f"for _ in range({MAX_JIT_ENTRY_STEPS}):\\n"
6046
" frame = gdb.selected_frame()\\n"
6147
" if frame is None or frame.name() != target:\\n"
@@ -103,10 +89,12 @@ def _extract_backtrace_frames(self, gdb_output):
10389
def _assert_jit_backtrace_shape(self, gdb_output, *, anchor_at_top):
10490
# Shape assertions applied to every JIT backtrace we produce:
10591
# 1. The synthetic JIT symbol appears exactly once. A second
106-
# py::jit_entry:<jit> frame would mean the unwinder is
92+
# py::jit:executor frame would mean the unwinder is
10793
# materializing two native frames for a single logical JIT
10894
# region, or failing to unwind out of the region entirely.
109-
# 2. At least one _PyEval_EvalFrameDefault / _PyEval_Vector
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
11098
# frame appears after the JIT frame, proving the unwinder
11199
# climbs back out of the JIT region into the eval loop.
112100
# Helper frames from inside the JITted region may still
@@ -116,11 +104,18 @@ def _assert_jit_backtrace_shape(self, gdb_output, *, anchor_at_top):
116104
frames = self._extract_backtrace_frames(gdb_output)
117105
backtrace = "\n".join(frames)
118106

119-
jit_frames = [frame for frame in frames if "py::jit_entry:<jit>" in frame]
107+
jit_frames = [frame for frame in frames if JIT_EXECUTOR_FRAME in frame]
120108
jit_count = len(jit_frames)
121109
self.assertEqual(
122110
jit_count, 1,
123-
f"expected exactly 1 py::jit_entry:<jit> frame, got {jit_count}\n"
111+
f"expected exactly 1 {JIT_EXECUTOR_FRAME} frame, got {jit_count}\n"
112+
f"backtrace:\n{backtrace}",
113+
)
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"
124119
f"backtrace:\n{backtrace}",
125120
)
126121
eval_frames = [frame for frame in frames if re.search(EVAL_FRAME_RE, frame)]
@@ -131,7 +126,15 @@ def _assert_jit_backtrace_shape(self, gdb_output, *, anchor_at_top):
131126
f"backtrace:\n{backtrace}",
132127
)
133128
jit_frame_index = next(
134-
i for i, frame in enumerate(frames) if "py::jit_entry:<jit>" in frame
129+
i for i, frame in enumerate(frames) if JIT_EXECUTOR_FRAME in frame
130+
)
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}",
135138
)
136139
eval_after_jit = any(
137140
re.search(EVAL_FRAME_RE, frame)
@@ -142,10 +145,23 @@ def _assert_jit_backtrace_shape(self, gdb_output, *, anchor_at_top):
142145
f"expected an eval frame after the JIT frame\n"
143146
f"backtrace:\n{backtrace}",
144147
)
148+
eval_after_entry = any(
149+
re.search(EVAL_FRAME_RE, frame)
150+
for frame in frames[jit_entry_index + 1:]
151+
)
152+
self.assertTrue(
153+
eval_after_entry,
154+
"expected an eval frame after _PyJIT_Entry\n"
155+
f"backtrace:\n{backtrace}",
156+
)
145157
relevant_end = max(
146158
i
147159
for i, frame in enumerate(frames)
148-
if "py::jit_entry:<jit>" in frame or re.search(EVAL_FRAME_RE, frame)
160+
if (
161+
JIT_EXECUTOR_FRAME in frame
162+
or re.search(JIT_ENTRY_RE, frame)
163+
or re.search(EVAL_FRAME_RE, frame)
164+
)
149165
)
150166
truncated_frames = [
151167
frame for frame in frames[: relevant_end + 1]
@@ -159,46 +175,29 @@ def _assert_jit_backtrace_shape(self, gdb_output, *, anchor_at_top):
159175
if anchor_at_top:
160176
self.assertRegex(
161177
frames[0],
162-
re.compile(r"^#0\s+py::jit_entry:<jit>"),
178+
re.compile(rf"^#0\s+{re.escape(JIT_EXECUTOR_FRAME)}"),
163179
)
164180

165-
def test_bt_shows_compiled_jit_entry(self):
166-
gdb_output = self.get_stack_trace(
167-
script=JIT_SAMPLE_SCRIPT,
168-
breakpoint="_Py_LazyJitShim",
169-
cmds_after_breakpoint=[
170-
BREAK_IN_COMPILED_JIT_ENTRY,
171-
"continue",
172-
"bt",
173-
],
174-
PYTHON_JIT="1",
175-
)
176-
# GDB registers the compiled JIT entry and per-trace JIT regions under
177-
# the same synthetic symbol name; breaking at the entry PC pins the
178-
# JIT frame at #0.
179-
self._assert_jit_backtrace_shape(gdb_output, anchor_at_top=True)
180-
181181
def test_bt_unwinds_through_jit_frames(self):
182182
gdb_output = self.get_stack_trace(
183183
script=JIT_SAMPLE_SCRIPT,
184184
cmds_after_breakpoint=["bt"],
185185
PYTHON_JIT="1",
186186
)
187187
# The executor should appear as a named JIT frame and unwind back into
188-
# the eval loop. Whether GDB also materializes a separate shim frame is
189-
# an implementation detail of the synthetic executor CFI.
188+
# the eval loop.
190189
self._assert_jit_backtrace_shape(gdb_output, anchor_at_top=False)
191190

192-
def test_bt_unwinds_from_inside_jit_entry(self):
191+
def test_bt_unwinds_from_inside_jit_executor(self):
193192
gdb_output = self.get_stack_trace(
194193
script=JIT_SAMPLE_SCRIPT,
195194
cmds_after_breakpoint=[
196-
FINISH_TO_JIT_ENTRY,
197-
STEP_INSIDE_JIT_ENTRY,
195+
FINISH_TO_JIT_EXECUTOR,
196+
STEP_INSIDE_JIT_EXECUTOR,
198197
"bt",
199198
],
200199
PYTHON_JIT="1",
201200
)
202-
# Once the selected PC is inside the JIT entry, we require that GDB
201+
# Once the selected PC is inside the JIT executor, we require that GDB
203202
# identifies the JIT frame at #0 and keeps unwinding into _PyEval_*.
204203
self._assert_jit_backtrace_shape(gdb_output, anchor_at_top=True)

Python/bytecodes.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6118,6 +6118,11 @@ 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();
61216126
assert(tstate->jit_exit == NULL || tstate->jit_exit->executor == current_executor);
61226127
tstate->current_executor = (PyObject *)current_executor;
61236128
if (!current_executor->vm_data.valid) {

Python/ceval.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1305,7 +1305,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
13051305
}
13061306
#ifdef _Py_TIER2
13071307
#ifdef _Py_JIT
1308-
_PyJitEntryFuncPtr _Py_jit_entry = _PyJIT;
1308+
_PyJitEntryFuncPtr _Py_jit_entry = _PyJIT_Entry;
13091309
#else
13101310
_PyJitEntryFuncPtr _Py_jit_entry = _PyTier2Interpreter;
13111311
#endif

Python/ceval_macros.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,20 @@
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+
171185

172186
/* PRE_DISPATCH_GOTO() does lltrace if enabled. Normally a no-op */
173187
#ifdef Py_DEBUG

Python/executor_cases.c.h

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

Python/jit.c

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@ jit_error(const char *message)
6363

6464
static void *
6565
jit_record_code(const void *code_addr, size_t code_size,
66-
const char *entry, const char *filename,
67-
const _PyJitUnwind_ShimCfi *shim_cfi)
66+
const char *entry, const char *filename)
6867
{
6968
#ifdef PY_HAVE_PERF_TRAMPOLINE
7069
_PyPerf_Callbacks callbacks;
@@ -78,20 +77,16 @@ jit_record_code(const void *code_addr, size_t code_size,
7877

7978
#if defined(__linux__) && defined(__ELF__)
8079
return _PyJitUnwind_GdbRegisterCode(
81-
code_addr, code_size, entry, filename, shim_cfi);
80+
code_addr, code_size, entry, filename);
8281
#else
8382
(void)code_addr;
8483
(void)code_size;
8584
(void)entry;
8685
(void)filename;
87-
(void)shim_cfi;
8886
return NULL;
8987
#endif
9088
}
9189

92-
static size_t _Py_jit_shim_size = 0;
93-
static void *_Py_jit_shim_gdb_handle = NULL;
94-
9590
static int
9691
address_in_executor_array(_PyExecutorObject **ptrs, size_t count, uintptr_t addr)
9792
{
@@ -749,9 +744,8 @@ _PyJIT_Compile(_PyExecutorObject *executor, const _PyUOpInstruction trace[], siz
749744
executor->jit_size = total_size;
750745
executor->jit_gdb_handle = jit_record_code(memory,
751746
code_size + state.trampolines.size,
752-
"jit_entry",
753-
"<jit>",
754-
/*shim_cfi=*/NULL);
747+
"jit",
748+
"executor");
755749
return 0;
756750
}
757751

0 commit comments

Comments
 (0)