# DExTer : Debugging Experience Tester # ~~~~~~ ~ ~~ ~ ~~ # # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. # See https://llvm.org/LICENSE.txt for license information. # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception """Conditional Controller Class for DExTer.-""" import os import time from collections import defaultdict from itertools import chain from dex.debugger.DebuggerControllers.ControllerHelpers import in_source_file, update_step_watches from dex.debugger.DebuggerControllers.DebuggerControllerBase import DebuggerControllerBase from dex.debugger.DebuggerBase import DebuggerBase from dex.utils.Exceptions import DebuggerException class BreakpointRange: """A range of breakpoints and a set of conditions. The leading breakpoint (on line `range_from`) is always active. When the leading breakpoint is hit the trailing range should be activated when `expression` evaluates to any value in `values`. If there are no conditions (`expression` is None) then the trailing breakpoint range should always be activated upon hitting the leading breakpoint. Args: expression: None for no conditions, or a str expression to compare against `values`. hit_count: None for no limit, or int to set the number of times the leading breakpoint is triggered before it is removed. """ def __init__(self, expression: str, path: str, range_from: int, range_to: int, values: list, hit_count: int): self.expression = expression self.path = path self.range_from = range_from self.range_to = range_to self.conditional_values = values self.max_hit_count = hit_count self.current_hit_count = 0 def has_conditions(self): return self.expression != None def get_conditional_expression_list(self): conditional_list = [] for value in self.conditional_values: # () == () conditional_expression = '({}) == ({})'.format(self.expression, value) conditional_list.append(conditional_expression) return conditional_list def add_hit(self): self.current_hit_count += 1 def should_be_removed(self): if self.max_hit_count == None: return False return self.current_hit_count >= self.max_hit_count class ConditionalController(DebuggerControllerBase): def __init__(self, context, step_collection): self.context = context self.step_collection = step_collection self._bp_ranges = None self._build_bp_ranges() self._watches = set() self._step_index = 0 self._pause_between_steps = context.options.pause_between_steps self._max_steps = context.options.max_steps # Map {id: BreakpointRange} self._leading_bp_handles = {} def _build_bp_ranges(self): commands = self.step_collection.commands self._bp_ranges = [] try: limit_commands = commands['DexLimitSteps'] for lc in limit_commands: bpr = BreakpointRange( lc.expression, lc.path, lc.from_line, lc.to_line, lc.values, lc.hit_count) self._bp_ranges.append(bpr) except KeyError: raise DebuggerException('Missing DexLimitSteps commands, cannot conditionally step.') def _set_leading_bps(self): # Set a leading breakpoint for each BreakpointRange, building a # map of {leading bp id: BreakpointRange}. for bpr in self._bp_ranges: if bpr.has_conditions(): # Add a conditional breakpoint for each condition. for cond_expr in bpr.get_conditional_expression_list(): id = self.debugger.add_conditional_breakpoint(bpr.path, bpr.range_from, cond_expr) self._leading_bp_handles[id] = bpr else: # Add an unconditional breakpoint. id = self.debugger.add_breakpoint(bpr.path, bpr.range_from) self._leading_bp_handles[id] = bpr def _run_debugger_custom(self): # TODO: Add conditional and unconditional breakpoint support to dbgeng. if self.debugger.get_name() == 'dbgeng': raise DebuggerException('DexLimitSteps commands are not supported by dbgeng') self.step_collection.clear_steps() self._set_leading_bps() for command_obj in chain.from_iterable(self.step_collection.commands.values()): self._watches.update(command_obj.get_watches()) self.debugger.launch() time.sleep(self._pause_between_steps) while not self.debugger.is_finished: while self.debugger.is_running: pass step_info = self.debugger.get_step_info(self._watches, self._step_index) if step_info.current_frame: self._step_index += 1 update_step_watches(step_info, self._watches, self.step_collection.commands) self.step_collection.new_step(self.context, step_info) bp_to_delete = [] for bp_id in self.debugger.get_triggered_breakpoint_ids(): try: # See if this is one of our leading breakpoints. bpr = self._leading_bp_handles[bp_id] except KeyError: # This is a trailing bp. Mark it for removal. bp_to_delete.append(bp_id) continue bpr.add_hit() if bpr.should_be_removed(): bp_to_delete.append(bp_id) del self._leading_bp_handles[bp_id] # Add a range of trailing breakpoints covering the lines # requested in the DexLimitSteps command. Ignore first line as # that's covered by the leading bp we just hit and include the # final line. for line in range(bpr.range_from + 1, bpr.range_to + 1): self.debugger.add_breakpoint(bpr.path, line) # Remove any trailing or expired leading breakpoints we just hit. for bp_id in bp_to_delete: self.debugger.delete_breakpoint(bp_id) self.debugger.go() time.sleep(self._pause_between_steps)