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 Visual Studio debugger via DTE.""" 8 9import abc 10import imp 11import os 12import sys 13from pathlib import PurePath, Path 14from collections import defaultdict, namedtuple 15 16from dex.command.CommandBase import StepExpectInfo 17from dex.debugger.DebuggerBase import DebuggerBase, watch_is_active 18from dex.dextIR import FrameIR, LocIR, StepIR, StopReason, ValueIR 19from dex.dextIR import StackFrame, SourceLocation, ProgramState 20from dex.utils.Exceptions import Error, LoadDebuggerException 21from dex.utils.ReturnCode import ReturnCode 22 23 24def _load_com_module(): 25 try: 26 module_info = imp.find_module( 27 "ComInterface", [os.path.join(os.path.dirname(__file__), "windows")] 28 ) 29 return imp.load_module("ComInterface", *module_info) 30 except ImportError as e: 31 raise LoadDebuggerException(e, sys.exc_info()) 32 33 34# VSBreakpoint(path: PurePath, line: int, col: int, cond: str). This is enough 35# info to identify breakpoint equivalence in visual studio based on the 36# properties we set through dexter currently. 37VSBreakpoint = namedtuple("VSBreakpoint", "path, line, col, cond") 38 39 40class VisualStudio( 41 DebuggerBase, metaclass=abc.ABCMeta 42): # pylint: disable=abstract-method 43 # Constants for results of Debugger.CurrentMode 44 # (https://msdn.microsoft.com/en-us/library/envdte.debugger.currentmode.aspx) 45 dbgDesignMode = 1 46 dbgBreakMode = 2 47 dbgRunMode = 3 48 49 def __init__(self, *args): 50 self.com_module = None 51 self._debugger = None 52 self._solution = None 53 self._fn_step = None 54 self._fn_go = None 55 # The next available unique breakpoint id. Use self._get_next_id(). 56 self._next_bp_id = 0 57 # VisualStudio appears to common identical breakpoints. That is, if you 58 # ask for a breakpoint that already exists the Breakpoints list will 59 # not grow. DebuggerBase requires all breakpoints have a unique id, 60 # even for duplicates, so we'll need to do some bookkeeping. Map 61 # {VSBreakpoint: list(id)} where id is the unique dexter-side id for 62 # the requested breakpoint. 63 self._vs_to_dex_ids = defaultdict(list) 64 # Map {id: VSBreakpoint} where id is unique and VSBreakpoint identifies 65 # a breakpoint in Visual Studio. There may be many ids mapped to a 66 # single VSBreakpoint. Use self._vs_to_dex_ids to find (dexter) 67 # breakpoints mapped to the same visual studio breakpoint. 68 self._dex_id_to_vs = {} 69 70 super(VisualStudio, self).__init__(*args) 71 72 def _create_solution(self): 73 self._solution.Create(self.context.working_directory.path, "DexterSolution") 74 try: 75 self._solution.AddFromFile(self._project_file) 76 except OSError: 77 raise LoadDebuggerException( 78 "could not debug the specified executable", sys.exc_info() 79 ) 80 81 def _load_solution(self): 82 try: 83 self._solution.Open(self.context.options.vs_solution) 84 except: 85 raise LoadDebuggerException( 86 "could not load specified vs solution at {}".format( 87 self.context.options.vs_solution 88 ), 89 sys.exc_info(), 90 ) 91 92 def _custom_init(self): 93 try: 94 self._debugger = self._interface.Debugger 95 self._debugger.HexDisplayMode = False 96 97 self._interface.MainWindow.Visible = self.context.options.show_debugger 98 99 self._solution = self._interface.Solution 100 if self.context.options.vs_solution is None: 101 self._create_solution() 102 else: 103 self._load_solution() 104 105 self._fn_step = self._debugger.StepInto 106 self._fn_go = self._debugger.Go 107 108 except AttributeError as e: 109 raise LoadDebuggerException(str(e), sys.exc_info()) 110 111 def _custom_exit(self): 112 if self._interface: 113 self._interface.Quit() 114 115 @property 116 def _project_file(self): 117 return self.context.options.executable 118 119 @abc.abstractproperty 120 def _dte_version(self): 121 pass 122 123 @property 124 def _location(self): 125 # TODO: Find a better way of determining path, line and column info 126 # that doesn't require reading break points. This method requires 127 # all lines to have a break point on them. 128 bp = self._debugger.BreakpointLastHit 129 return { 130 "path": getattr(bp, "File", None), 131 "lineno": getattr(bp, "FileLine", None), 132 "column": getattr(bp, "FileColumn", None), 133 } 134 135 @property 136 def _mode(self): 137 return self._debugger.CurrentMode 138 139 def _load_interface(self): 140 self.com_module = _load_com_module() 141 return self.com_module.DTE(self._dte_version) 142 143 @property 144 def version(self): 145 try: 146 return self._interface.Version 147 except AttributeError: 148 return None 149 150 def clear_breakpoints(self): 151 for bp in self._debugger.Breakpoints: 152 bp.Delete() 153 self._vs_to_dex_ids.clear() 154 self._dex_id_to_vs.clear() 155 156 def _add_breakpoint(self, file_, line): 157 return self._add_conditional_breakpoint(file_, line, "") 158 159 def _get_next_id(self): 160 # "Generate" a new unique id for the breakpoint. 161 id = self._next_bp_id 162 self._next_bp_id += 1 163 return id 164 165 def _add_conditional_breakpoint(self, file_, line, condition): 166 col = 1 167 vsbp = VSBreakpoint(PurePath(file_), line, col, condition) 168 new_id = self._get_next_id() 169 170 # Do we have an exact matching breakpoint already? 171 if vsbp in self._vs_to_dex_ids: 172 self._vs_to_dex_ids[vsbp].append(new_id) 173 self._dex_id_to_vs[new_id] = vsbp 174 return new_id 175 176 # Breakpoint doesn't exist already. Add it now. 177 count_before = self._debugger.Breakpoints.Count 178 self._debugger.Breakpoints.Add("", file_, line, col, condition) 179 # Our internal representation of VS says that the breakpoint doesn't 180 # already exist so we do not expect this operation to fail here. 181 assert count_before < self._debugger.Breakpoints.Count 182 # We've added a new breakpoint, record its id. 183 self._vs_to_dex_ids[vsbp].append(new_id) 184 self._dex_id_to_vs[new_id] = vsbp 185 return new_id 186 187 def get_triggered_breakpoint_ids(self): 188 """Returns a set of opaque ids for just-triggered breakpoints.""" 189 bps_hit = self._debugger.AllBreakpointsLastHit 190 bp_id_list = [] 191 # Intuitively, AllBreakpointsLastHit breakpoints are the last hit 192 # _bound_ breakpoints. A bound breakpoint's parent holds the info of 193 # the breakpoint the user requested. Our internal state tracks the user 194 # requested breakpoints so we look at the Parent of these triggered 195 # breakpoints to determine which have been hit. 196 for bp in bps_hit: 197 # All bound breakpoints should have the user-defined breakpoint as 198 # a parent. 199 assert bp.Parent 200 vsbp = VSBreakpoint( 201 PurePath(bp.Parent.File), 202 bp.Parent.FileLine, 203 bp.Parent.FileColumn, 204 bp.Parent.Condition, 205 ) 206 try: 207 ids = self._vs_to_dex_ids[vsbp] 208 except KeyError: 209 pass 210 else: 211 bp_id_list += ids 212 return set(bp_id_list) 213 214 def delete_breakpoints(self, ids): 215 """Delete breakpoints by their ids. 216 217 Raises a KeyError if no breakpoint with this id exists. 218 """ 219 vsbp_set = set() 220 for id in ids: 221 vsbp = self._dex_id_to_vs[id] 222 223 # Remove our id from the associated list of dex ids. 224 self._vs_to_dex_ids[vsbp].remove(id) 225 del self._dex_id_to_vs[id] 226 227 # Bail if there are other uses of this vsbp. 228 if len(self._vs_to_dex_ids[vsbp]) > 0: 229 continue 230 # Otherwise find and delete it. 231 vsbp_set.add(vsbp) 232 233 vsbp_to_del_count = len(vsbp_set) 234 235 for bp in self._debugger.Breakpoints: 236 # We're looking at the user-set breakpoints so there should be no 237 # Parent. 238 assert bp.Parent == None 239 this_vsbp = VSBreakpoint( 240 PurePath(bp.File), bp.FileLine, bp.FileColumn, bp.Condition 241 ) 242 if this_vsbp in vsbp_set: 243 bp.Delete() 244 vsbp_to_del_count -= 1 245 if vsbp_to_del_count == 0: 246 break 247 if vsbp_to_del_count: 248 raise KeyError("did not find breakpoint to be deleted") 249 250 def _fetch_property(self, props, name): 251 num_props = props.Count 252 result = None 253 for x in range(1, num_props + 1): 254 item = props.Item(x) 255 if item.Name == name: 256 return item 257 assert False, "Couldn't find property {}".format(name) 258 259 def launch(self, cmdline): 260 exe_path = Path(self.context.options.executable) 261 self.context.logger.note(f"VS: Using executable: '{exe_path}'") 262 cmdline_str = " ".join(cmdline) 263 if self.context.options.target_run_args: 264 cmdline_str += f" {self.context.options.target_run_args}" 265 if cmdline_str: 266 self.context.logger.note(f"VS: Using executable args: '{cmdline_str}'") 267 268 # In a slightly baroque manner, lookup the VS project that runs when 269 # you click "run", and set its command line options to the desired 270 # command line options. 271 startup_proj_name = str( 272 self._fetch_property(self._interface.Solution.Properties, "StartupProject") 273 ) 274 project = self._fetch_property(self._interface.Solution, startup_proj_name) 275 ActiveConfiguration = self._fetch_property( 276 project.Properties, "ActiveConfiguration" 277 ).Object 278 ActiveConfiguration.DebugSettings.CommandArguments = cmdline_str 279 280 self.context.logger.note("Launching VS debugger...") 281 self._fn_go(False) 282 283 def step(self): 284 self._fn_step(False) 285 286 def go(self) -> ReturnCode: 287 self._fn_go(False) 288 return ReturnCode.OK 289 290 def set_current_stack_frame(self, idx: int = 0): 291 thread = self._debugger.CurrentThread 292 stack_frames = thread.StackFrames 293 try: 294 stack_frame = stack_frames[idx] 295 self._debugger.CurrentStackFrame = stack_frame.raw 296 except IndexError: 297 raise Error( 298 "attempted to access stack frame {} out of {}".format( 299 idx, len(stack_frames) 300 ) 301 ) 302 303 def _get_step_info(self, watches, step_index): 304 thread = self._debugger.CurrentThread 305 stackframes = thread.StackFrames 306 307 frames = [] 308 state_frames = [] 309 310 loc = LocIR(**self._location) 311 valid_loc_for_watch = loc.path and os.path.exists(loc.path) 312 313 for idx, sf in enumerate(stackframes): 314 frame = FrameIR( 315 function=self._sanitize_function_name(sf.FunctionName), 316 is_inlined=sf.FunctionName.startswith("[Inline Frame]"), 317 loc=LocIR(path=None, lineno=None, column=None), 318 ) 319 320 fname = frame.function or "" # pylint: disable=no-member 321 if any(name in fname for name in self.frames_below_main): 322 break 323 324 state_frame = StackFrame( 325 function=frame.function, is_inlined=frame.is_inlined, watches={} 326 ) 327 328 if valid_loc_for_watch and idx == 0: 329 for watch_info in watches: 330 if watch_is_active(watch_info, loc.path, idx, loc.lineno): 331 watch_expr = watch_info.expression 332 state_frame.watches[watch_expr] = self.evaluate_expression( 333 watch_expr, idx 334 ) 335 336 state_frames.append(state_frame) 337 frames.append(frame) 338 339 if frames: 340 frames[0].loc = loc 341 state_frames[0].location = SourceLocation(**self._location) 342 343 reason = StopReason.BREAKPOINT 344 if loc.path is None: # pylint: disable=no-member 345 reason = StopReason.STEP 346 347 program_state = ProgramState(frames=state_frames) 348 349 return StepIR( 350 step_index=step_index, 351 frames=frames, 352 stop_reason=reason, 353 program_state=program_state, 354 ) 355 356 @property 357 def is_running(self): 358 return self._mode == VisualStudio.dbgRunMode 359 360 @property 361 def is_finished(self): 362 return self._mode == VisualStudio.dbgDesignMode 363 364 @property 365 def frames_below_main(self): 366 return [ 367 "[Inline Frame] invoke_main", 368 "__scrt_common_main_seh", 369 "__tmainCRTStartup", 370 "mainCRTStartup", 371 ] 372 373 def evaluate_expression(self, expression, frame_idx=0) -> ValueIR: 374 if frame_idx != 0: 375 self.set_current_stack_frame(frame_idx) 376 result = self._debugger.GetExpression(expression) 377 if frame_idx != 0: 378 self.set_current_stack_frame(0) 379 value = result.Value 380 381 is_optimized_away = any( 382 s in value 383 for s in [ 384 "Variable is optimized away and not available", 385 "Value is not available, possibly due to optimization", 386 ] 387 ) 388 389 is_irretrievable = any( 390 s in value 391 for s in [ 392 "???", 393 "<Unable to read memory>", 394 ] 395 ) 396 397 # an optimized away value is still counted as being able to be 398 # evaluated. 399 could_evaluate = result.IsValidValue or is_optimized_away or is_irretrievable 400 401 return ValueIR( 402 expression=expression, 403 value=value, 404 type_name=result.Type, 405 error_string=None, 406 is_optimized_away=is_optimized_away, 407 could_evaluate=could_evaluate, 408 is_irretrievable=is_irretrievable, 409 ) 410