| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| #ifndef Py_INTERNAL_JIT_UNWIND_H | ||
| #define Py_INTERNAL_JIT_UNWIND_H | ||
|
|
||
| #ifndef Py_BUILD_CORE | ||
| # error "this header requires Py_BUILD_CORE define" | ||
| #endif | ||
|
|
||
| #include <stddef.h> | ||
| #include <stdint.h> | ||
|
|
||
| #if defined(_Py_JIT) && defined(__linux__) && defined(__ELF__) | ||
| # define PY_HAVE_JIT_GDB_UNWIND | ||
| #endif | ||
|
|
||
| #if defined(PY_HAVE_PERF_TRAMPOLINE) || defined(PY_HAVE_JIT_GDB_UNWIND) | ||
|
|
||
| #if defined(PY_HAVE_JIT_GDB_UNWIND) | ||
| extern PyMutex _Py_jit_debug_mutex; | ||
| #endif | ||
|
|
||
| /* DWARF exception-handling pointer encodings shared by JIT unwind users. */ | ||
| enum { | ||
| DWRF_EH_PE_absptr = 0x00, | ||
| DWRF_EH_PE_omit = 0xff, | ||
|
|
||
| /* Data type encodings */ | ||
| DWRF_EH_PE_uleb128 = 0x01, | ||
| DWRF_EH_PE_udata2 = 0x02, | ||
| DWRF_EH_PE_udata4 = 0x03, | ||
| DWRF_EH_PE_udata8 = 0x04, | ||
| DWRF_EH_PE_sleb128 = 0x09, | ||
| DWRF_EH_PE_sdata2 = 0x0a, | ||
| DWRF_EH_PE_sdata4 = 0x0b, | ||
| DWRF_EH_PE_sdata8 = 0x0c, | ||
| DWRF_EH_PE_signed = 0x08, | ||
|
|
||
| /* Reference type encodings */ | ||
| DWRF_EH_PE_pcrel = 0x10, | ||
| DWRF_EH_PE_textrel = 0x20, | ||
| DWRF_EH_PE_datarel = 0x30, | ||
| DWRF_EH_PE_funcrel = 0x40, | ||
| DWRF_EH_PE_aligned = 0x50, | ||
| DWRF_EH_PE_indirect = 0x80 | ||
| }; | ||
|
|
||
| /* Return the size of the generated .eh_frame data for the given encoding. */ | ||
| size_t _PyJitUnwind_EhFrameSize(int absolute_addr); | ||
|
|
||
| /* | ||
| * Build DWARF .eh_frame data for JIT code; returns size written or 0 on error. | ||
| * absolute_addr selects the FDE address encoding: | ||
| * - 0: PC-relative offsets (perf jitdump synthesized DSO). | ||
| * - nonzero: absolute addresses (GDB JIT in-memory ELF). | ||
| */ | ||
| size_t _PyJitUnwind_BuildEhFrame(uint8_t *buffer, size_t buffer_size, | ||
| const void *code_addr, size_t code_size, | ||
| int absolute_addr); | ||
|
|
||
| void *_PyJitUnwind_GdbRegisterCode(const void *code_addr, | ||
| size_t code_size, | ||
| const char *entry, | ||
| const char *filename); | ||
|
|
||
| void _PyJitUnwind_GdbUnregisterCode(void *handle); | ||
|
|
||
| #endif // defined(PY_HAVE_PERF_TRAMPOLINE) || defined(PY_HAVE_JIT_GDB_UNWIND) | ||
|
|
||
| #endif // Py_INTERNAL_JIT_UNWIND_H | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| # Sample script for use by test_gdb.test_jit | ||
|
|
||
| import _testinternalcapi | ||
| import operator | ||
|
|
||
|
|
||
| WARMUP_ITERATIONS = _testinternalcapi.TIER2_THRESHOLD + 10 | ||
|
|
||
|
|
||
| def jit_bt_hot(depth, warming_up_caller=False): | ||
| if depth == 0: | ||
| if not warming_up_caller: | ||
| id(42) | ||
| return | ||
|
|
||
| for iteration in range(WARMUP_ITERATIONS): | ||
| operator.call( | ||
| jit_bt_hot, | ||
| depth - 1, | ||
| warming_up_caller or iteration + 1 != WARMUP_ITERATIONS, | ||
| ) | ||
|
|
||
|
|
||
| # Warm the shared shim once without hitting builtin_id so the real run uses | ||
| # the steady-state shim path when GDB breaks inside id(42). | ||
| jit_bt_hot(1, warming_up_caller=True) | ||
| jit_bt_hot(1) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,201 @@ | ||
| import os | ||
| import platform | ||
| import re | ||
| import sys | ||
| import unittest | ||
|
|
||
| from .util import setup_module, DebuggerTests | ||
|
|
||
|
|
||
| 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. | ||
| # is_enabled() implies is_available() and also implies that the runtime has | ||
| # JIT execution active; interpreter-only tier 2 builds don't hit this path. | ||
| @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(hasattr(sys, "_jit") and sys._jit.is_enabled(), | ||
| "requires a JIT-enabled build with 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) |
| Back | FazBrowse Home | New Git URL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Qualitythis is missing Py_BUILD_CORE guard no?
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Qualityyes, I've seen now the other headers files.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.