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)