brintos

brintos / llvm-project-archived public Read only

0
0
Text · 14.5 KiB · f84c4fd Raw
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