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 _custom_init(self): 71 try: 72 self._debugger = self._interface.Debugger 73 self._debugger.HexDisplayMode = False 74 75 self._interface.MainWindow.Visible = ( 76 self.context.options.show_debugger) 77 78 self._solution = self._interface.Solution 79 self._solution.Create(self.context.working_directory.path, 80 'DexterSolution') 81 82 try: 83 self._solution.AddFromFile(self._project_file) 84 except OSError: 85 raise LoadDebuggerException( 86 'could not debug the specified executable', sys.exc_info()) 87 88 self._fn_step = self._debugger.StepInto 89 self._fn_go = self._debugger.Go 90 91 except AttributeError as e: 92 raise LoadDebuggerException(str(e), sys.exc_info()) 93 94 def _custom_exit(self): 95 if self._interface: 96 self._interface.Quit() 97 98 @property 99 def _project_file(self): 100 return self.context.options.executable 101 102 @abc.abstractproperty 103 def _dte_version(self): 104 pass 105 106 @property 107 def _location(self): 108 #TODO: Find a better way of determining path, line and column info 109 # that doesn't require reading break points. This method requires 110 # all lines to have a break point on them. 111 bp = self._debugger.BreakpointLastHit 112 return { 113 'path': getattr(bp, 'File', None), 114 'lineno': getattr(bp, 'FileLine', None), 115 'column': getattr(bp, 'FileColumn', None) 116 } 117 118 @property 119 def _mode(self): 120 return self._debugger.CurrentMode 121 122 def _load_interface(self): 123 self.com_module = _load_com_module() 124 return self.com_module.DTE(self._dte_version) 125 126 @property 127 def version(self): 128 try: 129 return self._interface.Version 130 except AttributeError: 131 return None 132 133 def clear_breakpoints(self): 134 for bp in self._debugger.Breakpoints: 135 bp.Delete() 136 self._vs_to_dex_ids.clear() 137 self._dex_id_to_vs.clear() 138 139 def _add_breakpoint(self, file_, line): 140 return self._add_conditional_breakpoint(file_, line, '') 141 142 def _get_next_id(self): 143 # "Generate" a new unique id for the breakpoint. 144 id = self._next_bp_id 145 self._next_bp_id += 1 146 return id 147 148 def _add_conditional_breakpoint(self, file_, line, condition): 149 col = 1 150 vsbp = VSBreakpoint(PurePath(file_), line, col, condition) 151 new_id = self._get_next_id() 152 153 # Do we have an exact matching breakpoint already? 154 if vsbp in self._vs_to_dex_ids: 155 self._vs_to_dex_ids[vsbp].append(new_id) 156 self._dex_id_to_vs[new_id] = vsbp 157 return new_id 158 159 # Breakpoint doesn't exist already. Add it now. 160 count_before = self._debugger.Breakpoints.Count 161 self._debugger.Breakpoints.Add('', file_, line, col, condition) 162 # Our internal representation of VS says that the breakpoint doesn't 163 # already exist so we do not expect this operation to fail here. 164 assert count_before < self._debugger.Breakpoints.Count 165 # We've added a new breakpoint, record its id. 166 self._vs_to_dex_ids[vsbp].append(new_id) 167 self._dex_id_to_vs[new_id] = vsbp 168 return new_id 169 170 def get_triggered_breakpoint_ids(self): 171 """Returns a set of opaque ids for just-triggered breakpoints. 172 """ 173 bps_hit = self._debugger.AllBreakpointsLastHit 174 bp_id_list = [] 175 # Intuitively, AllBreakpointsLastHit breakpoints are the last hit 176 # _bound_ breakpoints. A bound breakpoint's parent holds the info of 177 # the breakpoint the user requested. Our internal state tracks the user 178 # requested breakpoints so we look at the Parent of these triggered 179 # breakpoints to determine which have been hit. 180 for bp in bps_hit: 181 # All bound breakpoints should have the user-defined breakpoint as 182 # a parent. 183 assert bp.Parent 184 vsbp = VSBreakpoint(PurePath(bp.Parent.File), bp.Parent.FileLine, 185 bp.Parent.FileColumn, bp.Parent.Condition) 186 try: 187 ids = self._vs_to_dex_ids[vsbp] 188 except KeyError: 189 pass 190 else: 191 bp_id_list += ids 192 return set(bp_id_list) 193 194 def delete_breakpoint(self, id): 195 """Delete a breakpoint by id. 196 197 Raises a KeyError if no breakpoint with this id exists. 198 """ 199 vsbp = self._dex_id_to_vs[id] 200 201 # Remove our id from the associated list of dex ids. 202 self._vs_to_dex_ids[vsbp].remove(id) 203 del self._dex_id_to_vs[id] 204 205 # Bail if there are other uses of this vsbp. 206 if len(self._vs_to_dex_ids[vsbp]) > 0: 207 return 208 # Otherwise find and delete it. 209 for bp in self._debugger.Breakpoints: 210 # We're looking at the user-set breakpoints so there shouild be no 211 # Parent. 212 assert bp.Parent == None 213 this_vsbp = VSBreakpoint(PurePath(bp.File), bp.FileLine, 214 bp.FileColumn, bp.Condition) 215 if vsbp == this_vsbp: 216 bp.Delete() 217 break 218 219 def launch(self): 220 self._fn_go() 221 222 def step(self): 223 self._fn_step() 224 225 def go(self) -> ReturnCode: 226 self._fn_go() 227 return ReturnCode.OK 228 229 def set_current_stack_frame(self, idx: int = 0): 230 thread = self._debugger.CurrentThread 231 stack_frames = thread.StackFrames 232 try: 233 stack_frame = stack_frames[idx] 234 self._debugger.CurrentStackFrame = stack_frame.raw 235 except IndexError: 236 raise Error('attempted to access stack frame {} out of {}' 237 .format(idx, len(stack_frames))) 238 239 def _get_step_info(self, watches, step_index): 240 thread = self._debugger.CurrentThread 241 stackframes = thread.StackFrames 242 243 frames = [] 244 state_frames = [] 245 246 247 loc = LocIR(**self._location) 248 valid_loc_for_watch = loc.path and os.path.exists(loc.path) 249 250 for idx, sf in enumerate(stackframes): 251 frame = FrameIR( 252 function=self._sanitize_function_name(sf.FunctionName), 253 is_inlined=sf.FunctionName.startswith('[Inline Frame]'), 254 loc=LocIR(path=None, lineno=None, column=None)) 255 256 fname = frame.function or '' # pylint: disable=no-member 257 if any(name in fname for name in self.frames_below_main): 258 break 259 260 state_frame = StackFrame(function=frame.function, 261 is_inlined=frame.is_inlined, 262 watches={}) 263 264 if valid_loc_for_watch and idx == 0: 265 for watch_info in watches: 266 if watch_is_active(watch_info, loc.path, idx, loc.lineno): 267 watch_expr = watch_info.expression 268 state_frame.watches[watch_expr] = self.evaluate_expression(watch_expr, idx) 269 270 271 state_frames.append(state_frame) 272 frames.append(frame) 273 274 if frames: 275 frames[0].loc = loc 276 state_frames[0].location = SourceLocation(**self._location) 277 278 reason = StopReason.BREAKPOINT 279 if loc.path is None: # pylint: disable=no-member 280 reason = StopReason.STEP 281 282 program_state = ProgramState(frames=state_frames) 283 284 return StepIR( 285 step_index=step_index, frames=frames, stop_reason=reason, 286 program_state=program_state) 287 288 @property 289 def is_running(self): 290 return self._mode == VisualStudio.dbgRunMode 291 292 @property 293 def is_finished(self): 294 return self._mode == VisualStudio.dbgDesignMode 295 296 @property 297 def frames_below_main(self): 298 return [ 299 '[Inline Frame] invoke_main', '__scrt_common_main_seh', 300 '__tmainCRTStartup', 'mainCRTStartup' 301 ] 302 303 def evaluate_expression(self, expression, frame_idx=0) -> ValueIR: 304 if frame_idx != 0: 305 self.set_current_stack_frame(frame_idx) 306 result = self._debugger.GetExpression(expression) 307 if frame_idx != 0: 308 self.set_current_stack_frame(0) 309 value = result.Value 310 311 is_optimized_away = any(s in value for s in [ 312 'Variable is optimized away and not available', 313 'Value is not available, possibly due to optimization', 314 ]) 315 316 is_irretrievable = any(s in value for s in [ 317 '???', 318 '<Unable to read memory>', 319 ]) 320 321 # an optimized away value is still counted as being able to be 322 # evaluated. 323 could_evaluate = (result.IsValidValue or is_optimized_away 324 or is_irretrievable) 325 326 return ValueIR( 327 expression=expression, 328 value=value, 329 type_name=result.Type, 330 error_string=None, 331 is_optimized_away=is_optimized_away, 332 could_evaluate=could_evaluate, 333 is_irretrievable=is_irretrievable, 334 ) 335