| 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) |