387 lines · python
1# DExTer : Debugging Experience Tester2# ~~~~~~ ~ ~~ ~ ~~3#4# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.5# See https://llvm.org/LICENSE.txt for license information.6# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception7"""Conditional Controller Class for DExTer.-"""8 9 10import os11import time12from collections import defaultdict13from itertools import chain14 15from dex.debugger.DebuggerControllers.ControllerHelpers import (16 in_source_file,17 update_step_watches,18)19from dex.debugger.DebuggerControllers.DebuggerControllerBase import (20 DebuggerControllerBase,21)22from dex.debugger.DebuggerBase import DebuggerBase23from dex.utils.Exceptions import DebuggerException24from dex.utils.Timeout import Timeout25from dex.dextIR import LocIR26 27class BreakpointRange:28 """A range of breakpoints and a set of conditions.29 30 The leading breakpoint (on line `range_from`) is always active.31 32 When the leading breakpoint is hit the trailing range should be activated33 when `expression` evaluates to any value in `values`. If there are no34 conditions (`expression` is None) then the trailing breakpoint range should35 always be activated upon hitting the leading breakpoint.36 37 Args:38 expression: None for no conditions, or a str expression to compare39 against `values`.40 41 hit_count: None for no limit, or int to set the number of times the42 leading breakpoint is triggered before it is removed.43 """44 45 def __init__(46 self,47 expression: str,48 path: str,49 range_from: int,50 range_to: int,51 values: list,52 hit_count: int,53 finish_on_remove: bool,54 is_continue: bool = False,55 function: str = None,56 addr: str = None,57 ):58 self.expression = expression59 self.path = path60 self.range_from = range_from61 self.range_to = range_to62 self.conditional_values = values63 self.max_hit_count = hit_count64 self.current_hit_count = 065 self.finish_on_remove = finish_on_remove66 self.is_continue = is_continue67 self.function = function68 self.addr = addr69 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: int90 ):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 )132 133 def has_conditions(self):134 return self.expression is not None135 136 def get_conditional_expression_list(self):137 conditional_list = []138 for value in self.conditional_values:139 # (<expression>) == (<value>)140 conditional_expression = "({}) == ({})".format(self.expression, value)141 conditional_list.append(conditional_expression)142 return conditional_list143 144 def add_hit(self):145 self.current_hit_count += 1146 147 def should_be_removed(self):148 if self.max_hit_count is None:149 return False150 return self.current_hit_count >= self.max_hit_count151 152 153class ConditionalController(DebuggerControllerBase):154 def __init__(self, context, step_collection):155 self._bp_ranges = None156 self._watches = set()157 self._step_index = 0158 self._pause_between_steps = context.options.pause_between_steps159 self._max_steps = context.options.max_steps160 # Map {id: BreakpointRange}161 self._leading_bp_handles = {}162 super(ConditionalController, self).__init__(context, step_collection)163 self._build_bp_ranges()164 165 def _build_bp_ranges(self):166 commands = self.step_collection.commands167 self._bp_ranges = []168 169 cond_controller_cmds = ["DexLimitSteps", "DexStepFunction", "DexContinue"]170 if not any(c in commands for c in cond_controller_cmds):171 raise DebuggerException(172 f"No conditional commands {cond_controller_cmds}, cannot conditionally step."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)186 if "DexFinishTest" in commands:187 for c in commands["DexFinishTest"]:188 bpr = BreakpointRange.finish_test(189 c.expression, c.path, c.on_line, c.values, c.hit_count + 1190 )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_count196 )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_count202 )203 self._bp_ranges.append(bpr)204 205 def _set_leading_bps(self):206 # Set a leading breakpoint for each BreakpointRange, building a207 # map of {leading bp id: BreakpointRange}.208 for bpr in self._bp_ranges:209 if bpr.has_conditions():210 # Add a conditional breakpoint for each condition.211 for cond_expr in bpr.get_conditional_expression_list():212 id = self.debugger.add_conditional_breakpoint(213 bpr.path, bpr.range_from, cond_expr214 )215 self._leading_bp_handles[id] = bpr216 elif bpr.function is not None:217 id = self.debugger.add_function_breakpoint(bpr.function)218 self._leading_bp_handles[id] = bpr219 else:220 # Add an unconditional breakpoint.221 id = self.debugger.add_breakpoint(bpr.path, bpr.range_from)222 self._leading_bp_handles[id] = bpr223 224 def _run_debugger_custom(self, cmdline):225 # TODO: Add conditional and unconditional breakpoint support to dbgeng.226 if self.debugger.get_name() == "dbgeng":227 raise DebuggerException(228 "DexLimitSteps commands are not supported by dbgeng"229 )230 231 self.step_collection.clear_steps()232 self._set_leading_bps()233 234 for command_obj in chain.from_iterable(self.step_collection.commands.values()):235 self._watches.update(command_obj.get_watches())236 237 self.debugger.launch(cmdline)238 time.sleep(self._pause_between_steps)239 240 exit_desired = False241 timed_out = False242 total_timeout = Timeout(self.context.options.timeout_total)243 244 step_function_backtraces: list[list[str]] = []245 self.instr_bp_ids = set()246 247 while not self.debugger.is_finished:248 breakpoint_timeout = Timeout(self.context.options.timeout_breakpoint)249 while self.debugger.is_running and not timed_out:250 # Check to see whether we've timed out while we're waiting.251 if total_timeout.timed_out():252 self.context.logger.error(253 "Debugger session has been "254 f"running for {total_timeout.elapsed}s, timeout reached!"255 )256 timed_out = True257 if breakpoint_timeout.timed_out():258 self.context.logger.error(259 f"Debugger session has not "260 f"hit a breakpoint for {breakpoint_timeout.elapsed}s, timeout "261 "reached!"262 )263 timed_out = True264 265 if timed_out or self.debugger.is_finished:266 break267 268 step_info = self.debugger.get_step_info(self._watches, self._step_index)269 backtrace = None270 if step_info.current_frame:271 backtrace = [f.function for f in step_info.frames]272 273 record_step = False274 debugger_continue = False275 bp_to_delete = []276 for bp_id in self.debugger.get_triggered_breakpoint_ids():277 try:278 # See if this is one of our leading breakpoints.279 bpr = self._leading_bp_handles[bp_id]280 record_step = True281 except KeyError:282 # This is a trailing bp. Mark it for removal.283 bp_to_delete.append(bp_id)284 if bp_id in self.instr_bp_ids:285 self.instr_bp_ids.remove(bp_id)286 else:287 record_step = True288 continue289 290 bpr.add_hit()291 if bpr.should_be_removed():292 if bpr.finish_on_remove:293 exit_desired = True294 bp_to_delete.append(bp_id)295 del self._leading_bp_handles[bp_id]296 297 if bpr.function is not None:298 if step_info.frames:299 # Add this backtrace to the stack. While the current300 # backtrace matches the top of the stack we'll step,301 # and while there's a backtrace in the stack that302 # is a subset of the current backtrace we'll step-out.303 if (304 len(step_function_backtraces) == 0305 or backtrace != step_function_backtraces[-1]306 ):307 step_function_backtraces.append(backtrace)308 309 # Add an address breakpoint so we don't fall out310 # the end of nested DexStepFunctions with a DexContinue.311 addr = self.debugger.get_pc(frame_idx=1)312 instr_id = self.debugger.add_instruction_breakpoint(addr)313 # Note the breakpoint so we don't log the source location314 # it in the trace later.315 self.instr_bp_ids.add(instr_id)316 317 elif bpr.is_continue:318 debugger_continue = True319 if bpr.range_to is not None:320 self.debugger.add_breakpoint(bpr.path, bpr.range_to)321 322 else:323 # Add a range of trailing breakpoints covering the lines324 # requested in the DexLimitSteps command. Ignore first line as325 # that's covered by the leading bp we just hit and include the326 # final line.327 for line in range(bpr.range_from + 1, bpr.range_to + 1):328 id = self.debugger.add_breakpoint(bpr.path, line)329 330 # Remove any trailing or expired leading breakpoints we just hit.331 self.debugger.delete_breakpoints(bp_to_delete)332 333 debugger_next = False334 debugger_out = False335 if not debugger_continue and step_info.current_frame and step_info.frames:336 while len(step_function_backtraces) > 0:337 match_subtrace = False # Backtrace contains a target trace.338 match_trace = False # Backtrace matches top of target stack.339 340 # The top of the step_function_backtraces stack contains a341 # backtrace that we want to step through. Check if the342 # current backtrace ("backtrace") either matches that trace343 # or otherwise contains it.344 target_backtrace = step_function_backtraces[-1]345 if len(backtrace) >= len(target_backtrace):346 match_trace = len(backtrace) == len(target_backtrace)347 # Check if backtrace contains target_backtrace, matching348 # from the end (bottom of call stack) backwards.349 match_subtrace = (350 backtrace[-len(target_backtrace) :] == target_backtrace351 )352 353 if match_trace:354 # We want to step through this function; do so and355 # log the steps in the step trace.356 debugger_next = True357 record_step = True358 break359 elif match_subtrace:360 # There's a function we care about buried in the361 # current backtrace. Step-out until we get to it.362 debugger_out = True363 break364 else:365 # Drop backtraces that are not match_subtraces of the current366 # backtrace; the functions we wanted to step through367 # there are no longer reachable.368 step_function_backtraces.pop()369 370 if record_step and step_info.current_frame:371 self._step_index += 1372 # Record the step.373 update_step_watches(374 step_info, self._watches, self.step_collection.commands375 )376 self.step_collection.new_step(self.context, step_info)377 378 if exit_desired:379 break380 elif debugger_next:381 self.debugger.step_next()382 elif debugger_out:383 self.debugger.step_out()384 else:385 self.debugger.go()386 time.sleep(self._pause_between_steps)387