diff --git a/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py b/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py --- a/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py +++ b/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py @@ -125,26 +125,46 @@ pass def add_breakpoint(self, file_, line): + """Returns a unique opaque breakpoint id. + + The ID type depends on the debugger being used, but will probably be + an int. + """ return self._add_breakpoint(self._external_to_debug_path(file_), line) @abc.abstractmethod def _add_breakpoint(self, file_, line): + """Returns a unique opaque breakpoint id. + """ pass def add_conditional_breakpoint(self, file_, line, condition): + """Returns a unique opaque breakpoint id. + + The ID type depends on the debugger being used, but will probably be + an int. + """ return self._add_conditional_breakpoint( self._external_to_debug_path(file_), line, condition) @abc.abstractmethod def _add_conditional_breakpoint(self, file_, line, condition): + """Returns a unique opaque breakpoint id. + """ pass - def delete_conditional_breakpoint(self, file_, line, condition): - return self._delete_conditional_breakpoint( - self._external_to_debug_path(file_), line, condition) + @abc.abstractmethod + def delete_breakpoint(self, id): + """Delete a breakpoint by id. + + Raises a KeyError if no breakpoint with this id exists. + """ + pass @abc.abstractmethod - def _delete_conditional_breakpoint(self, file_, line, condition): + def get_triggered_breakpoint_ids(self): + """Returns a set of opaque ids for just-triggered breakpoints. + """ pass @abc.abstractmethod diff --git a/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ConditionalController.py b/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ConditionalController.py --- a/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ConditionalController.py +++ b/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ConditionalController.py @@ -42,17 +42,18 @@ def __init__(self, context, step_collection): self.context = context self.step_collection = step_collection - self._conditional_bps = None + self._conditional_bp_ranges = None + self._build_conditional_bp_ranges() self._watches = set() self._step_index = 0 - self._build_conditional_bps() - self._path_and_line_to_conditional_bp = defaultdict(list) self._pause_between_steps = context.options.pause_between_steps self._max_steps = context.options.max_steps + # Map {id: ConditionalBpRange} + self._conditional_bp_handles = {} - def _build_conditional_bps(self): + def _build_conditional_bp_ranges(self): commands = self.step_collection.commands - self._conditional_bps = [] + self._conditional_bp_ranges = [] try: limit_commands = commands['DexLimitSteps'] for lc in limit_commands: @@ -62,22 +63,19 @@ lc.from_line, lc.to_line, lc.values) - self._conditional_bps.append(conditional_bp) + self._conditional_bp_ranges.append(conditional_bp) except KeyError: raise DebuggerException('Missing DexLimitSteps commands, cannot conditionally step.') def _set_conditional_bps(self): - # When we break in the debugger we need a quick and easy way to look up - # which conditional bp we've breaked on. - for cbp in self._conditional_bps: - conditional_bp_list = self._path_and_line_to_conditional_bp[(cbp.path, cbp.range_from)] - conditional_bp_list.append(cbp) - - # Set break points only on the first line of any conditional range, we'll set - # more break points for a range when the condition is satisfied. - for cbp in self._conditional_bps: + # Set a conditional breakpoint for each ConditionalBpRange and build a + # map of {id: ConditionalBpRange}. + for cbp in self._conditional_bp_ranges: for cond_expr in cbp.get_conditional_expression_list(): - self.debugger.add_conditional_breakpoint(cbp.path, cbp.range_from, cond_expr) + id = self.debugger.add_conditional_breakpoint(cbp.path, + cbp.range_from, + cond_expr) + self._conditional_bp_handles[id] = cbp def _conditional_met(self, cbp): for cond_expr in cbp.get_conditional_expression_list(): @@ -98,7 +96,7 @@ self._watches.update(command_obj.get_watches()) self.debugger.launch() - time.sleep(self._pause_between_steps) + time.sleep(self._pause_between_steps) while not self.debugger.is_finished: while self.debugger.is_running: pass @@ -109,19 +107,28 @@ update_step_watches(step_info, self._watches, self.step_collection.commands) self.step_collection.new_step(self.context, step_info) - loc = step_info.current_location - conditional_bp_key = (loc.path, loc.lineno) - if conditional_bp_key in self._path_and_line_to_conditional_bp: + bp_to_delete = [] + for bp_id in self.debugger.get_triggered_breakpoint_ids(): + try: + # See if this is one of our conditional breakpoints. + cbp = self._conditional_bp_handles[bp_id] + except KeyError: + # This is an unconditional bp. Mark it for removal. + bp_to_delete.append(bp_id) + continue + # We have triggered a breakpoint with a condition. Check that + # the condition has been met. + if self._conditional_met(cbp): + # Add a range of unconditional breakpoints covering the + # lines requested in the DexLimitSteps command. Ignore + # first line as that's the conditional bp we just hit and + # include the final line. + for line in range(cbp.range_from + 1, cbp.range_to + 1): + self.debugger.add_breakpoint(cbp.path, line) + + # Remove any unconditional breakpoints we just hit. + for bp_id in bp_to_delete: + self.debugger.delete_breakpoint(bp_id) - conditional_bps = self._path_and_line_to_conditional_bp[conditional_bp_key] - for cbp in conditional_bps: - if self._conditional_met(cbp): - # Unconditional range should ignore first line as that's the - # conditional bp we just hit and should be inclusive of final line - for line in range(cbp.range_from + 1, cbp.range_to + 1): - self.debugger.add_conditional_breakpoint(cbp.path, line, condition='') - - # Clear any uncondtional break points at this loc. - self.debugger.delete_conditional_breakpoint(file_=loc.path, line=loc.lineno, condition='') self.debugger.go() time.sleep(self._pause_between_steps) diff --git a/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py b/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py --- a/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py +++ b/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py @@ -87,7 +87,10 @@ # but is something that should be considered in the future. raise NotImplementedError('add_conditional_breakpoint is not yet implemented by dbgeng') - def _delete_conditional_breakpoint(self, file_, line, condition): + def get_triggered_breakpoint_ids(self): + raise NotImplementedError('get_triggered_breakpoint_ids is not yet implemented by dbgeng') + + def delete_breakpoint(self, id): # 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') diff --git a/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py b/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py --- a/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py +++ b/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py @@ -104,9 +104,11 @@ self._target.DeleteAllBreakpoints() def _add_breakpoint(self, file_, line): - if not self._target.BreakpointCreateByLocation(file_, line): + bp = self._target.BreakpointCreateByLocation(file_, line) + if not bp: raise DebuggerException( 'could not add breakpoint [{}:{}]'.format(file_, line)) + return bp.GetID() def _add_conditional_breakpoint(self, file_, line, condition): bp = self._target.BreakpointCreateByLocation(file_, line) @@ -115,37 +117,24 @@ else: raise DebuggerException( 'could not add breakpoint [{}:{}]'.format(file_, line)) - - def _delete_conditional_breakpoint(self, file_, line, condition): - bp_count = self._target.GetNumBreakpoints() - bps = [self._target.GetBreakpointAtIndex(ix) for ix in range(0, bp_count)] - - for bp in bps: - bp_cond = bp.GetCondition() - bp_cond = bp_cond if bp_cond is not None else '' - - if bp_cond != condition: - continue - - # If one of the bound bp locations for this bp is bound to the same - # line in file_ above, then delete the entire parent bp and all - # bp locs. - # https://lldb.llvm.org/python_reference/lldb.SBBreakpoint-class.html - for breakpoint_location in bp: - sb_address = breakpoint_location.GetAddress() - - sb_line_entry = sb_address.GetLineEntry() - bl_line = sb_line_entry.GetLine() - - sb_file_entry = sb_line_entry.GetFileSpec() - bl_dir = sb_file_entry.GetDirectory() - bl_file_name = sb_file_entry.GetFilename() - - bl_file_path = os.path.join(bl_dir, bl_file_name) - - if bl_file_path == file_ and bl_line == line: - self._target.BreakpointDelete(bp.GetID()) - break + return bp.GetID() + + def get_triggered_breakpoint_ids(self): + # Breakpoints can only have been triggered if we've hit one. + stop_reason = self._translate_stop_reason(self._thread.GetStopReason()) + if stop_reason != StopReason.BREAKPOINT: + return [] + # Breakpoints have two data parts: Breakpoint ID, Location ID. We're + # only interested in the Breakpoint ID so we skip every other item. + return set([self._thread.GetStopReasonDataAtIndex(i) + for i in range(0, self._thread.GetStopReasonDataCount(), 2)]) + + def delete_breakpoint(self, id): + bp = self._target.FindBreakpointByID(id) + if not bp: + # The ID is not valid. + raise KeyError + self._target.BreakpointDelete(bp.GetID()) def launch(self): self._process = self._target.LaunchSimple(None, None, os.getcwd()) diff --git a/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py b/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py --- a/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py +++ b/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py @@ -10,6 +10,9 @@ import imp import os import sys +from pathlib import PurePath +from collections import namedtuple +from collections import defaultdict from dex.debugger.DebuggerBase import DebuggerBase from dex.dextIR import FrameIR, LocIR, StepIR, StopReason, ValueIR @@ -28,6 +31,11 @@ raise LoadDebuggerException(e, sys.exc_info()) +# VSBreakpoint(path: PurePath, line: int, col: int, cond: str). This is enough +# info to identify breakpoint equivalence in visual studio based on the +# properties we set through dexter currently. +VSBreakpoint = namedtuple('VSBreakpoint', 'path, line, col, cond') + class VisualStudio(DebuggerBase, metaclass=abc.ABCMeta): # pylint: disable=abstract-method # Constants for results of Debugger.CurrentMode @@ -42,6 +50,21 @@ self._solution = None self._fn_step = None self._fn_go = None + # The next available unique breakpoint id. Use self._get_next_id(). + self._next_bp_id = 0 + # VisualStudio appears to common identical breakpoints. That is, if you + # ask for a breakpoint that already exists the Breakpoints list will + # not grow. DebuggerBase requires all breakpoints have a unique id, + # even for duplicates, so we'll need to do some bookkeeping. Map + # {VSBreakpoint: list(id)} where id is the unique dexter-side id for + # the requested breakpoint. + self._vs_to_dex_ids = defaultdict(list) + # Map {id: VSBreakpoint} where id is unique and VSBreakpoint identifies + # a breakpoint in Visual Studio. There may be many ids mapped to a + # single VSBreakpoint. Use self._vs_to_dex_ids to find (dexter) + # breakpoints mapped to the same visual studio breakpoint. + self._dex_id_to_vs = {} + super(VisualStudio, self).__init__(*args) def _custom_init(self): @@ -110,21 +133,88 @@ def clear_breakpoints(self): for bp in self._debugger.Breakpoints: bp.Delete() + self._vs_to_dex_ids.clear() + self._dex_id_to_vs.clear() def _add_breakpoint(self, file_, line): - self._debugger.Breakpoints.Add('', file_, line) + return self._add_conditional_breakpoint(file_, line, '') - def _add_conditional_breakpoint(self, file_, line, condition): - column = 1 - self._debugger.Breakpoints.Add('', file_, line, column, condition) + def _get_next_id(self): + # "Generate" a new unique id for the breakpoint. + id = self._next_bp_id + self._next_bp_id += 1 + return id - def _delete_conditional_breakpoint(self, file_, line, condition): + def _add_conditional_breakpoint(self, file_, line, condition): + col = 1 + vsbp = VSBreakpoint(PurePath(file_), line, col, condition) + new_id = self._get_next_id() + + # Do we have an exact matching breakpoint already? + if vsbp in self._vs_to_dex_ids: + self._vs_to_dex_ids[vsbp].append(new_id) + self._dex_id_to_vs[new_id] = vsbp + return new_id + + # Breakpoint doesn't exist already. Add it now. + count_before = self._debugger.Breakpoints.Count + self._debugger.Breakpoints.Add('', file_, line, col, condition) + # Our internal representation of VS says that the breakpoint doesn't + # already exist so we do not expect this operation to fail here. + assert count_before < self._debugger.Breakpoints.Count + # We've added a new breakpoint, record its id. + self._vs_to_dex_ids[vsbp].append(new_id) + self._dex_id_to_vs[new_id] = vsbp + return new_id + + def get_triggered_breakpoint_ids(self): + """Returns a set of opaque ids for just-triggered breakpoints. + """ + bps_hit = self._debugger.AllBreakpointsLastHit + bp_id_list = [] + # Intuitively, AllBreakpointsLastHit breakpoints are the last hit + # _bound_ breakpoints. A bound breakpoint's parent holds the info of + # the breakpoint the user requested. Our internal state tracks the user + # requested breakpoints so we look at the Parent of these triggered + # breakpoints to determine which have been hit. + for bp in bps_hit: + # All bound breakpoints should have the user-defined breakpoint as + # a parent. + assert bp.Parent + vsbp = VSBreakpoint(PurePath(bp.Parent.File), bp.Parent.FileLine, + bp.Parent.FileColumn, bp.Parent.Condition) + try: + ids = self._vs_to_dex_ids[vsbp] + except KeyError: + pass + else: + bp_id_list += ids + return set(bp_id_list) + + def delete_breakpoint(self, id): + """Delete a breakpoint by id. + + Raises a KeyError if no breakpoint with this id exists. + """ + vsbp = self._dex_id_to_vs[id] + + # Remove our id from the associated list of dex ids. + self._vs_to_dex_ids[vsbp].remove(id) + del self._dex_id_to_vs[id] + + # Bail if there are other uses of this vsbp. + if len(self._vs_to_dex_ids[vsbp]) > 0: + return + # Otherwise find and delete it. for bp in self._debugger.Breakpoints: - for bound_bp in bp.Children: - if (bound_bp.File == file_ and bound_bp.FileLine == line and - bound_bp.Condition == condition): - bp.Delete() - break + # We're looking at the user-set breakpoints so there shouild be no + # Parent. + assert bp.Parent == None + this_vsbp = VSBreakpoint(PurePath(bp.File), bp.FileLine, + bp.FileColumn, bp.Condition) + if vsbp == this_vsbp: + bp.Delete() + break def launch(self): self._fn_go() diff --git a/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_line_mismatch.cpp b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_line_mismatch.cpp new file mode 100644 --- /dev/null +++ b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_line_mismatch.cpp @@ -0,0 +1,25 @@ +// Purpose: +// Check that \DexLimitSteps works even if the opening breakpoint line +// doesn't exist. This can happen due to optimisations or label is on an +// empty line. +// +// FIXME: Windows regression tests run with dbgeng. \DexLimitSteps isn't yet +// supported with dbgeng. +// +// REQUIRES: system-linux +// +// RUN: %dexter_regression_test -- %s | FileCheck %s +// CHECK: limit_steps_line_mismatch.cpp + +int main() { + int i = 0; + for (; i < 2; i++) { + // DexLabel('from') + int x = i; + } + int ret = 0; + return ret; // DexLabel('to') +} + +// DexLimitSteps('1', '1', from_line='from', to_line='to') +// DexExpectWatchValue('i', 0, 1, 2, from_line='from', to_line='to')