Breakpoint deletion in visual studio is currently implemented by iterating over the breakpoints we want to delete, for each of which we iterate over the complete set of breakpoints in the debugger instance until we find the one we wish to delete. Ideally we would resolve this by directly deleting each breakpoint by some ID rather than searching through the full breakpoint list for them, but in the absence of such a feature in VS we can instead invert the loop to improve performance. This patch changes breakpoint deletion to iterate over the complete list of breakpoints, deleting breakpoints that match the breakpoints we expect to delete by checking set membership. This represents a worst-case improvement from O(nm) to O(n), for 'm' breakpoints being deleted out of 'n' total. In practise this is almost exactly 'm'-times faster, as when we delete multiple breakpoints they are typically adjacent in the full breakpoint list. Differential Revision: https://reviews.llvm.org/D120658
187 lines
7.0 KiB
Python
187 lines
7.0 KiB
Python
# DExTer : Debugging Experience Tester
|
|
# ~~~~~~ ~ ~~ ~ ~~
|
|
#
|
|
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
# See https://llvm.org/LICENSE.txt for license information.
|
|
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
|
|
import sys
|
|
import os
|
|
import platform
|
|
|
|
from dex.debugger.DebuggerBase import DebuggerBase, watch_is_active
|
|
from dex.dextIR import FrameIR, LocIR, StepIR, StopReason, ValueIR
|
|
from dex.dextIR import ProgramState, StackFrame, SourceLocation
|
|
from dex.utils.Exceptions import DebuggerException, LoadDebuggerException
|
|
from dex.utils.ReturnCode import ReturnCode
|
|
|
|
if platform.system() == "Windows":
|
|
# Don't load on linux; _load_interface will croak before any names are used.
|
|
from . import setup
|
|
from . import probe_process
|
|
from . import breakpoint
|
|
|
|
class DbgEng(DebuggerBase):
|
|
def __init__(self, context, *args):
|
|
self.breakpoints = []
|
|
self.running = False
|
|
self.finished = False
|
|
self.step_info = None
|
|
super(DbgEng, self).__init__(context, *args)
|
|
|
|
def _custom_init(self):
|
|
try:
|
|
res = setup.setup_everything(self.context.options.executable)
|
|
self.client = res
|
|
self.running = True
|
|
except Exception as e:
|
|
raise Exception('Failed to start debuggee: {}'.format(e))
|
|
|
|
def _custom_exit(self):
|
|
setup.cleanup(self.client)
|
|
|
|
def _load_interface(self):
|
|
arch = platform.architecture()[0]
|
|
machine = platform.machine()
|
|
if arch == '32bit' and machine == 'AMD64':
|
|
# This python process is 32 bits, but is sitting on a 64 bit machine.
|
|
# Bad things may happen, don't support it.
|
|
raise LoadDebuggerException('Can\'t run Dexter dbgeng on 32 bit python in a 64 bit environment')
|
|
|
|
if platform.system() != 'Windows':
|
|
raise LoadDebuggerException('DbgEng supports Windows only')
|
|
|
|
# Otherwise, everything was imported earlier
|
|
|
|
@classmethod
|
|
def get_name(cls):
|
|
return 'dbgeng'
|
|
|
|
@classmethod
|
|
def get_option_name(cls):
|
|
return 'dbgeng'
|
|
|
|
@property
|
|
def frames_below_main(self):
|
|
return []
|
|
|
|
@property
|
|
def version(self):
|
|
# I don't believe there's a well defined DbgEng version, outside of the
|
|
# version of Windows being used.
|
|
return "1"
|
|
|
|
def clear_breakpoints(self):
|
|
for x in self.breakpoints:
|
|
x.RemoveFlags(breakpoint.BreakpointFlags.DEBUG_BREAKPOINT_ENABLED)
|
|
self.client.Control.RemoveBreakpoint(x)
|
|
|
|
def _add_breakpoint(self, file_, line):
|
|
# Breakpoint setting/deleting is not supported by dbgeng at this moment
|
|
# but is something that should be considered in the future.
|
|
# TODO: this method is called in the DefaultController but has no effect.
|
|
pass
|
|
|
|
def _add_conditional_breakpoint(self, file_, line, condition):
|
|
# breakpoint setting/deleting is not supported by dbgeng at this moment
|
|
# but is something that should be considered in the future.
|
|
raise NotImplementedError('add_conditional_breakpoint is not yet implemented by dbgeng')
|
|
|
|
def get_triggered_breakpoint_ids(self):
|
|
raise NotImplementedError('get_triggered_breakpoint_ids is not yet implemented by dbgeng')
|
|
|
|
def delete_breakpoints(self, ids):
|
|
# breakpoint setting/deleting is not supported by dbgeng at this moment
|
|
# but is something that should be considered in the future.
|
|
raise NotImplementedError('delete_conditional_breakpoint is not yet implemented by dbgeng')
|
|
|
|
def launch(self, cmdline):
|
|
assert len(cmdline) == 0, "Command lines unimplemented for dbgeng right now"
|
|
# We are, by this point, already launched.
|
|
self.step_info = probe_process.probe_state(self.client)
|
|
|
|
def step(self):
|
|
res = setup.step_once(self.client)
|
|
if not res:
|
|
self.finished = True
|
|
self.step_info = res
|
|
|
|
def go(self):
|
|
# FIXME: running freely doesn't seem to reliably stop when back in a
|
|
# relevant source file -- this is likely to be a problem when setting
|
|
# breakpoints. Until that's fixed, single step instead of running
|
|
# freely. This isn't very efficient, but at least makes progress.
|
|
self.step()
|
|
|
|
def _get_step_info(self, watches, step_index):
|
|
frames = self.step_info
|
|
state_frames = []
|
|
|
|
# For now assume the base function is the... function, ignoring
|
|
# inlining.
|
|
dex_frames = []
|
|
for i, x in enumerate(frames):
|
|
# XXX Might be able to get columns out through
|
|
# GetSourceEntriesByOffset, not a priority now
|
|
loc = LocIR(path=x.source_file, lineno=x.line_no, column=0)
|
|
new_frame = FrameIR(function=x.function_name, is_inlined=False, loc=loc)
|
|
dex_frames.append(new_frame)
|
|
|
|
state_frame = StackFrame(function=new_frame.function,
|
|
is_inlined=new_frame.is_inlined,
|
|
location=SourceLocation(path=x.source_file,
|
|
lineno=x.line_no,
|
|
column=0),
|
|
watches={})
|
|
for expr in map(
|
|
# Filter out watches that are not active in the current frame,
|
|
# and then evaluate all the active watches.
|
|
lambda watch_info, idx=i:
|
|
self.evaluate_expression(watch_info.expression, idx),
|
|
filter(
|
|
lambda watch_info, idx=i, line_no=loc.lineno, path=loc.path:
|
|
watch_is_active(watch_info, path, idx, line_no),
|
|
watches)):
|
|
state_frame.watches[expr.expression] = expr
|
|
state_frames.append(state_frame)
|
|
|
|
return StepIR(
|
|
step_index=step_index, frames=dex_frames,
|
|
stop_reason=StopReason.STEP,
|
|
program_state=ProgramState(state_frames))
|
|
|
|
@property
|
|
def is_running(self):
|
|
return False # We're never free-running
|
|
|
|
@property
|
|
def is_finished(self):
|
|
return self.finished
|
|
|
|
def evaluate_expression(self, expression, frame_idx=0):
|
|
# XXX: cdb insists on using '->' to examine fields of structures,
|
|
# as it appears to reserve '.' for other purposes.
|
|
fixed_expr = expression.replace('.', '->')
|
|
|
|
orig_scope_idx = self.client.Symbols.GetCurrentScopeFrameIndex()
|
|
self.client.Symbols.SetScopeFrameByIndex(frame_idx)
|
|
|
|
res = self.client.Control.Evaluate(fixed_expr)
|
|
if res is not None:
|
|
result, typename = self.client.Control.Evaluate(fixed_expr)
|
|
could_eval = True
|
|
else:
|
|
result, typename = (None, None)
|
|
could_eval = False
|
|
|
|
self.client.Symbols.SetScopeFrameByIndex(orig_scope_idx)
|
|
|
|
return ValueIR(
|
|
expression=expression,
|
|
value=str(result),
|
|
type_name=typename,
|
|
error_string="",
|
|
could_evaluate=could_eval,
|
|
is_optimized_away=False,
|
|
is_irretrievable=not could_eval)
|