xref: /llvm-project/cross-project-tests/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py (revision 6779376ee917279b16e256839d236cfdf8fd9ab9)
1# DExTer : Debugging Experience Tester
2# ~~~~~~   ~         ~~         ~   ~~
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-exception
7"""Interface for communicating with the LLDB debugger via its python interface.
8"""
9
10import os
11import shlex
12from subprocess import CalledProcessError, check_output, STDOUT
13import sys
14
15from dex.debugger.DebuggerBase import DebuggerBase, watch_is_active
16from dex.dextIR import FrameIR, LocIR, StepIR, StopReason, ValueIR
17from dex.dextIR import StackFrame, SourceLocation, ProgramState
18from dex.utils.Exceptions import DebuggerException, LoadDebuggerException
19from dex.utils.ReturnCode import ReturnCode
20from dex.utils.Imports import load_module
21
22
23class LLDB(DebuggerBase):
24    def __init__(self, context, *args):
25        self.lldb_executable = context.options.lldb_executable
26        self._debugger = None
27        self._target = None
28        self._process = None
29        self._thread = None
30        # Map {id (int): condition (str)} for breakpoints which have a
31        # condition. See get_triggered_breakpoint_ids usage for more info.
32        self._breakpoint_conditions = {}
33        super(LLDB, self).__init__(context, *args)
34
35    def _custom_init(self):
36        self._debugger = self._interface.SBDebugger.Create()
37        self._debugger.SetAsync(False)
38        self._target = self._debugger.CreateTargetWithFileAndArch(
39            self.context.options.executable, self.context.options.arch
40        )
41        if not self._target:
42            raise LoadDebuggerException(
43                'could not create target for executable "{}" with arch:{}'.format(
44                    self.context.options.executable, self.context.options.arch
45                )
46            )
47
48    def _custom_exit(self):
49        if getattr(self, "_process", None):
50            self._process.Kill()
51        if getattr(self, "_debugger", None) and getattr(self, "_target", None):
52            self._debugger.DeleteTarget(self._target)
53
54    def _translate_stop_reason(self, reason):
55        if reason == self._interface.eStopReasonNone:
56            return None
57        if reason == self._interface.eStopReasonBreakpoint:
58            return StopReason.BREAKPOINT
59        if reason == self._interface.eStopReasonPlanComplete:
60            return StopReason.STEP
61        if reason == self._interface.eStopReasonThreadExiting:
62            return StopReason.PROGRAM_EXIT
63        if reason == self._interface.eStopReasonException:
64            return StopReason.ERROR
65        return StopReason.OTHER
66
67    def _load_interface(self):
68        try:
69            args = [self.lldb_executable, "-P"]
70            pythonpath = check_output(args, stderr=STDOUT).rstrip().decode("utf-8")
71        except CalledProcessError as e:
72            raise LoadDebuggerException(str(e), sys.exc_info())
73        except OSError as e:
74            raise LoadDebuggerException(
75                '{} ["{}"]'.format(e.strerror, self.lldb_executable), sys.exc_info()
76            )
77
78        if not os.path.isdir(pythonpath):
79            raise LoadDebuggerException(
80                'path "{}" does not exist [result of {}]'.format(pythonpath, args),
81                sys.exc_info(),
82            )
83
84        try:
85            return load_module("lldb", pythonpath)
86        except ImportError as e:
87            msg = str(e)
88            if msg.endswith("not a valid Win32 application."):
89                msg = "{} [Are you mixing 32-bit and 64-bit binaries?]".format(msg)
90            raise LoadDebuggerException(msg, sys.exc_info())
91
92    @classmethod
93    def get_name(cls):
94        return "lldb"
95
96    @classmethod
97    def get_option_name(cls):
98        return "lldb"
99
100    @property
101    def version(self):
102        try:
103            return self._interface.SBDebugger_GetVersionString()
104        except AttributeError:
105            return None
106
107    def clear_breakpoints(self):
108        self._target.DeleteAllBreakpoints()
109
110    def _add_breakpoint(self, file_, line):
111        return self._add_conditional_breakpoint(file_, line, None)
112
113    def _add_conditional_breakpoint(self, file_, line, condition):
114        bp = self._target.BreakpointCreateByLocation(file_, line)
115        if not bp:
116            raise DebuggerException(
117                "could not add breakpoint [{}:{}]".format(file_, line)
118            )
119        id = bp.GetID()
120        if condition:
121            bp.SetCondition(condition)
122            assert id not in self._breakpoint_conditions
123            self._breakpoint_conditions[id] = condition
124        return id
125
126    def _evaulate_breakpoint_condition(self, id):
127        """Evaluate the breakpoint condition and return the result.
128
129        Returns True if a conditional breakpoint with the specified id cannot
130        be found (i.e. assume it is an unconditional breakpoint).
131        """
132        try:
133            condition = self._breakpoint_conditions[id]
134        except KeyError:
135            # This must be an unconditional breakpoint.
136            return True
137        valueIR = self.evaluate_expression(condition)
138        return valueIR.type_name == "bool" and valueIR.value == "true"
139
140    def get_triggered_breakpoint_ids(self):
141        # Breakpoints can only have been triggered if we've hit one.
142        stop_reason = self._translate_stop_reason(self._thread.GetStopReason())
143        if stop_reason != StopReason.BREAKPOINT:
144            return []
145        breakpoint_ids = set()
146        # When the stop reason is eStopReasonBreakpoint, GetStopReasonDataCount
147        # counts all breakpoints associated with the location that lldb has
148        # stopped at, regardless of their condition. I.e. Even if we have two
149        # breakpoints at the same source location that have mutually exclusive
150        # conditions, both will be counted by GetStopReasonDataCount when
151        # either condition is true. Check each breakpoint condition manually to
152        # filter the list down to breakpoints that have caused this stop.
153        #
154        # Breakpoints have two data parts: Breakpoint ID, Location ID. We're
155        # only interested in the Breakpoint ID so we skip every other item.
156        for i in range(0, self._thread.GetStopReasonDataCount(), 2):
157            id = self._thread.GetStopReasonDataAtIndex(i)
158            if self._evaulate_breakpoint_condition(id):
159                breakpoint_ids.add(id)
160        return breakpoint_ids
161
162    def delete_breakpoints(self, ids):
163        for id in ids:
164            bp = self._target.FindBreakpointByID(id)
165            if not bp:
166                # The ID is not valid.
167                raise KeyError
168            try:
169                del self._breakpoint_conditions[id]
170            except KeyError:
171                # This must be an unconditional breakpoint.
172                pass
173            self._target.BreakpointDelete(id)
174
175    def launch(self, cmdline):
176        num_resolved_breakpoints = 0
177        for b in self._target.breakpoint_iter():
178            num_resolved_breakpoints += b.GetNumLocations() > 0
179        assert num_resolved_breakpoints > 0
180
181        if self.context.options.target_run_args:
182            cmdline += shlex.split(self.context.options.target_run_args)
183        launch_info = self._target.GetLaunchInfo()
184        launch_info.SetWorkingDirectory(os.getcwd())
185        launch_info.SetArguments(cmdline, True)
186        error = self._interface.SBError()
187        self._process = self._target.Launch(launch_info, error)
188
189        if error.Fail():
190            raise DebuggerException(error.GetCString())
191        if not os.path.exists(self._target.executable.fullpath):
192            raise DebuggerException("exe does not exist")
193        if not self._process or self._process.GetNumThreads() == 0:
194            raise DebuggerException("could not launch process")
195        if self._process.GetNumThreads() != 1:
196            raise DebuggerException("multiple threads not supported")
197        self._thread = self._process.GetThreadAtIndex(0)
198
199        num_stopped_threads = 0
200        for thread in self._process:
201            if thread.GetStopReason() == self._interface.eStopReasonBreakpoint:
202                num_stopped_threads += 1
203        assert num_stopped_threads > 0
204        assert self._thread, (self._process, self._thread)
205
206    def step(self):
207        self._thread.StepInto()
208        stop_reason = self._thread.GetStopReason()
209        # If we (1) completed a step and (2) are sitting at a breakpoint,
210        # but (3) the breakpoint is not reported as the stop reason, then
211        # we'll need to step once more to hit the breakpoint.
212        #
213        # dexter sets breakpoints on every source line, then steps
214        # each source line. Older lldb's would overwrite the stop
215        # reason with "breakpoint hit" when we stopped at a breakpoint,
216        # even if the breakpoint hadn't been exectued yet.  One
217        # step per source line, hitting a breakpoint each time.
218        #
219        # But a more accurate behavior is that the step completes
220        # with step-completed stop reason, then when we step again,
221        # we execute the breakpoint and stop (with the pc the same) and
222        # a breakpoint-hit stop reason.  So we need to step twice per line.
223        if stop_reason == self._interface.eStopReasonPlanComplete:
224            stepped_to_breakpoint = False
225            pc = self._thread.GetFrameAtIndex(0).GetPC()
226            for bp in self._target.breakpoints:
227                for bploc in bp.locations:
228                    if (
229                        bploc.IsEnabled()
230                        and bploc.GetAddress().GetLoadAddress(self._target) == pc
231                    ):
232                        stepped_to_breakpoint = True
233            if stepped_to_breakpoint:
234                self._thread.StepInto()
235
236    def go(self) -> ReturnCode:
237        self._process.Continue()
238        return ReturnCode.OK
239
240    def _get_step_info(self, watches, step_index):
241        frames = []
242        state_frames = []
243
244        for i in range(0, self._thread.GetNumFrames()):
245            sb_frame = self._thread.GetFrameAtIndex(i)
246            sb_line = sb_frame.GetLineEntry()
247            sb_filespec = sb_line.GetFileSpec()
248
249            try:
250                path = os.path.join(
251                    sb_filespec.GetDirectory(), sb_filespec.GetFilename()
252                )
253            except (AttributeError, TypeError):
254                path = None
255
256            function = self._sanitize_function_name(sb_frame.GetFunctionName())
257
258            loc_dict = {
259                "path": path,
260                "lineno": sb_line.GetLine(),
261                "column": sb_line.GetColumn(),
262            }
263            loc = LocIR(**loc_dict)
264            valid_loc_for_watch = loc.path and os.path.exists(loc.path)
265
266            frame = FrameIR(function=function, is_inlined=sb_frame.IsInlined(), loc=loc)
267
268            if any(
269                name in (frame.function or "")  # pylint: disable=no-member
270                for name in self.frames_below_main
271            ):
272                break
273
274            frames.append(frame)
275
276            state_frame = StackFrame(
277                function=frame.function,
278                is_inlined=frame.is_inlined,
279                location=SourceLocation(**loc_dict),
280                watches={},
281            )
282            if valid_loc_for_watch:
283                for expr in map(
284                    # Filter out watches that are not active in the current frame,
285                    # and then evaluate all the active watches.
286                    lambda watch_info, idx=i: self.evaluate_expression(
287                        watch_info.expression, idx
288                    ),
289                    filter(
290                        lambda watch_info, idx=i, line_no=loc.lineno, loc_path=loc.path: watch_is_active(
291                            watch_info, loc_path, idx, line_no
292                        ),
293                        watches,
294                    ),
295                ):
296                    state_frame.watches[expr.expression] = expr
297            state_frames.append(state_frame)
298
299        if len(frames) == 1 and frames[0].function is None:
300            frames = []
301            state_frames = []
302
303        reason = self._translate_stop_reason(self._thread.GetStopReason())
304
305        return StepIR(
306            step_index=step_index,
307            frames=frames,
308            stop_reason=reason,
309            program_state=ProgramState(state_frames),
310        )
311
312    @property
313    def is_running(self):
314        # We're not running in async mode so this is always False.
315        return False
316
317    @property
318    def is_finished(self):
319        return not self._thread.GetFrameAtIndex(0)
320
321    @property
322    def frames_below_main(self):
323        return ["__scrt_common_main_seh", "__libc_start_main", "__libc_start_call_main"]
324
325    def evaluate_expression(self, expression, frame_idx=0) -> ValueIR:
326        result = self._thread.GetFrameAtIndex(frame_idx).EvaluateExpression(expression)
327        error_string = str(result.error)
328
329        value = result.value
330        could_evaluate = not any(
331            s in error_string
332            for s in [
333                "Can't run the expression locally",
334                "use of undeclared identifier",
335                "no member named",
336                "Couldn't lookup symbols",
337                "Couldn't look up symbols",
338                "reference to local variable",
339                "invalid use of 'this' outside of a non-static member function",
340            ]
341        )
342
343        is_optimized_away = any(
344            s in error_string
345            for s in [
346                "value may have been optimized out",
347            ]
348        )
349
350        is_irretrievable = any(
351            s in error_string
352            for s in [
353                "couldn't get the value of variable",
354                "couldn't read its memory",
355                "couldn't read from memory",
356                "Cannot access memory at address",
357                "invalid address (fault address:",
358            ]
359        )
360
361        if could_evaluate and not is_irretrievable and not is_optimized_away:
362            assert error_string == "success", (error_string, expression, value)
363            # assert result.value is not None, (result.value, expression)
364
365        if error_string == "success":
366            error_string = None
367
368        # attempt to find expression as a variable, if found, take the variable
369        # obj's type information as it's 'usually' more accurate.
370        var_result = self._thread.GetFrameAtIndex(frame_idx).FindVariable(expression)
371        if str(var_result.error) == "success":
372            type_name = var_result.type.GetDisplayTypeName()
373        else:
374            type_name = result.type.GetDisplayTypeName()
375
376        return ValueIR(
377            expression=expression,
378            value=value,
379            type_name=type_name,
380            error_string=error_string,
381            could_evaluate=could_evaluate,
382            is_optimized_away=is_optimized_away,
383            is_irretrievable=is_irretrievable,
384        )
385