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