22
22
from dex .debugger .DebuggerBase import DebuggerBase
23
23
from dex .utils .Exceptions import DebuggerException
24
24
from dex .utils .Timeout import Timeout
25
-
25
+ from dex . dextIR import LocIR
26
26
27
27
class BreakpointRange :
28
28
"""A range of breakpoints and a set of conditions.
@@ -51,6 +51,9 @@ def __init__(
51
51
values : list ,
52
52
hit_count : int ,
53
53
finish_on_remove : bool ,
54
+ is_continue : bool = False ,
55
+ function : str | None = None ,
56
+ addr : str | None = None ,
54
57
):
55
58
self .expression = expression
56
59
self .path = path
@@ -60,6 +63,72 @@ def __init__(
60
63
self .max_hit_count = hit_count
61
64
self .current_hit_count = 0
62
65
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
+ )
63
132
64
133
def has_conditions (self ):
65
134
return self .expression is not None
@@ -96,36 +165,40 @@ def __init__(self, context, step_collection):
96
165
def _build_bp_ranges (self ):
97
166
commands = self .step_collection .commands
98
167
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 ):
113
171
raise DebuggerException (
114
- "Missing DexLimitSteps commands, cannot conditionally step."
172
+ f"No conditional commands { cond_controller_cmds } , cannot conditionally step."
115
173
)
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 )
116
186
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
127
190
)
128
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 (c .expression , c .path , c .hit_count )
201
+ self ._bp_ranges .append (bpr )
129
202
130
203
def _set_leading_bps (self ):
131
204
# Set a leading breakpoint for each BreakpointRange, building a
@@ -138,6 +211,12 @@ def _set_leading_bps(self):
138
211
bpr .path , bpr .range_from , cond_expr
139
212
)
140
213
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
141
220
else :
142
221
# Add an unconditional breakpoint.
143
222
id = self .debugger .add_breakpoint (bpr .path , bpr .range_from )
@@ -163,6 +242,9 @@ def _run_debugger_custom(self, cmdline):
163
242
timed_out = False
164
243
total_timeout = Timeout (self .context .options .timeout_total )
165
244
245
+ step_function_backtraces : list [list [str ]] = []
246
+ self .instr_bp_ids = set ()
247
+
166
248
while not self .debugger .is_finished :
167
249
breakpoint_timeout = Timeout (self .context .options .timeout_breakpoint )
168
250
while self .debugger .is_running and not timed_out :
@@ -185,21 +267,26 @@ def _run_debugger_custom(self, cmdline):
185
267
break
186
268
187
269
step_info = self .debugger .get_step_info (self ._watches , self ._step_index )
270
+ backtrace = None
188
271
if step_info .current_frame :
189
272
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 ])
194
274
275
+ log_step = False
276
+ debugger_continue = False
195
277
bp_to_delete = []
196
278
for bp_id in self .debugger .get_triggered_breakpoint_ids ():
197
279
try :
198
280
# See if this is one of our leading breakpoints.
199
281
bpr = self ._leading_bp_handles [bp_id ]
282
+ log_step = True
200
283
except KeyError :
201
284
# This is a trailing bp. Mark it for removal.
202
285
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
203
290
continue
204
291
205
292
bpr .add_hit ()
@@ -208,17 +295,97 @@ def _run_debugger_custom(self, cmdline):
208
295
exit_desired = True
209
296
bp_to_delete .append (bp_id )
210
297
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
+ )
217
333
218
334
# Remove any trailing or expired leading breakpoints we just hit.
219
335
self .debugger .delete_breakpoints (bp_to_delete )
220
336
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
+
221
383
if exit_desired :
222
384
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 ()
224
391
time .sleep (self ._pause_between_steps )
0 commit comments