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_breakpoint(self, id): 208 """Delete a breakpoint by id. 209 210 Raises a KeyError if no breakpoint with this id exists. 211 """ 212 vsbp = self._dex_id_to_vs[id] 213 214 # Remove our id from the associated list of dex ids. 215 self._vs_to_dex_ids[vsbp].remove(id) 216 del self._dex_id_to_vs[id] 217 218 # Bail if there are other uses of this vsbp. 219 if len(self._vs_to_dex_ids[vsbp]) > 0: 220 return 221 # Otherwise find and delete it. 222 for bp in self._debugger.Breakpoints: 223 # We're looking at the user-set breakpoints so there shouild be no 224 # Parent. 225 assert bp.Parent == None 226 this_vsbp = VSBreakpoint(PurePath(bp.File), bp.FileLine, 227 bp.FileColumn, bp.Condition) 228 if vsbp == this_vsbp: 229 bp.Delete() 230 break 231 232 def launch(self): 233 self._fn_go() 234 235 def step(self): 236 self._fn_step() 237 238 def go(self) -> ReturnCode: 239 self._fn_go() 240 return ReturnCode.OK 241 242 def set_current_stack_frame(self, idx: int = 0): 243 thread = self._debugger.CurrentThread 244 stack_frames = thread.StackFrames 245 try: 246 stack_frame = stack_frames[idx] 247 self._debugger.CurrentStackFrame = stack_frame.raw 248 except IndexError: 249 raise Error('attempted to access stack frame {} out of {}' 250 .format(idx, len(stack_frames))) 251 252 def _get_step_info(self, watches, step_index): 253 thread = self._debugger.CurrentThread 254 stackframes = thread.StackFrames 255 256 frames = [] 257 state_frames = [] 258 259 260 loc = LocIR(**self._location) 261 valid_loc_for_watch = loc.path and os.path.exists(loc.path) 262 263 for idx, sf in enumerate(stackframes): 264 frame = FrameIR( 265 function=self._sanitize_function_name(sf.FunctionName), 266 is_inlined=sf.FunctionName.startswith('[Inline Frame]'), 267 loc=LocIR(path=None, lineno=None, column=None)) 268 269 fname = frame.function or '' # pylint: disable=no-member 270 if any(name in fname for name in self.frames_below_main): 271 break 272 273 state_frame = StackFrame(function=frame.function, 274 is_inlined=frame.is_inlined, 275 watches={}) 276 277 if valid_loc_for_watch and idx == 0: 278 for watch_info in watches: 279 if watch_is_active(watch_info, loc.path, idx, loc.lineno): 280 watch_expr = watch_info.expression 281 state_frame.watches[watch_expr] = self.evaluate_expression(watch_expr, idx) 282 283 284 state_frames.append(state_frame) 285 frames.append(frame) 286 287 if frames: 288 frames[0].loc = loc 289 state_frames[0].location = SourceLocation(**self._location) 290 291 reason = StopReason.BREAKPOINT 292 if loc.path is None: # pylint: disable=no-member 293 reason = StopReason.STEP 294 295 program_state = ProgramState(frames=state_frames) 296 297 return StepIR( 298 step_index=step_index, frames=frames, stop_reason=reason, 299 program_state=program_state) 300 301 @property 302 def is_running(self): 303 return self._mode == VisualStudio.dbgRunMode 304 305 @property 306 def is_finished(self): 307 return self._mode == VisualStudio.dbgDesignMode 308 309 @property 310 def frames_below_main(self): 311 return [ 312 '[Inline Frame] invoke_main', '__scrt_common_main_seh', 313 '__tmainCRTStartup', 'mainCRTStartup' 314 ] 315 316 def evaluate_expression(self, expression, frame_idx=0) -> ValueIR: 317 if frame_idx != 0: 318 self.set_current_stack_frame(frame_idx) 319 result = self._debugger.GetExpression(expression) 320 if frame_idx != 0: 321 self.set_current_stack_frame(0) 322 value = result.Value 323 324 is_optimized_away = any(s in value for s in [ 325 'Variable is optimized away and not available', 326 'Value is not available, possibly due to optimization', 327 ]) 328 329 is_irretrievable = any(s in value for s in [ 330 '???', 331 '<Unable to read memory>', 332 ]) 333 334 # an optimized away value is still counted as being able to be 335 # evaluated. 336 could_evaluate = (result.IsValidValue or is_optimized_away 337 or is_irretrievable) 338 339 return ValueIR( 340 expression=expression, 341 value=value, 342 type_name=result.Type, 343 error_string=None, 344 is_optimized_away=is_optimized_away, 345 could_evaluate=could_evaluate, 346 is_irretrievable=is_irretrievable, 347 ) 348