Skip to content

Commit 3bc05d4

Browse files
authored
[Dexter] Add DAP instruction and function breakpoint handling (#152718)
Add `add_function_breakpoint` and `add_instruction_breakpoint` to DebuggerBase. Add implementations for DAP-based debuggers.
1 parent ecc3a80 commit 3bc05d4

File tree

3 files changed

+146
-29
lines changed

3 files changed

+146
-29
lines changed

cross-project-tests/debuginfo-tests/dexter/dex/debugger/DAP.py

Lines changed: 120 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@ def __init__(self, context, *args):
218218
self.file_to_bp = defaultdict(list)
219219
# { dex_breakpoint_id -> (file, line, condition) }
220220
self.bp_info = {}
221+
# { dex_breakpoint_id -> function_name }
222+
self.function_bp_info = {}
223+
# { dex_breakpoint_id -> instruction_reference }
224+
self.instruction_bp_info = {}
221225
# We don't rely on IDs returned directly from the debug adapter. Instead, we use dexter breakpoint IDs, and
222226
# maintain a two-way-mapping of dex_bp_id<->dap_bp_id. This also allows us to defer the setting of breakpoints
223227
# in the debug adapter itself until necessary.
@@ -226,6 +230,8 @@ def __init__(self, context, *args):
226230
self.dex_id_to_dap_id = {}
227231
self.dap_id_to_dex_ids = {}
228232
self.pending_breakpoints: bool = False
233+
self.pending_function_breakpoints: bool = False
234+
self.pending_instruction_breakpoints: bool = False
229235
# List of breakpoints, indexed by BP ID
230236
# Each entry has the source file (for use in referencing desired_bps), and the DA-assigned
231237
# ID for that breakpoint if it has one (if it has been removed or not yet created then it will be None).
@@ -288,6 +294,26 @@ def make_set_breakpoint_request(source: str, bps) -> dict:
288294
{"source": {"path": source}, "breakpoints": [bp.toDict() for bp in bps]},
289295
)
290296

297+
@staticmethod
298+
def make_set_function_breakpoint_request(function_names: list) -> dict:
299+
# Function breakpoints may specify conditions and hit counts, though we
300+
# don't use those here (though perhaps we should use native hit count,
301+
# rather than emulating it ConditionalController, now that we have a
302+
# shared interface (DAP)).
303+
return DAP.make_request(
304+
"setFunctionBreakpoints",
305+
{"breakpoints": [{"name": f} for f in function_names]},
306+
)
307+
308+
@staticmethod
309+
def make_set_instruction_breakpoint_request(addrs: list) -> dict:
310+
# Instruction breakpoints have additional fields we're ignoring for the
311+
# moment.
312+
return DAP.make_request(
313+
"setInstructionBreakpoints",
314+
{"breakpoints": [{"instructionReference": a} for a in addrs]},
315+
)
316+
291317
############################################################################
292318
## DAP communication & state-handling functions
293319

@@ -569,45 +595,96 @@ def clear_breakpoints(self):
569595
def _add_breakpoint(self, file, line):
570596
return self._add_conditional_breakpoint(file, line, None)
571597

598+
def add_function_breakpoint(self, name: str):
599+
if not self._debugger_state.capabilities.supportsFunctionBreakpoints:
600+
raise DebuggerException("Debugger does not support function breakpoints")
601+
new_id = self.get_next_bp_id()
602+
self.function_bp_info[new_id] = name
603+
self.pending_function_breakpoints = True
604+
return new_id
605+
606+
def add_instruction_breakpoint(self, addr: str):
607+
if not self._debugger_state.capabilities.supportsInstructionBreakpoints:
608+
raise DebuggerException("Debugger does not support instruction breakpoints")
609+
new_id = self.get_next_bp_id()
610+
self.instruction_bp_info[new_id] = addr
611+
self.pending_instruction_breakpoints = True
612+
return new_id
613+
572614
def _add_conditional_breakpoint(self, file, line, condition):
573615
new_id = self.get_next_bp_id()
574616
self.file_to_bp[file].append(new_id)
575617
self.bp_info[new_id] = (file, line, condition)
576618
self.pending_breakpoints = True
577619
return new_id
578620

