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