Skip to content

Commit ba89c6c

Browse files
committed
[Dexter] Implement DexStepFunction and DexContinue
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 3c16976 commit ba89c6c

File tree

5 files changed

+313
-38
lines changed

5 files changed

+313
-38
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/DebuggerControllers/ConditionalController.py

Lines changed: 204 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 = None,
56+
addr: str | None = 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,36 +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
127190
)
128191
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(c.expression, c.path, c.hit_count)
201+
self._bp_ranges.append(bpr)
129202

130203
def _set_leading_bps(self):
131204
# Set a leading breakpoint for each BreakpointRange, building a
@@ -138,6 +211,12 @@ def _set_leading_bps(self):
138211
bpr.path, bpr.range_from, cond_expr
139212
)
140213
self._leading_bp_handles[id] = bpr
214+
elif bpr.function is not None:
215+
id = self.debugger.add_function_breakpoint(bpr.function)
216+
self.context.logger.warning(
217+
f"Set leading breakpoint {id} at {bpr.function}"
218+
)
219+
self._leading_bp_handles[id] = bpr
141220
else:
142221
# Add an unconditional breakpoint.
143222
id = self.debugger.add_breakpoint(bpr.path, bpr.range_from)
@@ -163,6 +242,9 @@ def _run_debugger_custom(self, cmdline):
163242
timed_out = False
164243
total_timeout = Timeout(self.context.options.timeout_total)
165244

245+
step_function_backtraces: list[list[str]] = []
246+
self.instr_bp_ids = set()
247+
166248
while not self.debugger.is_finished:
167249
breakpoint_timeout = Timeout(self.context.options.timeout_breakpoint)
168250
while self.debugger.is_running and not timed_out:
@@ -185,21 +267,26 @@ def _run_debugger_custom(self, cmdline):
185267
break
186268

187269
step_info = self.debugger.get_step_info(self._watches, self._step_index)
270+
backtrace = None
188271
if step_info.current_frame:
189272
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)
273+
backtrace = list([f.function for f in step_info.frames])
194274

275+
log_step = False
276+
debugger_continue = False
195277
bp_to_delete = []
196278
for bp_id in self.debugger.get_triggered_breakpoint_ids():
197279
try:
198280
# See if this is one of our leading breakpoints.
199281
bpr = self._leading_bp_handles[bp_id]
282+
log_step = True
200283
except KeyError:
201284
# This is a trailing bp. Mark it for removal.
202285
bp_to_delete.append(bp_id)
286+
if bp_id in self.instr_bp_ids:
287+
self.instr_bp_ids.remove(bp_id)
288+
else:
289+
log_step = True
203290
continue
204291

205292
bpr.add_hit()
@@ -208,17 +295,97 @@ def _run_debugger_custom(self, cmdline):
208295
exit_desired = True
209296
bp_to_delete.append(bp_id)
210297
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)
298+
299+
if bpr.function is not None:
300+
if step_info.frames:
301+
# Add this backtrace to the stack. While the current
302+
# backtrace matches the top of the stack we'll step,
303+
# and while there's a backtrace in the stack that
304+
# is a subset of the current backtrack we'll step-out.
305+
if (
306+
len(step_function_backtraces) == 0
307+
or backtrace != step_function_backtraces[-1]
308+
):
309+
step_function_backtraces.append(backtrace)
310+
311+
# Add an address breakpoint so we don't fall out
312+
# the end of nested DexStepFunctions with a DexContinue.
313+
addr = self.debugger.get_pc(frame_idx=1)
314+
instr_id = self.debugger.add_instruction_breakpoint(addr)
315+
# Note the breakpoint so we don't log the source location
316+
# it in the trace later.
317+
self.instr_bp_ids.add(instr_id)
318+
319+
elif bpr.is_continue:
320+
debugger_continue = True
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)
330+
self.context.logger.warning(
331+
f"Set trailing breakpoint {id} at {line}"
332+
)
217333

218334
# Remove any trailing or expired leading breakpoints we just hit.
219335
self.debugger.delete_breakpoints(bp_to_delete)
220336

337+
debugger_next = False
338+
debugger_out = False
339+
if (
340+
not debugger_continue
341+
and step_info.current_frame
342+
and step_info.frames
343+
):
344+
while len(step_function_backtraces) > 0:
345+
match_subtrace = False # Backtrace contains a target trace.
346+
match_trace = False # Backtrace matches top of target stack.
347+
if len(backtrace) >= len(step_function_backtraces[-1]):
348+
match_subtrace = True
349+
match_trace = len(backtrace) == len(
350+
step_function_backtraces[-1]
351+
)
352+
for i, f in enumerate(reversed(step_function_backtraces[-1])):
353+
if backtrace[-1 - i] != f:
354+
match_subtrace = False
355+
match_trace = False
356+
break
357+
358+
if match_trace:
359+
# We want to step through this function; do so and
360+
# log the steps in the step trace.
361+
debugger_next = True
362+
log_step = True
363+
break
364+
elif match_subtrace:
365+
# There's a function we care about buried in the
366+
# current backtrace. Step-out until we get to it.
367+
debugger_out = True
368+
break
369+
else:
370+
# Drop backtraces that are not match_subtraces of the current
371+
# backtrace; the functions we wanted to step through
372+
# there are no longer reachable.
373+
step_function_backtraces.pop()
374+
375+
376+
if log_step and step_info.current_frame:
377+
# Record the step.
378+
update_step_watches(
379+
step_info, self._watches, self.step_collection.commands
380+
)
381+
self.step_collection.new_step(self.context, step_info)
382+
221383
if exit_desired:
222384
break
223-
self.debugger.go()
385+
elif debugger_next:
386+
self.debugger.step_next()
387+
elif debugger_out:
388+
self.debugger.step_out()
389+
else:
390+
self.debugger.go()
224391
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)