621+
def _update_breakpoint_ids_after_request(self, dex_bp_ids: list, response: dict):
622+
dap_bp_ids = [bp["id"] for bp in response["body"]["breakpoints"]]
623+
if len(dex_bp_ids) != len(dap_bp_ids):
624+
self.context.logger.error(
625+
f"Sent request to set {len(dex_bp_ids)} breakpoints, but received {len(dap_bp_ids)} in response."
626+
)
627+
visited_dap_ids = set()
628+
for i, dex_bp_id in enumerate(dex_bp_ids):
629+
dap_bp_id = dap_bp_ids[i]
630+
self.dex_id_to_dap_id[dex_bp_id] = dap_bp_id
631+
# We take the mappings in the response as the canonical mapping, meaning that if the debug server has
632+
# simply *changed* the DAP ID for a breakpoint we overwrite the existing mapping rather than adding to
633+
# it, but if we receive the same DAP ID for multiple Dex IDs *then* we store a one-to-many mapping.
634+
if dap_bp_id in visited_dap_ids:
635+
self.dap_id_to_dex_ids[dap_bp_id].append(dex_bp_id)
636+
else:
637+
self.dap_id_to_dex_ids[dap_bp_id] = [dex_bp_id]
638+
visited_dap_ids.add(dap_bp_id)
639+
579640
def _flush_breakpoints(self):
580-
if not self.pending_breakpoints:
581-
return
582-
for file in self.file_to_bp.keys():
583-
desired_bps = self._get_desired_bps(file)
641+
# Normal and conditional breakpoints.
642+
if self.pending_breakpoints:
643+
self.pending_breakpoints = False
644+
for file in self.file_to_bp.keys():
645+
desired_bps = self._get_desired_bps(file)
646+
request_id = self.send_message(
647+
self.make_set_breakpoint_request(file, desired_bps)
648+
)
649+
result = self._await_response(request_id, 10)
650+
if not result["success"]:
651+
raise DebuggerException(f"could not set breakpoints for '{file}'")
652+
# The debug adapter may have chosen to merge our breakpoints. From here we need to identify such cases and
653+
# handle them so that our internal bookkeeping is correct.
654+
dex_bp_ids = self.get_current_bps(file)
655+
self._update_breakpoint_ids_after_request(dex_bp_ids, result)
656+
657+
# Function breakpoints.
658+
if self.pending_function_breakpoints:
659+
self.pending_function_breakpoints = False
660+
desired_bps = list(self.function_bp_info.values())
584661
request_id = self.send_message(
585-
self.make_set_breakpoint_request(file, desired_bps)
662+
self.make_set_function_breakpoint_request(desired_bps)
586663
)
587664
result = self._await_response(request_id, 10)
588665
if not result["success"]:
589-
raise DebuggerException(f"could not set breakpoints for '{file}'")
590-
# The debug adapter may have chosen to merge our breakpoints. From here we need to identify such cases and
591-
# handle them so that our internal bookkeeping is correct.
592-
dex_bp_ids = self.get_current_bps(file)
593-
dap_bp_ids = [bp["id"] for bp in result["body"]["breakpoints"]]
594-
if len(dex_bp_ids) != len(dap_bp_ids):
595-
self.context.logger.error(
596-
f"Sent request to set {len(dex_bp_ids)} breakpoints, but received {len(dap_bp_ids)} in response."
666+
raise DebuggerException(
667+
f"could not set function breakpoints: '{desired_bps}'"
597668
)
598-
visited_dap_ids = set()
599-
for i, dex_bp_id in enumerate(dex_bp_ids):
600-
dap_bp_id = dap_bp_ids[i]
601-
self.dex_id_to_dap_id[dex_bp_id] = dap_bp_id
602-
# We take the mappings in the response as the canonical mapping, meaning that if the debug server has
603-
# simply *changed* the DAP ID for a breakpoint we overwrite the existing mapping rather than adding to
604-
# it, but if we receive the same DAP ID for multiple Dex IDs *then* we store a one-to-many mapping.
605-
if dap_bp_id in visited_dap_ids:
606-
self.dap_id_to_dex_ids[dap_bp_id].append(dex_bp_id)
607-
else:
608-
self.dap_id_to_dex_ids[dap_bp_id] = [dex_bp_id]
609-
visited_dap_ids.add(dap_bp_id)
610-
self.pending_breakpoints = False
669+
# We expect the breakpoint order to match in request and response.
670+
dex_bp_ids = list(self.function_bp_info.keys())
671+
self._update_breakpoint_ids_after_request(dex_bp_ids, result)
672+
673+
# Address / instruction breakpoints.
674+
if self.pending_instruction_breakpoints:
675+
self.pending_instruction_breakpoints = False
676+
desired_bps = list(self.instruction_bp_info.values())
677+
request_id = self.send_message(
678+
self.make_set_instruction_breakpoint_request(desired_bps)
679+
)
680+
result = self._await_response(request_id, 10)
681+
if not result["success"]:
682+
raise DebuggerException(
683+
f"could not set instruction breakpoints: '{desired_bps}'"
684+
)
685+
# We expect the breakpoint order to match in request and response.
686+
dex_bp_ids = list(self.instruction_bp_info.keys())
687+
self._update_breakpoint_ids_after_request(dex_bp_ids, result)
611688

612689
def _confirm_triggered_breakpoint_ids(self, dex_bp_ids):
613690
"""Can be overridden for any specific implementations that need further processing from the debug server's
@@ -632,8 +709,16 @@ def get_triggered_breakpoint_ids(self):
632709
def delete_breakpoints(self, ids):
633710
per_file_deletions = defaultdict(list)
634711
for dex_bp_id in ids:
635-
source, _, _ = self.bp_info[dex_bp_id]
636-
per_file_deletions[source].append(dex_bp_id)
712+
if dex_bp_id in self.bp_info:
713+
source, _, _ = self.bp_info[dex_bp_id]
714+
per_file_deletions[source].append(dex_bp_id)
715+
elif dex_bp_id in self.function_bp_info:
716+
del self.function_bp_info[dex_bp_id]
717+
self.pending_function_breakpoints = True
718+
elif dex_bp_id in self.instruction_bp_info:
719+
del self.instruction_bp_info[dex_bp_id]
720+
self.pending_instruction_breakpoints = True
721+
637722
for file, deleted_ids in per_file_deletions.items():
638723
old_len = len(self.file_to_bp[file])
639724
self.file_to_bp[file] = [
@@ -651,7 +736,13 @@ def _get_launch_params(self, cmdline):
651736
""" "Set the debugger-specific params used in a launch request."""
652737

653738
def launch(self, cmdline):
654-
assert len(self.file_to_bp.keys()) > 0
739+
# FIXME: Should this be a warning or exception, rather than assert?
740+
assert (
741+
len(self.file_to_bp)
742+
+ len(self.function_bp_info)
743+
+ len(self.instruction_bp_info)
744+
> 0
745+
), "Expected at least one breakpoint before launching"
655746

656747
if self.context.options.target_run_args:
657748
cmdline += shlex.split(self.context.options.target_run_args)

cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,22 @@ def _add_conditional_breakpoint(self, file_, line, condition):
166166
"""Returns a unique opaque breakpoint id."""
167167
pass
168168

169+
def add_function_breakpoint(self, name):
170+
"""Returns a unique opaque breakpoint id.
171+
172+
The ID type depends on the debugger being used, but will probably be
173+
an int.
174+
"""
175+
raise NotImplementedError()
176+
177+
def add_instruction_breakpoint(self, addr):
178+
"""Returns a unique opaque breakpoint id.
179+
180+
The ID type depends on the debugger being used, but will probably be
181+
an int.
182+
"""
183+
raise NotImplementedError()
184+
169185
@abc.abstractmethod
170186
def delete_breakpoints(self, ids):
171187
"""Delete a set of breakpoints by ids.

cross-project-tests/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,16 @@ def _confirm_triggered_breakpoint_ids(self, dex_bp_ids):
528528
manually check conditions here."""
529529
confirmed_breakpoint_ids = set()
530530
for dex_bp_id in dex_bp_ids:
531+
# Function and instruction breakpoints don't use conditions.
532+
# FIXME: That's not a DAP restriction, so they could in future.
533+
if dex_bp_id not in self.bp_info:
534+
assert (
535+
dex_bp_id in self.function_bp_info
536+
or dex_bp_id in self.instruction_bp_info
537+
)
538+
confirmed_breakpoint_ids.add(dex_bp_id)
539+
continue
540+
531541
_, _, cond = self.bp_info[dex_bp_id]
532542
if cond is None:
533543
confirmed_breakpoint_ids.add(dex_bp_id)

0 commit comments

Comments
 (0)