mirror of
https://github.com/python/cpython.git
synced 2026-05-06 04:37:33 -04:00
211 lines
8.4 KiB
Python
211 lines
8.4 KiB
Python
import os
|
|
import platform
|
|
import re
|
|
import sys
|
|
import unittest
|
|
|
|
from test.support import import_helper
|
|
|
|
from .util import setup_module, DebuggerTests
|
|
|
|
|
|
_testinternalcapi = import_helper.import_module("_testinternalcapi")
|
|
NATIVE_JIT_ENABLED = (
|
|
hasattr(sys, "_jit")
|
|
and sys._jit.is_enabled()
|
|
and _testinternalcapi.get_jit_backend() == "jit"
|
|
)
|
|
|
|
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 executor.
|
|
# That gives a deterministic backtrace starting with py::jit:executor.
|
|
#
|
|
# 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
|
|
# After landing on the JIT entry frame, single-step a bounded number of
|
|
# instructions further into the blob so the backtrace is taken from JIT code
|
|
# itself rather than the immediate helper-return site. The exact number of
|
|
# steps is not significant: each step is cross-checked against the selected
|
|
# frame's symbol so the test fails loudly if stepping escapes the registered
|
|
# JIT region, instead of asserting against a misleading backtrace.
|
|
MAX_JIT_ENTRY_STEPS = 4
|
|
EVAL_FRAME_RE = r"(_PyEval_EvalFrameDefault|_PyEval_Vector)"
|
|
JIT_EXECUTOR_FRAME = "py::jit:executor"
|
|
JIT_ENTRY_SYMBOL = "_PyJIT_Entry"
|
|
BACKTRACE_FRAME_RE = re.compile(r"^#\d+\s+.*$", re.MULTILINE)
|
|
|
|
FINISH_TO_JIT_EXECUTOR = (
|
|
"python exec(\"import gdb\\n"
|
|
f"target = {JIT_EXECUTOR_FRAME!r}\\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\")"
|
|
)
|
|
STEP_INSIDE_JIT_EXECUTOR = (
|
|
"python exec(\"import gdb\\n"
|
|
f"target = {JIT_EXECUTOR_FRAME!r}\\n"
|
|
f"for _ in range({MAX_JIT_ENTRY_STEPS}):\\n"
|
|
" frame = gdb.selected_frame()\\n"
|
|
" if frame is None or frame.name() != target:\\n"
|
|
" raise RuntimeError('left JIT region during stepping: '\\n"
|
|
" + repr(frame and frame.name()))\\n"
|
|
" gdb.execute('si')\\n"
|
|
"frame = gdb.selected_frame()\\n"
|
|
"if frame is None or frame.name() != target:\\n"
|
|
" raise RuntimeError('stepped out of JIT region after si')\\n\")"
|
|
)
|
|
|
|
|
|
def setUpModule():
|
|
setup_module()
|
|
|
|
|
|
# The GDB JIT interface registration is gated on __linux__ && __ELF__ in
|
|
# Python/jit_unwind.c, and the synthetic EH-frame is only implemented for
|
|
# x86_64 and AArch64 (a #error fires otherwise). Skip cleanly on other
|
|
# platforms or architectures instead of producing timeouts / empty backtraces.
|
|
# sys._jit.is_enabled() is true for --enable-experimental-jit=interpreter,
|
|
# but these tests need native JIT code and a py::jit:executor frame.
|
|
@unittest.skipUnless(sys.platform == "linux",
|
|
"GDB JIT interface is only implemented for Linux + ELF")
|
|
@unittest.skipUnless(platform.machine() in ("x86_64", "aarch64"),
|
|
"GDB JIT CFI emitter only supports x86_64 and AArch64")
|
|
@unittest.skipUnless(NATIVE_JIT_ENABLED,
|
|
"requires native JIT execution active")
|
|
class JitBacktraceTests(DebuggerTests):
|
|
def get_stack_trace(self, **kwargs):
|
|
# These tests validate the JIT-relevant part of the backtrace via
|
|
# _assert_jit_backtrace_shape, so an unrelated "?? ()" frame below
|
|
# the JIT/eval segment (e.g. libc without debug info) is tolerable.
|
|
kwargs.setdefault("skip_on_truncation", False)
|
|
return super().get_stack_trace(**kwargs)
|
|
|
|
def _extract_backtrace_frames(self, gdb_output):
|
|
frames = BACKTRACE_FRAME_RE.findall(gdb_output)
|
|
self.assertGreater(
|
|
len(frames), 0,
|
|
f"expected at least one GDB backtrace frame in output:\n{gdb_output}",
|
|
)
|
|
return frames
|
|
|
|
def _assert_jit_backtrace_shape(self, gdb_output, *, anchor_at_top):
|
|
# Shape assertions applied to every JIT backtrace we produce:
|
|
# 1. The synthetic JIT symbol appears exactly once. A second
|
|
# py::jit:executor frame would mean the unwinder is
|
|
# materializing two native frames for a single logical JIT
|
|
# region, or failing to unwind out of the region entirely.
|
|
# 2. The unwinder must climb directly back out of the JIT region
|
|
# into the eval loop. _PyJIT_Entry only exists to establish the
|
|
# physical frame; the synthetic executor FDE collapses it away.
|
|
# 3. For tests that assert a specific entry PC, the JIT frame
|
|
# is also at #0.
|
|
frames = self._extract_backtrace_frames(gdb_output)
|
|
backtrace = "\n".join(frames)
|
|
|
|
jit_frames = [frame for frame in frames if JIT_EXECUTOR_FRAME in frame]
|
|
jit_count = len(jit_frames)
|
|
self.assertEqual(
|
|
jit_count, 1,
|
|
f"expected exactly 1 {JIT_EXECUTOR_FRAME} frame, got {jit_count}\n"
|
|
f"backtrace:\n{backtrace}",
|
|
)
|
|
eval_frames = [frame for frame in frames if re.search(EVAL_FRAME_RE, frame)]
|
|
eval_count = len(eval_frames)
|
|
self.assertGreaterEqual(
|
|
eval_count, 1,
|
|
f"expected at least one _PyEval_* frame, got {eval_count}\n"
|
|
f"backtrace:\n{backtrace}",
|
|
)
|
|
jit_frame_index = next(
|
|
i for i, frame in enumerate(frames) if JIT_EXECUTOR_FRAME in frame
|
|
)
|
|
frames_after_jit = frames[jit_frame_index + 1:]
|
|
first_eval_offset = next(
|
|
(
|
|
i for i, frame in enumerate(frames_after_jit)
|
|
if re.search(EVAL_FRAME_RE, frame)
|
|
),
|
|
None,
|
|
)
|
|
self.assertIsNotNone(
|
|
first_eval_offset,
|
|
f"expected an eval frame after the JIT frame\n"
|
|
f"backtrace:\n{backtrace}",
|
|
)
|
|
unexpected_between = frames_after_jit[:first_eval_offset]
|
|
self.assertFalse(
|
|
unexpected_between,
|
|
"expected the executor frame to unwind directly into eval\n"
|
|
f"backtrace:\n{backtrace}",
|
|
)
|
|
relevant_end = max(
|
|
i
|
|
for i, frame in enumerate(frames)
|
|
if (
|
|
JIT_EXECUTOR_FRAME in frame
|
|
or re.search(EVAL_FRAME_RE, frame)
|
|
)
|
|
)
|
|
truncated_frames = [
|
|
frame for frame in frames[: relevant_end + 1]
|
|
if " ?? ()" in frame
|
|
]
|
|
self.assertFalse(
|
|
truncated_frames,
|
|
"unexpected truncated frame before the validated JIT/eval segment\n"
|
|
f"backtrace:\n{backtrace}",
|
|
)
|
|
if anchor_at_top:
|
|
self.assertRegex(
|
|
frames[0],
|
|
re.compile(rf"^#0\s+{re.escape(JIT_EXECUTOR_FRAME)}"),
|
|
)
|
|
|
|
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.
|
|
self._assert_jit_backtrace_shape(gdb_output, anchor_at_top=False)
|
|
|
|
def test_bt_handoff_from_jit_entry_to_executor(self):
|
|
gdb_output = self.get_stack_trace(
|
|
script=JIT_SAMPLE_SCRIPT,
|
|
breakpoint=JIT_ENTRY_SYMBOL,
|
|
cmds_after_breakpoint=[
|
|
"delete 1",
|
|
"tbreak builtin_id",
|
|
"continue",
|
|
"bt",
|
|
],
|
|
PYTHON_JIT="1",
|
|
)
|
|
# If we stop first in the shim and then continue into the real JIT
|
|
# workload, the final backtrace should match the architecture's
|
|
# executor unwind contract.
|
|
self._assert_jit_backtrace_shape(gdb_output, anchor_at_top=False)
|
|
|
|
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,
|
|
STEP_INSIDE_JIT_EXECUTOR,
|
|
"bt",
|
|
],
|
|
PYTHON_JIT="1",
|
|
)
|
|
# Once the selected PC is inside the JIT executor, we require that GDB
|
|
# identifies the JIT frame at #0 and keeps unwinding into _PyEval_*.
|
|
self._assert_jit_backtrace_shape(gdb_output, anchor_at_top=True)
|