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