Skip to content

Commit 7089cfd

Browse files
committed
[Dexter] Add DAP instruction and function breakpoint handling
1 parent 2d33b79 commit 7089cfd

File tree

3 files changed

+145
-29
lines changed

3 files changed

+145
-29
lines changed

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

Lines changed: 122 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@ def __init__(self, context, *args):
185185
self.file_to_bp = defaultdict(list)
186186
# { dex_breakpoint_id -> (file, line, condition) }
187187
self.bp_info = {}
188+
# { dex_breakpoint_id -> function_name }
189+
self.function_bp_info = {}
190+
# { dex_breakpoint_id -> instruction_reference }
191+
self.instruction_bp_info = {}
188192
# We don't rely on IDs returned directly from the debug adapter. Instead, we use dexter breakpoint IDs, and
189193
# maintain a two-way-mapping of dex_bp_id<->dap_bp_id. This also allows us to defer the setting of breakpoints
190194
# in the debug adapter itself until necessary.
@@ -193,6 +197,8 @@ def __init__(self, context, *args):
193197
self.dex_id_to_dap_id = {}
194198
self.dap_id_to_dex_ids = {}
195199
self.pending_breakpoints: bool = False
200+
self.pending_function_breakpoints: bool = False
201+
self.pending_instruction_breakpoints: bool = False
196202
# List of breakpoints, indexed by BP ID
197203
# Each entry has the source file (for use in referencing desired_bps), and the DA-assigned
198204
# ID for that breakpoint if it has one (if it has been removed or not yet created then it will be None).
@@ -255,6 +261,26 @@ def make_set_breakpoint_request(source: str, bps) -> dict:
255261
{"source": {"path": source}, "breakpoints": [bp.toDict() for bp in bps]},
256262
)
257263

264+
@staticmethod
265+
def make_set_function_breakpoint_request(function_names: list[str]) -> dict:
266+
# Function breakpoints may specify conditions and hit counts, though we
267+
# don't use those here (though perhaps we should use native hit count,
268+
# rather than emulating it ConditionalController, now that we have a
269+
# shared interface (DAP)).
270+
return DAP.make_request(
271+
"setFunctionBreakpoints",
272+
{"breakpoints": [{"name": f} for f in function_names]},
273+
)
274+
275+
@staticmethod
276+
def make_set_instruction_breakpoint_request(addrs: list[str]) -> dict:
277+
# Instruction breakpoints have additional fields we're ignoring for the
278+
# moment.
279+
return DAP.make_request(
280+
"setInstructionBreakpoints",
281+
{"breakpoints": [{"instructionReference": a} for a in addrs]},
282+
)
283+
258284
############################################################################
259285
## DAP communication & state-handling functions
260286

@@ -524,45 +550,98 @@ def clear_breakpoints(self):
524550
def _add_breakpoint(self, file, line):
525551
return self._add_conditional_breakpoint(file, line, None)
526552

553+
def add_function_breakpoint(self, name: str):
554+
if not self._debugger_state.capabilities.supportsFunctionBreakpoints:
555+
raise DebuggerException("Debugger does not support function breakpoints")
556+
new_id = self.get_next_bp_id()
557+
self.function_bp_info[new_id] = name
558+
self.pending_function_breakpoints = True
559+
return new_id
560+
561+
def add_instruction_breakpoint(self, addr: str):
562+
if not self._debugger_state.capabilities.supportsInstructionBreakpoints:
563+
raise DebuggerException("Debugger does not support instruction breakpoints")
564+
new_id = self.get_next_bp_id()
565+
self.instruction_bp_info[new_id] = addr
566+
self.pending_instruction_breakpoints = True
567+
return new_id
568+
527569
def _add_conditional_breakpoint(self, file, line, condition):
528570
new_id = self.get_next_bp_id()
529571
self.file_to_bp[file].append(new_id)
530572
self.bp_info[new_id] = (file, line, condition)
531573
self.pending_breakpoints = True
532574
return new_id
533575

