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 ,
56
+ addr : str = 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,34 +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
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
127
202
)
128
203
self ._bp_ranges .append (bpr )
129
204
@@ -138,6 +213,9 @@ def _set_leading_bps(self):
138
213
bpr .path , bpr .range_from , cond_expr
139
214
)
140
215
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
141
219
else :
142
220
# Add an unconditional breakpoint.
143
221
id = self .debugger .add_breakpoint (bpr .path , bpr .range_from )
@@ -163,6 +241,9 @@ def _run_debugger_custom(self, cmdline):
163
241
timed_out = False
164
242
total_timeout = Timeout (self .context .options .timeout_total )
165
243
244
+ step_function_backtraces : list [list [str ]] = []
245
+ self .instr_bp_ids = set ()
246
+
166
247
while not self .debugger .is_finished :
167
248
breakpoint_timeout = Timeout (self .context .options .timeout_breakpoint )
168
249
while self .debugger .is_running and not timed_out :
@@ -185,21 +266,26 @@ def _run_debugger_custom(self, cmdline):
185
266
break
186
267
187
268
step_info = self .debugger .get_step_info (self ._watches , self ._step_index )
269
+ backtrace = None
188
270
if step_info .current_frame :
189
271
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 ]
194
273
274
+ record_step = False
275
+ debugger_continue = False
195
276
bp_to_delete = []
196
277
for bp_id in self .debugger .get_triggered_breakpoint_ids ():
197
278
try :
198
279
# See if this is one of our leading breakpoints.
199
280
bpr = self ._leading_bp_handles [bp_id ]
281
+ record_step = True
200
282
except KeyError :
201
283
# This is a trailing bp. Mark it for removal.
202
284
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
203
289
continue
204
290
205
291
bpr .add_hit ()
@@ -208,17 +294,93 @@ def _run_debugger_custom(self, cmdline):
208
294
exit_desired = True
209
295
bp_to_delete .append (bp_id )
210
296
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 )
217
330
218
331
# Remove any trailing or expired leading breakpoints we just hit.
219
332
self .debugger .delete_breakpoints (bp_to_delete )
220
333
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
+
221
378
if exit_desired :
222
379
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 ()
224
386
time .sleep (self ._pause_between_steps )
0 commit comments