2222from dex .debugger .DebuggerBase import DebuggerBase
2323from dex .utils .Exceptions import DebuggerException
2424from dex .utils .Timeout import Timeout
25-
25+ from dex . dextIR import LocIR
2626
2727class 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 )
0 commit comments