576+
def _update_breakpoint_ids_after_request(
577+
self, dex_bp_ids: list[int], response: dict
578+
):
579+
dap_bp_ids = [bp["id"] for bp in response["body"]["breakpoints"]]
580+
if len(dex_bp_ids) != len(dap_bp_ids):
581+
self.context.logger.error(
582+
f"Sent request to set {len(dex_bp_ids)} breakpoints, but received {len(dap_bp_ids)} in response."
583+
)
584+
visited_dap_ids = set()
585+
for i, dex_bp_id in enumerate(dex_bp_ids):
586+
dap_bp_id = dap_bp_ids[i]
587+
self.dex_id_to_dap_id[dex_bp_id] = dap_bp_id
588+
# We take the mappings in the response as the canonical mapping, meaning that if the debug server has
589+
# simply *changed* the DAP ID for a breakpoint we overwrite the existing mapping rather than adding to
590+
# it, but if we receive the same DAP ID for multiple Dex IDs *then* we store a one-to-many mapping.
591+
if dap_bp_id in visited_dap_ids:
592+
self.dap_id_to_dex_ids[dap_bp_id].append(dex_bp_id)
593+
else:
594+
self.dap_id_to_dex_ids[dap_bp_id] = [dex_bp_id]
595+
visited_dap_ids.add(dap_bp_id)
596+
534597
def _flush_breakpoints(self):
535-
if not self.pending_breakpoints:
536-
return
537-
for file in self.file_to_bp.keys():
538-
desired_bps = self._get_desired_bps(file)
598+
# Normal and conditional breakpoints.
599+
if self.pending_breakpoints:
600+
self.pending_breakpoints = False
601+
for file in self.file_to_bp.keys():
602+
desired_bps = self._get_desired_bps(file)
603+
request_id = self.send_message(
604+
self.make_set_breakpoint_request(file, desired_bps)
605+
)
606+
result = self._await_response(request_id, 10)
607+
if not result["success"]:
608+
raise DebuggerException(f"could not set breakpoints for '{file}'")
609+
# The debug adapter may have chosen to merge our breakpoints. From here we need to identify such cases and
610+
# handle them so that our internal bookkeeping is correct.
611+
dex_bp_ids = self.get_current_bps(file)
612+
self._update_breakpoint_ids_after_request(dex_bp_ids, result)
613+
614+
# Funciton breakpoints.
615+
if self.pending_function_breakpoints:
616+
self.pending_function_breakpoints = False
617+
desired_bps = list(self.function_bp_info.values())
539618
request_id = self.send_message(
540-
self.make_set_breakpoint_request(file, desired_bps)
619+
self.make_set_function_breakpoint_request(desired_bps)
541620
)
542621
result = self._await_response(request_id, 10)
543622
if not result["success"]:
544-
raise DebuggerException(f"could not set breakpoints for '{file}'")
545-
# The debug adapter may have chosen to merge our breakpoints. From here we need to identify such cases and
546-
# handle them so that our internal bookkeeping is correct.
547-
dex_bp_ids = self.get_current_bps(file)
548-
dap_bp_ids = [bp["id"] for bp in result["body"]["breakpoints"]]
549-
if len(dex_bp_ids) != len(dap_bp_ids):
550-
self.context.logger.error(
551-
f"Sent request to set {len(dex_bp_ids)} breakpoints, but received {len(dap_bp_ids)} in response."
623+
raise DebuggerException(
624+
f"could not set function breakpoints: '{desired_bps}'"
552625
)
553-
visited_dap_ids = set()
554-
for i, dex_bp_id in enumerate(dex_bp_ids):
555-
dap_bp_id = dap_bp_ids[i]
556-
self.dex_id_to_dap_id[dex_bp_id] = dap_bp_id
557-
# We take the mappings in the response as the canonical mapping, meaning that if the debug server has
558-
# simply *changed* the DAP ID for a breakpoint we overwrite the existing mapping rather than adding to
559-
# it, but if we receive the same DAP ID for multiple Dex IDs *then* we store a one-to-many mapping.
560-
if dap_bp_id in visited_dap_ids:
561-
self.dap_id_to_dex_ids[dap_bp_id].append(dex_bp_id)
562-
else:
563-
self.dap_id_to_dex_ids[dap_bp_id] = [dex_bp_id]
564-
visited_dap_ids.add(dap_bp_id)
565-
self.pending_breakpoints = False
626+
# Is this right? Are we guarenteed the order of the outgoing/incoming lists?
627+
dex_bp_ids = list(self.function_bp_info.keys())
628+
self._update_breakpoint_ids_after_request(dex_bp_ids, result)
629+
630+
# Address / instruction breakpoints.
631+
if self.pending_instruction_breakpoints:
632+
self.pending_instruction_breakpoints = False
633+
desired_bps = list(self.instruction_bp_info.values())
634+
request_id = self.send_message(
635+
self.make_set_instruction_breakpoint_request(desired_bps)
636+
)
637+
result = self._await_response(request_id, 10)
638+
if not result["success"]:
639+
raise DebuggerException(
640+
f"could not set instruction breakpoints: '{desired_bps}'"
641+
)
642+
# Is this right? Are we guarenteed the order of the outgoing/incoming lists?
643+
dex_bp_ids = list(self.instruction_bp_info.keys())
644+
self._update_breakpoint_ids_after_request(dex_bp_ids, result)
566645

567646
def _confirm_triggered_breakpoint_ids(self, dex_bp_ids):
568647
"""Can be overridden for any specific implementations that need further processing from the debug server's
@@ -587,8 +666,16 @@ def get_triggered_breakpoint_ids(self):
587666
def delete_breakpoints(self, ids):
588667
per_file_deletions = defaultdict(list)
589668
for dex_bp_id in ids:
590-
source, _, _ = self.bp_info[dex_bp_id]
591-
per_file_deletions[source].append(dex_bp_id)
669+
if dex_bp_id in self.bp_info:
670+
source, _, _ = self.bp_info[dex_bp_id]
671+
per_file_deletions[source].append(dex_bp_id)
672+
elif dex_bp_id in self.function_bp_info:
673+
del self.function_bp_info[dex_bp_id]
674+
self.pending_function_breakpoints = True
675+
elif dex_bp_id in self.instruction_bp_info:
676+
del self.instruction_bp_info[dex_bp_id]
677+
self.pending_instruction_breakpoints = True
678+
592679
for file, deleted_ids in per_file_deletions.items():
593680
old_len = len(self.file_to_bp[file])
594681
self.file_to_bp[file] = [
@@ -606,7 +693,13 @@ def _get_launch_params(self, cmdline):
606693
""" "Set the debugger-specific params used in a launch request."""
607694

608695
def launch(self, cmdline):
609-
assert len(self.file_to_bp.keys()) > 0
696+
# FIXME: This should probably not a warning, not an assert.
697+
assert (
698+
len(self.file_to_bp)
699+
+ len(self.function_bp_info)
700+
+ len(self.instruction_bp_info)
701+
> 0
702+
), "Expected at least one breakpoint before launching"
610703

611704
if self.context.options.target_run_args:
612705
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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,13 @@ 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 restruction, so they could in future.
533+
if dex_bp_id not in self.bp_info:
534+
assert dex_bp_id in self.function_bp_info or dex_bp_id in self.instruction_bp_info
535+
confirmed_breakpoint_ids.add(dex_bp_id)
536+
continue
537+
531538
_, _, cond = self.bp_info[dex_bp_id]
532539
if cond is None:
533540
confirmed_breakpoint_ids.add(dex_bp_id)

0 commit comments

Comments
 (0)