brintos

brintos / llvm-project-archived public Read only

0
0
Text · 20.9 KiB · 73b2a4d Raw
556 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"""Interface for communicating with the LLDB debugger via its python interface.8"""9 10import os11import shlex12from subprocess import CalledProcessError, check_output, STDOUT13import sys14 15from dex.debugger.DebuggerBase import DebuggerBase, watch_is_active16from dex.debugger.DAP import DAP17from dex.dextIR import FrameIR, LocIR, StepIR, StopReason, ValueIR18from dex.dextIR import StackFrame, SourceLocation, ProgramState19from dex.utils.Exceptions import DebuggerException, LoadDebuggerException20from dex.utils.ReturnCode import ReturnCode21from dex.utils.Imports import load_module22 23 24class LLDB(DebuggerBase):25    def __init__(self, context, *args):26        self.lldb_executable = context.options.lldb_executable27        self._debugger = None28        self._target = None29        self._process = None30        self._thread = None31        # Map {id (int): condition (str)} for breakpoints which have a32        # condition. See get_triggered_breakpoint_ids usage for more info.33        self._breakpoint_conditions = {}34        super(LLDB, self).__init__(context, *args)35 36    def _custom_init(self):37        self._debugger = self._interface.SBDebugger.Create()38        self._debugger.SetAsync(False)39        self._target = self._debugger.CreateTargetWithFileAndArch(40            self.context.options.executable, self.context.options.arch41        )42        if not self._target:43            raise LoadDebuggerException(44                'could not create target for executable "{}" with arch:{}'.format(45                    self.context.options.executable, self.context.options.arch46                )47            )48 49    def _custom_exit(self):50        if getattr(self, "_process", None):51            self._process.Kill()52        if getattr(self, "_debugger", None) and getattr(self, "_target", None):53            self._debugger.DeleteTarget(self._target)54 55    def _translate_stop_reason(self, reason):56        if reason == self._interface.eStopReasonNone:57            return None58        if reason == self._interface.eStopReasonBreakpoint:59            return StopReason.BREAKPOINT60        if reason == self._interface.eStopReasonPlanComplete:61            return StopReason.STEP62        if reason == self._interface.eStopReasonThreadExiting:63            return StopReason.PROGRAM_EXIT64        if reason == self._interface.eStopReasonException:65            return StopReason.ERROR66        return StopReason.OTHER67 68    def _load_interface(self):69        try:70            args = [self.lldb_executable, "-P"]71            pythonpath = check_output(args, stderr=STDOUT).rstrip().decode("utf-8")72        except CalledProcessError as e:73            raise LoadDebuggerException(str(e), sys.exc_info())74        except OSError as e:75            raise LoadDebuggerException(76                '{} ["{}"]'.format(e.strerror, self.lldb_executable), sys.exc_info()77            )78 79        if not os.path.isdir(pythonpath):80            raise LoadDebuggerException(81                'path "{}" does not exist [result of {}]'.format(pythonpath, args),82                sys.exc_info(),83            )84 85        try:86            return load_module("lldb", pythonpath)87        except ImportError as e:88            msg = str(e)89            if msg.endswith("not a valid Win32 application."):90                msg = "{} [Are you mixing 32-bit and 64-bit binaries?]".format(msg)91            raise LoadDebuggerException(msg, sys.exc_info())92 93    @classmethod94    def get_name(cls):95        return "lldb"96 97    @classmethod98    def get_option_name(cls):99        return "lldb"100 101    @property102    def version(self):103        try:104            return self._interface.SBDebugger_GetVersionString()105        except AttributeError:106            return None107 108    def clear_breakpoints(self):109        self._target.DeleteAllBreakpoints()110 111    def _add_breakpoint(self, file_, line):112        return self._add_conditional_breakpoint(file_, line, None)113 114    def _add_conditional_breakpoint(self, file_, line, condition):115        bp = self._target.BreakpointCreateByLocation(file_, line)116        if not bp:117            raise DebuggerException(118                "could not add breakpoint [{}:{}]".format(file_, line)119            )120        id = bp.GetID()121        if condition:122            bp.SetCondition(condition)123            assert id not in self._breakpoint_conditions124            self._breakpoint_conditions[id] = condition125        return id126 127    def _evaulate_breakpoint_condition(self, id):128        """Evaluate the breakpoint condition and return the result.129 130        Returns True if a conditional breakpoint with the specified id cannot131        be found (i.e. assume it is an unconditional breakpoint).132        """133        try:134            condition = self._breakpoint_conditions[id]135        except KeyError:136            # This must be an unconditional breakpoint.137            return True138        valueIR = self.evaluate_expression(condition)139        return valueIR.type_name == "bool" and valueIR.value == "true"140 141    def get_triggered_breakpoint_ids(self):142        # Breakpoints can only have been triggered if we've hit one.143        stop_reason = self._translate_stop_reason(self._thread.GetStopReason())144        if stop_reason != StopReason.BREAKPOINT:145            return []146        breakpoint_ids = set()147        # When the stop reason is eStopReasonBreakpoint, GetStopReasonDataCount148        # counts all breakpoints associated with the location that lldb has149        # stopped at, regardless of their condition. I.e. Even if we have two150        # breakpoints at the same source location that have mutually exclusive151        # conditions, both will be counted by GetStopReasonDataCount when152        # either condition is true. Check each breakpoint condition manually to153        # filter the list down to breakpoints that have caused this stop.154        #155        # Breakpoints have two data parts: Breakpoint ID, Location ID. We're156        # only interested in the Breakpoint ID so we skip every other item.157        for i in range(0, self._thread.GetStopReasonDataCount(), 2):158            id = self._thread.GetStopReasonDataAtIndex(i)159            if self._evaulate_breakpoint_condition(id):160                breakpoint_ids.add(id)161        return breakpoint_ids162 163    def delete_breakpoints(self, ids):164        for id in ids:165            bp = self._target.FindBreakpointByID(id)166            if not bp:167                # The ID is not valid.168                raise KeyError169            try:170                del self._breakpoint_conditions[id]171            except KeyError:172                # This must be an unconditional breakpoint.173                pass174            self._target.BreakpointDelete(id)175 176    def launch(self, cmdline):177        num_resolved_breakpoints = 0178        for b in self._target.breakpoint_iter():179            num_resolved_breakpoints += b.GetNumLocations() > 0180        assert num_resolved_breakpoints > 0181 182        if self.context.options.target_run_args:183            cmdline += shlex.split(self.context.options.target_run_args)184        launch_info = self._target.GetLaunchInfo()185        launch_info.SetWorkingDirectory(os.getcwd())186        launch_info.SetArguments(cmdline, True)187        error = self._interface.SBError()188        self._process = self._target.Launch(launch_info, error)189        190        if error.Fail():191            raise DebuggerException(error.GetCString())192        if not os.path.exists(self._target.executable.fullpath):193            raise DebuggerException("exe does not exist")194        if not self._process or self._process.GetNumThreads() == 0:195            raise DebuggerException("could not launch process")196        if self._process.GetNumThreads() != 1:197            raise DebuggerException("multiple threads not supported")198        self._thread = self._process.GetThreadAtIndex(0)199        200        num_stopped_threads = 0201        for thread in self._process:202            if thread.GetStopReason() == self._interface.eStopReasonBreakpoint:203                num_stopped_threads += 1204        assert num_stopped_threads > 0205        assert self._thread, (self._process, self._thread)206 207    def step_in(self):208        self._thread.StepInto()209        stop_reason = self._thread.GetStopReason()210        # If we (1) completed a step and (2) are sitting at a breakpoint,211        # but (3) the breakpoint is not reported as the stop reason, then212        # we'll need to step once more to hit the breakpoint.213        #214        # dexter sets breakpoints on every source line, then steps215        # each source line. Older lldb's would overwrite the stop216        # reason with "breakpoint hit" when we stopped at a breakpoint,217        # even if the breakpoint hadn't been exectued yet.  One218        # step per source line, hitting a breakpoint each time.219        #220        # But a more accurate behavior is that the step completes221        # with step-completed stop reason, then when we step again,222        # we execute the breakpoint and stop (with the pc the same) and223        # a breakpoint-hit stop reason.  So we need to step twice per line.224        if stop_reason == self._interface.eStopReasonPlanComplete:225            stepped_to_breakpoint = False226            pc = self._thread.GetFrameAtIndex(0).GetPC()227            for bp in self._target.breakpoints:228                for bploc in bp.locations:229                    if (230                        bploc.IsEnabled()231                        and bploc.GetAddress().GetLoadAddress(self._target) == pc232                    ):233                        stepped_to_breakpoint = True234            if stepped_to_breakpoint:235                self._process.Continue()236 237    def go(self) -> ReturnCode:238        self._process.Continue()239        return ReturnCode.OK240 241    def _get_step_info(self, watches, step_index):242        frames = []243        state_frames = []244 245        for i in range(0, self._thread.GetNumFrames()):246            sb_frame = self._thread.GetFrameAtIndex(i)247            sb_line = sb_frame.GetLineEntry()248            sb_filespec = sb_line.GetFileSpec()249 250            try:251                path = os.path.join(252                    sb_filespec.GetDirectory(), sb_filespec.GetFilename()253                )254            except (AttributeError, TypeError):255                path = None256 257            function = self._sanitize_function_name(sb_frame.GetFunctionName())258 259            loc_dict = {260                "path": path,261                "lineno": sb_line.GetLine(),262                "column": sb_line.GetColumn(),263            }264            loc = LocIR(**loc_dict)265            valid_loc_for_watch = loc.path and os.path.exists(loc.path)266 267            frame = FrameIR(function=function, is_inlined=sb_frame.IsInlined(), loc=loc)268 269            if any(270                name in (frame.function or "")  # pylint: disable=no-member271                for name in self.frames_below_main272            ):273                break274 275            frames.append(frame)276 277            state_frame = StackFrame(278                function=frame.function,279                is_inlined=frame.is_inlined,280                location=SourceLocation(**loc_dict),281                watches={},282            )283            if valid_loc_for_watch:284                for expr in map(285                    # Filter out watches that are not active in the current frame,286                    # and then evaluate all the active watches.287                    lambda watch_info, idx=i: self.evaluate_expression(288                        watch_info.expression, idx289                    ),290                    filter(291                        lambda watch_info, idx=i, line_no=loc.lineno, loc_path=loc.path: watch_is_active(292                            watch_info, loc_path, idx, line_no293                        ),294                        watches,295                    ),296                ):297                    state_frame.watches[expr.expression] = expr298            state_frames.append(state_frame)299 300        if len(frames) == 1 and frames[0].function is None:301            frames = []302            state_frames = []303 304        reason = self._translate_stop_reason(self._thread.GetStopReason())305 306        return StepIR(307            step_index=step_index,308            frames=frames,309            stop_reason=reason,310            program_state=ProgramState(state_frames),311        )312 313    @property314    def is_running(self):315        # We're not running in async mode so this is always False.316        return False317 318    @property319    def is_finished(self):320        return not self._thread.GetFrameAtIndex(0)321 322    @property323    def frames_below_main(self):324        return ["__scrt_common_main_seh", "__libc_start_main", "__libc_start_call_main"]325 326    def evaluate_expression(self, expression, frame_idx=0) -> ValueIR:327        result = self._thread.GetFrameAtIndex(frame_idx).EvaluateExpression(expression)328        error_string = str(result.error)329 330        value = result.value331        could_evaluate = not any(332            s in error_string333            for s in [334                "Can't run the expression locally",335                "use of undeclared identifier",336                "no member named",337                "Couldn't lookup symbols",338                "Couldn't look up symbols",339                "reference to local variable",340                "invalid use of 'this' outside of a non-static member function",341            ]342        )343 344        is_optimized_away = any(345            s in error_string346            for s in [347                "value may have been optimized out",348            ]349        )350 351        is_irretrievable = any(352            s in error_string353            for s in [354                "couldn't get the value of variable",355                "couldn't read its memory",356                "couldn't read from memory",357                "Cannot access memory at address",358                "invalid address (fault address:",359            ]360        )361 362        if could_evaluate and not is_irretrievable and not is_optimized_away:363            assert error_string == "success", (error_string, expression, value)364            # assert result.value is not None, (result.value, expression)365 366        if error_string == "success":367            error_string = None368 369        # attempt to find expression as a variable, if found, take the variable370        # obj's type information as it's 'usually' more accurate.371        var_result = self._thread.GetFrameAtIndex(frame_idx).FindVariable(expression)372        if str(var_result.error) == "success":373            type_name = var_result.type.GetDisplayTypeName()374        else:375            type_name = result.type.GetDisplayTypeName()376 377        return ValueIR(378            expression=expression,379            value=value,380            type_name=type_name,381            error_string=error_string,382            could_evaluate=could_evaluate,383            is_optimized_away=is_optimized_away,384            is_irretrievable=is_irretrievable,385        )386 387 388class LLDBDAP(DAP):389    def __init__(self, context, *args):390        self.lldb_dap_executable = context.options.lldb_executable391        super(LLDBDAP, self).__init__(context, *args)392 393    @classmethod394    def get_name(cls):395        return "lldb-dap"396 397    @classmethod398    def get_option_name(cls):399        return "lldb-dap"400 401    @property402    def version(self):403        return 1404 405    @property406    def _debug_adapter_name(self) -> str:407        return "lldb-dap"408 409    @property410    def _debug_adapter_executable(self) -> str:411        return self.lldb_dap_executable412 413    @property414    def frames_below_main(self):415        return [416            "__scrt_common_main_seh",417            "__libc_start_main",418            "__libc_start_call_main",419            "_start",420        ]421 422    def _post_step_hook(self):423        """Hook to be executed after completing a step request."""424        if self._debugger_state.stopped_reason == "step":425            trace_req_id = self.send_message(426                self.make_request(427                    "stackTrace", {"threadId": self._debugger_state.thread, "levels": 1}428                )429            )430            trace_response = self._await_response(trace_req_id)431            if not trace_response["success"]:432                raise DebuggerException("failed to get stack frames")433            stackframes = trace_response["body"]["stackFrames"]434            addr = stackframes[0]["instructionPointerReference"]435            try:436                path = stackframes[0]["source"]["path"]437            except KeyError:438                # We may have no path, e.g., if the module hasn't loaded or439                # there's no debug info. If that's the case we won't have440                # bound a source-location breakpoint here, so we can bail now.441                return442 443            if any(444                self._debugger_state.bp_addr_map.get(self.dex_id_to_dap_id[dex_bp_id])445                == addr446                for dex_bp_id in self.file_to_bp.get(path, [])447            ):448                # Step again now to get to the breakpoint.449                step_req_id = self.send_message(450                    self.make_request(451                        "continue", {"threadId": self._debugger_state.thread}452                    )453                )454                response = self._await_response(step_req_id)455                if not response["success"]:456                    raise DebuggerException("failed to step")457 458    def _get_launch_params(self, cmdline):459        cwd = os.getcwd()460        return {461            "cwd": cwd,462            "args": cmdline,463            "program": self.context.options.executable,464        }465 466    @staticmethod467    def _evaluate_result_value(468        expression: str, result_string: str, type_string469    ) -> ValueIR:470        could_evaluate = not any(471            s in result_string472            for s in [473                "Can't run the expression locally",474                "use of undeclared identifier",475                "no member named",476                "Couldn't lookup symbols",477                "Couldn't look up symbols",478                "reference to local variable",479                "invalid use of 'this' outside of a non-static member function",480            ]481        )482 483        is_optimized_away = any(484            s in result_string485            for s in [486                "value may have been optimized out",487            ]488        )489 490        is_irretrievable = any(491            s in result_string492            for s in [493                "couldn't get the value of variable",494                "couldn't read its memory",495                "couldn't read from memory",496                "Cannot access memory at address",497                "invalid address (fault address:",498            ]499        )500 501        if could_evaluate and not is_irretrievable and not is_optimized_away:502            error_string = None503        else:504            error_string = result_string505 506        return ValueIR(507            expression=expression,508            value=result_string,509            type_name=type_string,510            error_string=error_string,511            could_evaluate=could_evaluate,512            is_optimized_away=is_optimized_away,513            is_irretrievable=is_irretrievable,514        )515 516    def _update_requested_bp_list(self, bp_list):517        """ "As lldb-dap cannot have multiple breakpoints at the same location with different conditions, we must518        manually merge conditions here."""519        line_to_cond = {}520        for bp in bp_list:521            if bp.condition is None:522                line_to_cond[bp.line] = None523                continue524            # If we have a condition, we merge it with the existing condition if one exists, unless the known condition525            # is None in which case we preserve the None condition (as the underlying breakpoint should always be hit).526            if bp.line not in line_to_cond:527                line_to_cond[bp.line] = f"({bp.condition})"528            elif line_to_cond[bp.line] is not None:529                line_to_cond[bp.line] = f"{line_to_cond[bp.line]} || ({bp.condition})"530            bp.condition = line_to_cond[bp.line]531        return bp_list532 533    def _confirm_triggered_breakpoint_ids(self, dex_bp_ids):534        """ "As lldb returns every breakpoint at the current PC regardless of whether their condition was met, we must535        manually check conditions here."""536        confirmed_breakpoint_ids = set()537        for dex_bp_id in dex_bp_ids:538            # Function and instruction breakpoints don't use conditions.539            # FIXME: That's not a DAP restriction, so they could in future.540            if dex_bp_id not in self.bp_info:541                assert (542                    dex_bp_id in self.function_bp_info543                    or dex_bp_id in self.instruction_bp_info544                )545                confirmed_breakpoint_ids.add(dex_bp_id)546                continue547 548            _, _, cond = self.bp_info[dex_bp_id]549            if cond is None:550                confirmed_breakpoint_ids.add(dex_bp_id)551                continue552            valueIR = self.evaluate_expression(cond)553            if valueIR.type_name == "bool" and valueIR.value == "true":554                confirmed_breakpoint_ids.add(dex_bp_id)555        return confirmed_breakpoint_ids556