Skip to content

Commit d7d8703

Browse files
authored
[Dexter] Implement DexStepFunction and DexContinue (#152721)
Adding together in a single commit as their implementations are linked. Only supported for DAP debuggers. These two commands make it a bit easier to drive dexter: DexStepFunction tells dexter to step-next though a function and DexContinue tells dexter to continue (run free) from one breakpoint to another within the current DexStepFunction function. When the DexStepFunction function breakpoint is triggered, dexter sets an instruction breakpoint at the return-address. This is so that stepping can resume in any other DexStepFunctions deeps in the call stack.
1 parent a53a5ed commit d7d8703

File tree

6 files changed

+314
-41
lines changed

6 files changed

+314
-41
lines changed

cross-project-tests/debuginfo-tests/dexter/dex/command/ParseCommand.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
from dex.command.commands.DexFinishTest import DexFinishTest
3636
from dex.command.commands.DexUnreachable import DexUnreachable
3737
from dex.command.commands.DexWatch import DexWatch
38+
from dex.command.commands.DexStepFunction import DexStepFunction
39+
from dex.command.commands.DexContinue import DexContinue
3840
from dex.utils import Timer
3941
from dex.utils.Exceptions import CommandParseError, DebuggerException
4042

@@ -59,6 +61,8 @@ def _get_valid_commands():
5961
DexFinishTest.get_name(): DexFinishTest,
6062
DexUnreachable.get_name(): DexUnreachable,
6163
DexWatch.get_name(): DexWatch,
64+
DexStepFunction.get_name(): DexStepFunction,
65+
DexContinue.get_name(): DexContinue,
6266
}
6367

6468

