Skip to content

Commit f537f45

Browse files
committed
[Dexter] Add DAP instruction and function breakpoint handling
1 parent 73804d1 commit f537f45

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
@@ -219,6 +219,10 @@ def __init__(self, context, *args):
219219
self.file_to_bp: dict[str, list[int]] = defaultdict(list)
220220
# { dex_breakpoint_id -> (file, line, condition) }
221221
self.bp_info: dict[int, (str, int, str)] = {}
222+
# { dex_breakpoint_id -> function_name }
223+
self.function_bp_info: dict[int, str] = {}
224+
# { dex_breakpoint_id -> instruction_reference }
225+
self.instruction_bp_info: dict[int, str] = {}
222226
# We don't rely on IDs returned directly from the debug adapter. Instead, we use dexter breakpoint IDs, and
223227
# maintain a two-way-mapping of dex_bp_id<->dap_bp_id. This also allows us to defer the setting of breakpoints
224228
# in the debug adapter itself until necessary.
@@ -227,6 +231,8 @@ def __init__(self, context, *args):
227231
self.dex_id_to_dap_id: dict[int, int] = {}
228232
self.dap_id_to_dex_ids: dict[int, list[int]] = {}
229233
self.pending_breakpoints: bool = False
234+
self.pending_function_breakpoints: bool = False
235+
self.pending_instruction_breakpoints: bool = False
230236
# List of breakpoints, indexed by BP ID
231237
# Each entry has the source file (for use in referencing desired_bps), and the DA-assigned
232238
# ID for that breakpoint if it has one (if it has been removed or not yet created then it will be None).
@@ -289,6 +295,26 @@ def make_set_breakpoint_request(source: str, bps: list[BreakpointRequest]) -> di
289295
{"source": {"path": source}, "breakpoints": [bp.toDict() for bp in bps]},
290296
)
291297

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

@@ -575,45 +601,98 @@ def clear_breakpoints(self):
575601
def _add_breakpoint(self, file, line):
576602
return self._add_conditional_breakpoint(file, line, None)
577603

604+
def add_function_breakpoint(self, name: str):
605+
if not self._debugger_state.capabilities.supportsFunctionBreakpoints:
606+
raise DebuggerException("Debugger does not support function breakpoints")
607+
new_id = self.get_next_bp_id()
608+
self.function_bp_info[new_id] = name
609+
self.pending_function_breakpoints = True
610+
return new_id
611+
612+
def add_instruction_breakpoint(self, addr: str):
613+
if not self._debugger_state.capabilities.supportsInstructionBreakpoints:
614+
raise DebuggerException("Debugger does not support instruction breakpoints")
615+
new_id = self.get_next_bp_id()
616+
self.instruction_bp_info[new_id] = addr
617+
self.pending_instruction_breakpoints = True
618+
return new_id
619+
578620
def _add_conditional_breakpoint(self, file, line, condition):
579621
new_id = self.get_next_bp_id()
580622
self.file_to_bp[file].append(new_id)
581623
self.bp_info[new_id] = (file, line, condition)
582624
self.pending_breakpoints = True
583625
return new_id
584626

