Skip to content

Commit 769400b

Browse files
committed
GH-126910: add test for manual frame unwinding
1 parent cf71e34 commit 769400b

File tree

4 files changed

+498
-2
lines changed

4 files changed

+498
-2
lines changed

Include/internal/pycore_jit.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ typedef _Py_CODEUNIT *(*jit_func)(
2626
int _PyJIT_Compile(_PyExecutorObject *executor, const _PyUOpInstruction *trace, size_t length);
2727
void _PyJIT_Free(_PyExecutorObject *executor);
2828
void _PyJIT_Fini(void);
29+
PyAPI_FUNC(int) _PyJIT_AddressInJitCode(PyInterpreterState *interp, uintptr_t addr);
2930

3031
#endif // _Py_JIT
3132

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import json
2+
import os
3+
import platform
4+
import subprocess
5+
import sys
6+
import sysconfig
7+
import unittest
8+
9+
from test import support
10+
from test.support import import_helper
11+
12+
13+
_testinternalcapi = import_helper.import_module("_testinternalcapi")
14+
15+
16+
if not support.has_subprocess_support:
17+
raise unittest.SkipTest("test requires subprocess support")
18+
19+
20+
def _frame_pointers_expected(machine):
21+
cflags = " ".join(
22+
value for value in (
23+
sysconfig.get_config_var("PY_CORE_CFLAGS"),
24+
sysconfig.get_config_var("CFLAGS"),
25+
)
26+
if value
27+
)
28+
if "no-omit-frame-pointer" in cflags:
29+
return True
30+
if machine in {"aarch64", "arm64"}:
31+
return "-fomit-frame-pointer" not in cflags
32+
if machine == "x86_64":
33+
return False
34+
# MSVC ignores /Oy and /Oy- on x64/ARM64.
35+
if sys.platform == "win32" and machine == "arm64":
36+
# Windows ARM64 guidelines recommend frame pointers (x29) for stack walking.
37+
return True
38+
if sys.platform == "win32" and machine == "x86_64":
39+
# Windows x64 uses unwind metadata; frame pointers are not required.
40+
return None
41+
return None
42+
43+
44+
def _build_stack_and_unwind():
45+
import operator
46+
47+
def build_stack(n, unwinder, warming_up_caller=False):
48+
if warming_up_caller:
49+
return
50+
if n == 0:
51+
return unwinder()
52+
warming_up = True
53+
while warming_up:
54+
# Can't branch on JIT state inside JITted code, so compute here.
55+
warming_up = (
56+
hasattr(sys, "_jit")
57+
and sys._jit.is_enabled()
58+
and not sys._jit.is_active()
59+
)
60+
result = operator.call(build_stack, n - 1, unwinder, warming_up)
61+
return result
62+
63+
stack = build_stack(10, _testinternalcapi.manual_frame_pointer_unwind)
64+
return stack
65+
66+
67+
def _classify_stack(stack, jit_enabled):
68+
labels = _testinternalcapi.classify_stack_addresses(stack, jit_enabled)
69+
70+
annotated = []
71+
jit_frames = 0
72+
python_frames = 0
73+
other_frames = 0
74+
for idx, (frame, tag) in enumerate(zip(stack, labels)):
75+
addr = int(frame)
76+
if tag == "jit":
77+
jit_frames += 1
78+
elif tag == "python":
79+
python_frames += 1
80+
else:
81+
other_frames += 1
82+
annotated.append((idx, addr, tag))
83+
return annotated, python_frames, jit_frames, other_frames
84+
85+
86+
def _annotate_unwind():
87+
stack = _build_stack_and_unwind()
88+
jit_enabled = hasattr(sys, "_jit") and sys._jit.is_enabled()
89+
ranges = _testinternalcapi.get_jit_code_ranges() if jit_enabled else []
90+
if jit_enabled and ranges:
91+
print("JIT ranges:")
92+
for start, end in ranges:
93+
print(f" {int(start):#x}-{int(end):#x}")
94+
annotated, python_frames, jit_frames, other_frames = _classify_stack(
95+
stack, jit_enabled
96+
)
97+
for idx, addr, tag in annotated:
98+
print(f"#{idx:02d} {addr:#x} -> {tag}")
99+
return json.dumps({
100+
"length": len(stack),
101+
"python_frames": python_frames,
102+
"jit_frames": jit_frames,
103+
"other_frames": other_frames,
104+
})
105+
106+
107+
def _manual_unwind_length(**env):
108+
code = (
109+
"from test.test_frame_pointer_unwind import _annotate_unwind; "
110+
"print(_annotate_unwind());"
111+
)
112+
run_env = os.environ.copy()
113+
run_env.update(env)
114+
proc = subprocess.run(
115+
[sys.executable, "-c", code],
116+
env=run_env,
117+
capture_output=True,
118+
text=True,
119+
)
120+
# Surface the output for debugging/visibility when running this test
121+
if proc.stdout:
122+
print(proc.stdout, end="")
123+
if proc.returncode:
124+
raise RuntimeError(
125+
f"unwind helper failed (rc={proc.returncode}): {proc.stderr or proc.stdout}"
126+
)
127+
stdout_lines = proc.stdout.strip().splitlines()
128+
if not stdout_lines:
129+
raise RuntimeError("unwind helper produced no output")
130+
try:
131+
return json.loads(stdout_lines[-1])
132+
except ValueError as exc:
133+
raise RuntimeError(
134+
f"unexpected output from unwind helper: {proc.stdout!r}"
135+
) from exc
136+
137+
138+
class FramePointerUnwindTests(unittest.TestCase):
139+
140+
def setUp(self):
141+
super().setUp()
142+
machine = platform.machine().lower()
143+
expected = _frame_pointers_expected(machine)
144+
if expected is None:
145+
self.skipTest(f"unsupported architecture for frame pointer check: {machine}")
146+
try:
147+
_testinternalcapi.manual_frame_pointer_unwind()
148+
except RuntimeError as exc:
149+
if "not supported" in str(exc):
150+
self.skipTest("manual frame pointer unwinding not supported on this platform")
151+
raise
152+
self.machine = machine
153+
self.frame_pointers_expected = expected
154+
155+
def test_manual_unwind_respects_frame_pointers(self):
156+
jit_available = hasattr(sys, "_jit") and sys._jit.is_available()
157+
envs = [({"PYTHON_JIT": "0"}, False)]
158+
if jit_available:
159+
envs.append(({"PYTHON_JIT": "1"}, True))
160+
161+
for env, using_jit in envs:
162+
with self.subTest(env=env):
163+
result = _manual_unwind_length(**env)
164+
jit_frames = result["jit_frames"]
165+
python_frames = result.get("python_frames", 0)
166+
if self.frame_pointers_expected:
167+
self.assertGreater(
168+
python_frames,
169+
0,
170+
f"expected to find Python frames on {self.machine} with env {env}",
171+
)
172+
if using_jit:
173+
self.assertGreater(
174+
jit_frames,
175+
0,
176+
f"expected to find JIT frames on {self.machine} with env {env}",
177+
)
178+
else:
179+
self.assertEqual(
180+
jit_frames,
181+
0,
182+
f"unexpected JIT frames counted on {self.machine} with env {env}",
183+
)
184+
else:
185+
self.assertEqual(
186+
python_frames,
187+
1,
188+
f"unexpected Python frames counted on {self.machine} with env {env}",
189+
)
190+
self.assertEqual(
191+
jit_frames,
192+
0,
193+
f"unexpected JIT frames counted on {self.machine} with env {env}",
194+
)
195+
196+
197+
if __name__ == "__main__":
198+
unittest.main()

0 commit comments

Comments
 (0)