cross-project-tests/debuginfo-tests/dexter/dex/debugger/DAP.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -381,8 +381,6 @@ def _handle_message(
381381
else:
382382
pass
383383
elif message["type"] == "response":
384-
request_seq = message["request_seq"]
385-
debugger_state.set_response(request_seq, message)
386384
# TODO: We also receive a "continued" event, but it seems reasonable to set state based on either the
387385
# response or the event, since the DAP does not specify an order in which they are sent. May need revisiting
388386
# if there turns out to be some odd ordering issues, e.g. if we can receive messages in the order
@@ -409,6 +407,10 @@ def _handle_message(
409407
body = message.get("body")
410408
if body:
411409
debugger_state.capabilities.update(logger, body)
410+
# Now we've done whatever we need to do with the response, tell the
411+
# receiver thread we've got it.
412+
request_seq = message["request_seq"]
413+
debugger_state.set_response(request_seq, message)
412414

413415
def _colorize_dap_message(message: dict) -> dict:
414416
colorized_message = copy.deepcopy(message)
@@ -696,11 +698,12 @@ def get_triggered_breakpoint_ids(self):
696698
# Breakpoints can only have been triggered if we've hit one.
697699
stop_reason = self._translate_stop_reason(self._debugger_state.stopped_reason)
698700
if stop_reason != StopReason.BREAKPOINT:
699-
return []
701+
return set()
700702
breakpoint_ids = set(
701703
[
702704
dex_id
703705
for dap_id in self._debugger_state.stopped_bps
706+
if dap_id in self.dap_id_to_dex_ids
704707
for dex_id in self.dap_id_to_dex_ids[dap_id]
705708
]
706709
)

cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ConditionalController.py

Lines changed: 199 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from dex.debugger.DebuggerBase import DebuggerBase
2323
from dex.utils.Exceptions import DebuggerException
2424
from dex.utils.Timeout import Timeout
25-
25+
from dex.dextIR import LocIR
2626

2727
class BreakpointRange:
2828
"""A range of breakpoints and a set of conditions.
@@ -51,6 +51,9 @@ def __init__(
5151
values: list,
5252
hit_count: int,
5353
finish_on_remove: bool,
54+
is_continue: bool = False,
55+
function: str = None,
56+
addr: str = None,
5457
):
5558
self.expression = expression
5659
self.path = path
@@ -60,6 +63,72 @@ def __init__(
6063
self.max_hit_count = hit_count
6164
self.current_hit_count = 0
6265
self.finish_on_remove = finish_on_remove
66+
self.is_continue = is_continue
67+
self.function = function
68+
self.addr = addr
69+
70+
def limit_steps(
71+
expression: str,
72+
path: str,
73+
range_from: int,
74+
range_to: int,
75+
values: list,
76+
hit_count: int,
77+
):
78+
return BreakpointRange(
79+
expression,
80+
path,
81+
range_from,
82+
range_to,
83+
values,
84+
hit_count,
85+
False,
86+
)
87+
88+
def finish_test(
89+
expression: str, path: str, on_line: int, values: list, hit_count: int
90+
):
91+
return BreakpointRange(
92+
expression,
93+
path,
94+
on_line,
95+
on_line,
96+
values,
97+
hit_count,
98+
True,
99+
)
100+
101+
def continue_from_to(
102+
expression: str,
103+
path: str,
104+
from_line: int,
105+
to_line: int,
106+
values: list,
107+
hit_count: int,
108+
):
109+
return BreakpointRange(
110+
expression,
111+
path,
112+
from_line,
113+
to_line,
114+
values,
115+
hit_count,
116+
finish_on_remove=False,
117+
is_continue=True,
118+
)
119+
120+
def step_function(function: str, path: str, hit_count: int):
121+
return BreakpointRange(
122+
None,
123+
path,
124+
None,
125+
None,
126+
None,
127+
hit_count,
128+
finish_on_remove=False,
129+
is_continue=False,
130+
function=function,
131+
)
63132

64133
def has_conditions(self):
65134
return self.expression is not None
@@ -96,34 +165,40 @@ def __init__(self, context, step_collection):
96165
def _build_bp_ranges(self):
97166
commands = self.step_collection.commands
98167
self._bp_ranges = []
99-
try:
100-
limit_commands = commands["DexLimitSteps"]
101-
for lc in limit_commands:
102-
bpr = BreakpointRange(
103-
lc.expression,
104-
lc.path,
105-
lc.from_line,
106-
lc.to_line,
107-
lc.values,
108-
lc.hit_count,
109-
False,
110-
)
111-
self._bp_ranges.append(bpr)
112-
except KeyError:
168+
169+
cond_controller_cmds = ["DexLimitSteps", "DexStepFunction", "DexContinue"]
170+
if not any(c in commands for c in cond_controller_cmds):
113171
raise DebuggerException(
114-
"Missing DexLimitSteps commands, cannot conditionally step."
172+
f"No conditional commands {cond_controller_cmds}, cannot conditionally step."
115173
)
174+
175+
if "DexLimitSteps" in commands:
176+
for c in commands["DexLimitSteps"]:
177+
bpr = BreakpointRange.limit_steps(
178+
c.expression,
179+
c.path,
180+
c.from_line,
181+
c.to_line,
182+
c.values,
183+
c.hit_count,
184+
)
185+
self._bp_ranges.append(bpr)
116186
if "DexFinishTest" in commands:
117-
finish_commands = commands["DexFinishTest"]
118-
for ic in finish_commands:
119-
bpr = BreakpointRange(
120-
ic.expression,
121-
ic.path,
122-
ic.on_line,
123-
ic.on_line,
124-
ic.values,
125-
ic.hit_count + 1,
126-
True,
187+
for c in commands["DexFinishTest"]:
188+
bpr = BreakpointRange.finish_test(
189+
c.expression, c.path, c.on_line, c.values, c.hit_count + 1
190+
)
191+
self._bp_ranges.append(bpr)
192+
if "DexContinue" in commands:
193+
for c in commands["DexContinue"]:
194+
bpr = BreakpointRange.continue_from_to(
195+
c.expression, c.path, c.from_line, c.to_line, c.values, c.hit_count
196+
)
197+
self._bp_ranges.append(bpr)
198+
if "DexStepFunction" in commands:
199+
for c in commands["DexStepFunction"]:
200+
bpr = BreakpointRange.step_function(
201+
c.get_function(), c.path, c.hit_count
127202
)
128203
self._bp_ranges.append(bpr)
129204

@@ -138,6 +213,9 @@ def _set_leading_bps(self):
138213
bpr.path, bpr.range_from, cond_expr
139214
)
140215
self._leading_bp_handles[id] = bpr
216+
elif bpr.function is not None:
217+
id = self.debugger.add_function_breakpoint(bpr.function)
218+
self._leading_bp_handles[id] = bpr
141219
else:
142220
# Add an unconditional breakpoint.
143221
id = self.debugger.add_breakpoint(bpr.path, bpr.range_from)
@@ -163,6 +241,9 @@ def _run_debugger_custom(self, cmdline):
163241
timed_out = False
164242
total_timeout = Timeout(self.context.options.timeout_total)
165243

244+
step_function_backtraces: list[list[str]] = []
245+
self.instr_bp_ids = set()
246+
166247
while not self.debugger.is_finished:
167248
breakpoint_timeout = Timeout(self.context.options.timeout_breakpoint)
168249
while self.debugger.is_running and not timed_out:
@@ -185,21 +266,26 @@ def _run_debugger_custom(self, cmdline):
185266
break
186267

187268
step_info = self.debugger.get_step_info(self._watches, self._step_index)
269+
backtrace = None
188270
if step_info.current_frame:
189271
self._step_index += 1
190-
update_step_watches(
191-
step_info, self._watches, self.step_collection.commands
192-
)
193-
self.step_collection.new_step(self.context, step_info)
272+
backtrace = [f.function for f in step_info.frames]
194273

274+
record_step = False
275+
debugger_continue = False
195276
bp_to_delete = []
196277
for bp_id in self.debugger.get_triggered_breakpoint_ids():
197278
try:
198279
# See if this is one of our leading breakpoints.
199280
bpr = self._leading_bp_handles[bp_id]
281+
record_step = True
200282
except KeyError:
201283
# This is a trailing bp. Mark it for removal.
202284
bp_to_delete.append(bp_id)
285+
if bp_id in self.instr_bp_ids:
286+
self.instr_bp_ids.remove(bp_id)
287+
else:
288+
record_step = True
203289
continue
204290

205291
bpr.add_hit()
@@ -208,17 +294,93 @@ def _run_debugger_custom(self, cmdline):
208294
exit_desired = True
209295
bp_to_delete.append(bp_id)
210296
del self._leading_bp_handles[bp_id]
211-
# Add a range of trailing breakpoints covering the lines
212-
# requested in the DexLimitSteps command. Ignore first line as
213-
# that's covered by the leading bp we just hit and include the
214-
# final line.
215-
for line in range(bpr.range_from + 1, bpr.range_to + 1):
216-
self.debugger.add_breakpoint(bpr.path, line)
297+
298+
if bpr.function is not None:
299+
if step_info.frames:
300+
# Add this backtrace to the stack. While the current
301+
# backtrace matches the top of the stack we'll step,
302+
# and while there's a backtrace in the stack that
303+
# is a subset of the current backtrace we'll step-out.
304+
if (
305+
len(step_function_backtraces) == 0
306+
or backtrace != step_function_backtraces[-1]
307+
):
308+
step_function_backtraces.append(backtrace)
309+
310+
# Add an address breakpoint so we don't fall out
311+
# the end of nested DexStepFunctions with a DexContinue.
312+
addr = self.debugger.get_pc(frame_idx=1)
313+
instr_id = self.debugger.add_instruction_breakpoint(addr)
314+
# Note the breakpoint so we don't log the source location
315+
# it in the trace later.
316+
self.instr_bp_ids.add(instr_id)
317+
318+
elif bpr.is_continue:
319+
debugger_continue = True
320+
if bpr.range_to is not None:
321+
self.debugger.add_breakpoint(bpr.path, bpr.range_to)
322+
323+
else:
324+
# Add a range of trailing breakpoints covering the lines
325+
# requested in the DexLimitSteps command. Ignore first line as
326+
# that's covered by the leading bp we just hit and include the
327+
# final line.
328+
for line in range(bpr.range_from + 1, bpr.range_to + 1):
329+
id = self.debugger.add_breakpoint(bpr.path, line)
217330

218331
# Remove any trailing or expired leading breakpoints we just hit.
219332
self.debugger.delete_breakpoints(bp_to_delete)
220333

334+
debugger_next = False
335+
debugger_out = False
336+
if not debugger_continue and step_info.current_frame and step_info.frames:
337+
while len(step_function_backtraces) > 0:
338+
match_subtrace = False # Backtrace contains a target trace.
339+
match_trace = False # Backtrace matches top of target stack.
340+
341+
# The top of the step_function_backtraces stack contains a
342+
# backtrace that we want to step through. Check if the
343+
# current backtrace ("backtrace") either matches that trace
344+
# or otherwise contains it.
345+
target_backtrace = step_function_backtraces[-1]
346+
if len(backtrace) >= len(target_backtrace):
347+
match_trace = len(backtrace) == len(target_backtrace)
348+
# Check if backtrace contains target_backtrace, matching
349+
# from the end (bottom of call stack) backwards.
350+
match_subtrace = (
351+
backtrace[-len(target_backtrace) :] == target_backtrace
352+
)
353+
354+
if match_trace:
355+
# We want to step through this function; do so and
356+
# log the steps in the step trace.
357+
debugger_next = True
358+
record_step = True
359+
break
360+
elif match_subtrace:
361+
# There's a function we care about buried in the
362+
# current backtrace. Step-out until we get to it.
363+
debugger_out = True
364+
break
365+
else:
366+
# Drop backtraces that are not match_subtraces of the current
367+
# backtrace; the functions we wanted to step through
368+
# there are no longer reachable.
369+
step_function_backtraces.pop()
370+
371+
if record_step and step_info.current_frame:
372+
# Record the step.
373+
update_step_watches(
374+
step_info, self._watches, self.step_collection.commands
375+
)
376+
self.step_collection.new_step(self.context, step_info)
377+
221378
if exit_desired:
222379
break
223-
self.debugger.go()
380+
elif debugger_next:
381+
self.debugger.step_next()
382+
elif debugger_out:
383+
self.debugger.step_out()
384+
else:
385+
self.debugger.go()
224386
time.sleep(self._pause_between_steps)

cross-project-tests/debuginfo-tests/dexter/dex/tools/test/Tool.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ def _init_debugger_controller(self):
121121

122122
self.context.options.source_files.extend(list(new_source_files))
123123

124-
if "DexLimitSteps" in step_collection.commands:
124+
cond_controller_cmds = ["DexLimitSteps", "DexStepFunction", "DexContinue"]
125+
if any(c in step_collection.commands for c in cond_controller_cmds):
125126
debugger_controller = ConditionalController(self.context, step_collection)
126127
else:
127128
debugger_controller = DefaultController(self.context, step_collection)

0 commit comments

Comments
 (0)