627+
def _update_breakpoint_ids_after_request(
628+
self, dex_bp_ids: list[int], response: dict
629+
):
630+
dap_bp_ids = [bp["id"] for bp in response["body"]["breakpoints"]]
631+
if len(dex_bp_ids) != len(dap_bp_ids):
632+
self.context.logger.error(
633+
f"Sent request to set {len(dex_bp_ids)} breakpoints, but received {len(dap_bp_ids)} in response."
634+
)
635+
visited_dap_ids = set()
636+
for i, dex_bp_id in enumerate(dex_bp_ids):
637+
dap_bp_id = dap_bp_ids[i]
638+
self.dex_id_to_dap_id[dex_bp_id] = dap_bp_id
639+
# We take the mappings in the response as the canonical mapping, meaning that if the debug server has
640+
# simply *changed* the DAP ID for a breakpoint we overwrite the existing mapping rather than adding to
641+
# it, but if we receive the same DAP ID for multiple Dex IDs *then* we store a one-to-many mapping.
642+
if dap_bp_id in visited_dap_ids:
643+
self.dap_id_to_dex_ids[dap_bp_id].append(dex_bp_id)
644+
else:
645+
self.dap_id_to_dex_ids[dap_bp_id] = [dex_bp_id]
646+
visited_dap_ids.add(dap_bp_id)
647+
585648
def _flush_breakpoints(self):
586-
if not self.pending_breakpoints:
587-
return
588-
for file in self.file_to_bp.keys():
589-
desired_bps = self._get_desired_bps(file)
649+
# Normal and conditional breakpoints.
650+
if self.pending_breakpoints:
651+
self.pending_breakpoints = False
652+
for file in self.file_to_bp.keys():
653+
desired_bps = self._get_desired_bps(file)
654+
request_id = self.send_message(
655+
self.make_set_breakpoint_request(file, desired_bps)
656+
)
657+
result = self._await_response(request_id, 10)
658+
if not result["success"]:
659+
raise DebuggerException(f"could not set breakpoints for '{file}'")
660+
# The debug adapter may have chosen to merge our breakpoints. From here we need to identify such cases and
661+
# handle them so that our internal bookkeeping is correct.
662+
dex_bp_ids = self.get_current_bps(file)
663+
self._update_breakpoint_ids_after_request(dex_bp_ids, result)
664+
665+
# Funciton breakpoints.
666+
if self.pending_function_breakpoints:
667+
self.pending_function_breakpoints = False
668+
desired_bps = list(self.function_bp_info.values())
590669
request_id = self.send_message(
591-
self.make_set_breakpoint_request(file, desired_bps)
670+
self.make_set_function_breakpoint_request(desired_bps)
592671
)
593672
result = self._await_response(request_id, 10)
594673
if not result["success"]:
595-
raise DebuggerException(f"could not set breakpoints for '{file}'")
596-
# The debug adapter may have chosen to merge our breakpoints. From here we need to identify such cases and
597-
# handle them so that our internal bookkeeping is correct.
598-
dex_bp_ids = self.get_current_bps(file)
599-
dap_bp_ids = [bp["id"] for bp in result["body"]["breakpoints"]]
600-
if len(dex_bp_ids) != len(dap_bp_ids):
601-
self.context.logger.error(
602-
f"Sent request to set {len(dex_bp_ids)} breakpoints, but received {len(dap_bp_ids)} in response."
674+
raise DebuggerException(
675+
f"could not set function breakpoints: '{desired_bps}'"
603676
)
604-
visited_dap_ids = set()
605-
for i, dex_bp_id in enumerate(dex_bp_ids):
606-
dap_bp_id = dap_bp_ids[i]
607-
self.dex_id_to_dap_id[dex_bp_id] = dap_bp_id
608-
# We take the mappings in the response as the canonical mapping, meaning that if the debug server has
609-
# simply *changed* the DAP ID for a breakpoint we overwrite the existing mapping rather than adding to
610-
# it, but if we receive the same DAP ID for multiple Dex IDs *then* we store a one-to-many mapping.
611-
if dap_bp_id in visited_dap_ids:
612-
self.dap_id_to_dex_ids[dap_bp_id].append(dex_bp_id)
613-
else:
614-
self.dap_id_to_dex_ids[dap_bp_id] = [dex_bp_id]
615-
visited_dap_ids.add(dap_bp_id)
616-
self.pending_breakpoints = False
677+
# Is this right? Are we guarenteed the order of the outgoing/incoming lists?
678+
dex_bp_ids = list(self.function_bp_info.keys())
679+
self._update_breakpoint_ids_after_request(dex_bp_ids, result)
680+
681+
# Address / instruction breakpoints.
682+
if self.pending_instruction_breakpoints:
683+
self.pending_instruction_breakpoints = False
684+
desired_bps = list(self.instruction_bp_info.values())
685+
request_id = self.send_message(
686+
self.make_set_instruction_breakpoint_request(desired_bps)
687+
)
688+
result = self._await_response(request_id, 10)
689+
if not result["success"]:
690+
raise DebuggerException(
691+
f"could not set instruction breakpoints: '{desired_bps}'"
692+
)
693+
# Is this right? Are we guarenteed the order of the outgoing/incoming lists?
694+
dex_bp_ids = list(self.instruction_bp_info.keys())
695+
self._update_breakpoint_ids_after_request(dex_bp_ids, result)
617696

618697
def _confirm_triggered_breakpoint_ids(self, dex_bp_ids):
619698
"""Can be overridden for any specific implementations that need further processing from the debug server's
@@ -638,8 +717,16 @@ def get_triggered_breakpoint_ids(self):
638717
def delete_breakpoints(self, ids):
639718
per_file_deletions: dict[str, list[int]] = defaultdict(list)
640719
for dex_bp_id in ids:
641-
source, _, _ = self.bp_info[dex_bp_id]
642-
per_file_deletions[source].append(dex_bp_id)
720+
if dex_bp_id in self.bp_info:
721+
source, _, _ = self.bp_info[dex_bp_id]
722+
per_file_deletions[source].append(dex_bp_id)
723+
elif dex_bp_id in self.function_bp_info:
724+
del self.function_bp_info[dex_bp_id]
725+
self.pending_function_breakpoints = True
726+
elif dex_bp_id in self.instruction_bp_info:
727+
del self.instruction_bp_info[dex_bp_id]
728+
self.pending_instruction_breakpoints = True
729+
643730
for file, deleted_ids in per_file_deletions.items():
644731
old_len = len(self.file_to_bp[file])
645732
self.file_to_bp[file] = [
@@ -657,7 +744,13 @@ def _get_launch_params(self, cmdline):
657744
""" "Set the debugger-specific params used in a launch request."""
658745

659746
def launch(self, cmdline):
660-
assert len(self.file_to_bp.keys()) > 0
747+
# FIXME: This should probably not a warning, not an assert.
748+
assert (
749+
len(self.file_to_bp)
750+
+ len(self.function_bp_info)
751+
+ len(self.instruction_bp_info)
752+
> 0
753+
), "Expected at least one breakpoint before launching"
661754

662755
if self.context.options.target_run_args:
663756
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
@@ -533,6 +533,13 @@ def _confirm_triggered_breakpoint_ids(self, dex_bp_ids):
533533
manually check conditions here."""
534534
confirmed_breakpoint_ids = set()
535535
for dex_bp_id in dex_bp_ids:
536+
# Function and instruction breakpoints don't use conditions.
537+
# FIXME: That's not a DAP restruction, so they could in future.
538+
if dex_bp_id not in self.bp_info:
539+
assert dex_bp_id in self.function_bp_info or dex_bp_id in self.instruction_bp_info
540+
confirmed_breakpoint_ids.add(dex_bp_id)
541+
continue
542+
536543
_, _, cond = self.bp_info[dex_bp_id]
537544
if cond is None:
538545
confirmed_breakpoint_ids.add(dex_bp_id)

0 commit comments

Comments
 (0)