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 namedtuple 15from collections import defaultdict 16 17from dex.debugger.DebuggerBase import DebuggerBase 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 for idx, sf in enumerate(stackframes): 248 frame = FrameIR( 249 function=self._sanitize_function_name(sf.FunctionName), 250 is_inlined=sf.FunctionName.startswith('[Inline Frame]'), 251 loc=LocIR(path=None, lineno=None, column=None)) 252 253 fname = frame.function or '' # pylint: disable=no-member 254 if any(name in fname for name in self.frames_below_main): 255 break 256 257 258 state_frame = StackFrame(function=frame.function, 259 is_inlined=frame.is_inlined, 260 watches={}) 261 262 for watch in watches: 263 state_frame.watches[watch] = self.evaluate_expression( 264 watch, idx) 265 266 267 state_frames.append(state_frame) 268 frames.append(frame) 269 270 loc = LocIR(**self._location) 271 if frames: 272 frames[0].loc = loc 273 state_frames[0].location = SourceLocation(**self._location) 274 275 reason = StopReason.BREAKPOINT 276 if loc.path is None: # pylint: disable=no-member 277 reason = StopReason.STEP 278 279 program_state = ProgramState(frames=state_frames) 280 281 return StepIR( 282 step_index=step_index, frames=frames, stop_reason=reason, 283 program_state=program_state) 284 285 @property 286 def is_running(self): 287 return self._mode == VisualStudio.dbgRunMode 288 289 @property 290 def is_finished(self): 291 return self._mode == VisualStudio.dbgDesignMode 292 293 @property 294 def frames_below_main(self): 295 return [ 296 '[Inline Frame] invoke_main', '__scrt_common_main_seh', 297 '__tmainCRTStartup', 'mainCRTStartup' 298 ] 299 300 def evaluate_expression(self, expression, frame_idx=0) -> ValueIR: 301 self.set_current_stack_frame(frame_idx) 302 result = self._debugger.GetExpression(expression) 303 self.set_current_stack_frame(0) 304 value = result.Value 305 306 is_optimized_away = any(s in value for s in [ 307 'Variable is optimized away and not available', 308 'Value is not available, possibly due to optimization', 309 ]) 310 311 is_irretrievable = any(s in value for s in [ 312 '???', 313 '<Unable to read memory>', 314 ]) 315 316 # an optimized away value is still counted as being able to be 317 # evaluated. 318 could_evaluate = (result.IsValidValue or is_optimized_away 319 or is_irretrievable) 320 321 return ValueIR( 322 expression=expression, 323 value=value, 324 type_name=result.Type, 325 error_string=None, 326 is_optimized_away=is_optimized_away, 327 could_evaluate=could_evaluate, 328 is_irretrievable=is_irretrievable, 329 ) 330