Skip to content

Commit b9731c9

Browse files
author
Magne Hov
committed
add libpython_extensions.py with disas, advance and step features
1 parent 4aa01a1 commit b9731c9

File tree

1 file changed

+260
-0
lines changed

1 file changed

+260
-0
lines changed

libpython_extensions.py

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import dis
2+
import pathlib
3+
import sys
4+
from typing import Optional
5+
6+
import gdb
7+
from undodb.debugger_extensions import debugger_utils
8+
9+
import libpython
10+
11+
gdb.execute("alias -a pp = py-print")
12+
13+
14+
def check_python_version():
15+
"""
16+
Warn if the inferior's Python version does not match the debugger's Python version.
17+
"""
18+
inferior_version = gdb.parse_and_eval("PY_VERSION").string()
19+
debugger_version = ".".join(
20+
map(str, (sys.version_info.major, sys.version_info.minor))
21+
)
22+
if not inferior_version.startswith(debugger_version):
23+
print(
24+
f"Warning: Mismatched Python version between "
25+
f"inferior ({inferior_version}) and "
26+
f"debugger ({debugger_version}). "
27+
f"The bytecode shown might be wrong."
28+
)
29+
30+
31+
class PyDisassemble(gdb.Command):
32+
"""
33+
Disassemble the bytecode for the currently selected Python frame.
34+
"""
35+
36+
def __init__(self):
37+
gdb.Command.__init__(self, "py-dis", gdb.COMMAND_STACK, gdb.COMPLETE_NONE)
38+
39+
def invoke(self, args, from_tty):
40+
check_python_version()
41+
42+
frame = libpython.Frame.get_selected_bytecode_frame()
43+
if not frame:
44+
print("Unable to find frame with bytecode")
45+
return
46+
47+
frame_object = frame.get_pyop()
48+
# f_lasti is a wordcode index and so must be multiplied by 2 to get byte offset, see cpyton
49+
# commit fc840736e54da0557616882012f362b809490165.
50+
byte_index = frame_object.f_lasti * 2
51+
bytes_object = frame_object.co.pyop_field("co_code")
52+
53+
varnames, names, consts, cellvars, freevars = (
54+
frame_object.co.pyop_field(name).proxyval(set())
55+
for name in (
56+
"co_varnames",
57+
"co_names",
58+
"co_consts",
59+
"co_cellvars",
60+
"co_freevars",
61+
)
62+
)
63+
64+
dis._disassemble_bytes(
65+
bytes(map(ord, str(bytes_object))),
66+
byte_index,
67+
varnames,
68+
names,
69+
consts,
70+
cellvars + freevars,
71+
)
72+
73+
74+
PyDisassemble()
75+
76+
77+
def get_frame_function_name(frame: libpython.Frame) -> str | None:
78+
"""
79+
Return the name of the Python function that corresponds to the given Python frame.
80+
"""
81+
if frame.is_evalframe():
82+
pyop = frame.get_pyop()
83+
if pyop:
84+
return pyop.co_name.proxyval(set())
85+
else:
86+
info = frame.is_other_python_frame()
87+
if info:
88+
return str(info)
89+
return None
90+
91+
92+
def get_evalframe_function_name() -> Optional[str]:
93+
"""
94+
Attempt to return the name of the function in this eval frame.
95+
"""
96+
python_frame = libpython.Frame.get_selected_python_frame()
97+
return get_frame_function_name(python_frame)
98+
99+
100+
def get_cfunction_name() -> str:
101+
"""
102+
Return the name of the C-implemented function which is executing on this cpython frame.
103+
104+
This assumes we're stopped with a PyCFunctionObject object available in "func".
105+
"""
106+
func_ptr = gdb.selected_frame().read_var("func")
107+
python_cfunction = libpython.PyCFunctionObjectPtr.from_pyobject_ptr(func_ptr)
108+
return python_cfunction.proxyval(set()).ml_name
109+
110+
111+
class ConditionalBreakpoint(gdb.Breakpoint):
112+
"""
113+
Breakpoint that will stop the inferior iff the given predicate callable returns True.
114+
"""
115+
116+
def __init__(self, *args, **kwargs):
117+
self.predicate = kwargs.pop("predicate")
118+
super().__init__(*args, **kwargs)
119+
120+
def stop(self):
121+
return self.predicate()
122+
123+
124+
def advance_function(forward: bool, function_name: str) -> None:
125+
"""
126+
Continue the program forwards or backwards until the next time a Python function is called.
127+
"""
128+
with debugger_utils.breakpoints_suspended():
129+
direction = "forwards" if forward else "backwards"
130+
target = (
131+
f"Python function '{function_name}'"
132+
if function_name
133+
else "the next Python function call"
134+
)
135+
print(f"Running {direction} until {target}.")
136+
137+
breakpoints = []
138+
for location, get_name in [
139+
("cfunction_enter_call", get_cfunction_name),
140+
("_PyEval_EvalFrameDefault", get_evalframe_function_name),
141+
]:
142+
bp = ConditionalBreakpoint(
143+
location,
144+
internal=True,
145+
predicate=lambda f=get_name: not function_name or f() == function_name,
146+
)
147+
bp.silent = True
148+
breakpoints.append(bp)
149+
try:
150+
gdb.execute("continue" if forward else "reverse-continue")
151+
finally:
152+
for bp in breakpoints:
153+
bp.delete()
154+
155+
156+
class PythonAdvanceFunction(gdb.Command):
157+
"""
158+
Continue the program until the given Python function is called.
159+
"""
160+
161+
def __init__(self):
162+
super().__init__("py-advance-function", gdb.COMMAND_RUNNING)
163+
164+
def invoke(self, arg, from_tty):
165+
advance_function(True, arg)
166+
167+
168+
PythonAdvanceFunction()
169+
gdb.execute("alias -a pya = py-advance")
170+
171+
172+
class PythonReverseAdvanceFunction(gdb.Command):
173+
"""
174+
Continue the program backwards until the given Python function is called.
175+
"""
176+
177+
def __init__(self):
178+
super().__init__("py-reverse-advance-function", gdb.COMMAND_RUNNING)
179+
180+
def invoke(self, arg, from_tty):
181+
advance_function(False, arg)
182+
183+
184+
PythonReverseAdvanceFunction()
185+
gdb.execute("alias -a pyra = py-reverse-advance")
186+
187+
188+
def get_c_source_location(basename: str, content: str) -> str:
189+
"""
190+
Return linespec for a file matching the given basename and line matching the given content.
191+
192+
The basename is against source files currently known to the debugger. The content is matched
193+
against the first matching filename.
194+
195+
Raises a ValueError if the file or content wasn't found.
196+
"""
197+
sources = gdb.execute(f"info sources {basename}", to_string=True).splitlines()
198+
filename, *_ = (f for f in sources if basename in f)
199+
lines = pathlib.Path(filename).read_text().splitlines()
200+
for lineno, line in enumerate(lines):
201+
if content in line:
202+
return f"{basename}:{lineno}"
203+
raise ValueError(f"Failed to find {content=} in {basename=}")
204+
205+
206+
def python_step_bytecode(*, forwards: bool) -> None:
207+
"""
208+
Continue the program forwards or backwards until the next Python bytecode.
209+
"""
210+
if getattr(python_step_bytecode, "location", None) is None:
211+
try:
212+
basename = "ceval.c"
213+
python_step_bytecode.location = get_c_source_location(
214+
basename, "dispatch_opcode:"
215+
)
216+
except ValueError:
217+
raise gdb.GdbError(
218+
f"Failed to find Python bytecode interpreter loop in {basename}"
219+
)
220+
221+
with debugger_utils.breakpoints_suspended():
222+
bp = gdb.Breakpoint(python_step_bytecode.location, internal=True)
223+
bp.silent = True
224+
try:
225+
gdb.execute("continue" if forwards else "reverse-continue")
226+
finally:
227+
bp.delete()
228+
229+
230+
class PythonStep(gdb.Command):
231+
"""
232+
Continue the program forwards until the next Python bytecode.
233+
"""
234+
235+
def __init__(self):
236+
super().__init__("py-step", gdb.COMMAND_RUNNING)
237+
238+
def invoke(self, arg, from_tty):
239+
python_step_bytecode(forwards=True)
240+
241+
242+
PythonStep()
243+
gdb.execute("alias -a pys = py-step")
244+
245+
246+
class PythonReverseStep(gdb.Command):
247+
"""
248+
Continue the program backwards until the next Python bytecode.
249+
"""
250+
251+
def __init__(self):
252+
super().__init__("py-reverse-step", gdb.COMMAND_RUNNING)
253+
254+
def invoke(self, arg, from_tty):
255+
python_step_bytecode(forwards=False)
256+
257+
258+
PythonReverseStep()
259+
gdb.execute("alias -a pyrs = py-reverse-step")
260+
gdb.execute("alias -a py-rstep = py-reverse-step")

0 commit comments

Comments
 (0)