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