Skip to content

Commit e972d8c

Browse files
authored
[lldb-dap] assembly breakpoints (#139969)
* Support assembly source breakpoints * Change `sourceReference` to be the symbol load address for simplicity and consistency across threads/frames [Screencast From 2025-05-17 23-57-30.webm](https://github.com/user-attachments/assets/2e7c181d-42c1-4121-8f13-b180c19d0e33)
1 parent ee4002d commit e972d8c

23 files changed

+522
-136
lines changed

lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,36 @@ def dump_dap_log(log_file):
105105
print("========= END =========", file=sys.stderr)
106106

107107

108+
class Source(object):
109+
def __init__(
110+
self, path: Optional[str] = None, source_reference: Optional[int] = None
111+
):
112+
self._name = None
113+
self._path = None
114+
self._source_reference = None
115+
116+
if path is not None:
117+
self._name = os.path.basename(path)
118+
self._path = path
119+
elif source_reference is not None:
120+
self._source_reference = source_reference
121+
else:
122+
raise ValueError("Either path or source_reference must be provided")
123+
124+
def __str__(self):
125+
return f"Source(name={self.name}, path={self.path}), source_reference={self.source_reference})"
126+
127+
def as_dict(self):
128+
source_dict = {}
129+
if self._name is not None:
130+
source_dict["name"] = self._name
131+
if self._path is not None:
132+
source_dict["path"] = self._path
133+
if self._source_reference is not None:
134+
source_dict["sourceReference"] = self._source_reference
135+
return source_dict
136+
137+
108138
class DebugCommunication(object):
109139
def __init__(
110140
self,
@@ -949,15 +979,13 @@ def request_scopes(self, frameId):
949979
command_dict = {"command": "scopes", "type": "request", "arguments": args_dict}
950980
return self.send_recv(command_dict)
951981

952-
def request_setBreakpoints(self, file_path, line_array, data=None):
982+
def request_setBreakpoints(self, source: Source, line_array, data=None):
953983
"""data is array of parameters for breakpoints in line_array.
954984
Each parameter object is 1:1 mapping with entries in line_entry.
955985
It contains optional location/hitCondition/logMessage parameters.
956986
"""
957-
(dir, base) = os.path.split(file_path)
958-
source_dict = {"name": base, "path": file_path}
959987
args_dict = {
960-
"source": source_dict,
988+
"source": source.as_dict(),
961989
"sourceModified": False,
962990
}
963991
if line_array is not None:
@@ -1382,7 +1410,7 @@ def run_vscode(dbg, args, options):
13821410
else:
13831411
source_to_lines[path] = [int(line)]
13841412
for source in source_to_lines:
1385-
dbg.request_setBreakpoints(source, source_to_lines[source])
1413+
dbg.request_setBreakpoints(Source(source), source_to_lines[source])
13861414
if options.funcBreakpoints:
13871415
dbg.request_setFunctionBreakpoints(options.funcBreakpoints)
13881416

lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import uuid
55

66
import dap_server
7+
from dap_server import Source
78
from lldbsuite.test.lldbtest import *
89
from lldbsuite.test import lldbplatformutil
910
import lldbgdbserverutils
@@ -55,7 +56,9 @@ def set_source_breakpoints(self, source_path, lines, data=None):
5556
Each object in data is 1:1 mapping with the entry in lines.
5657
It contains optional location/hitCondition/logMessage parameters.
5758
"""
58-
response = self.dap_server.request_setBreakpoints(source_path, lines, data)
59+
response = self.dap_server.request_setBreakpoints(
60+
Source(source_path), lines, data
61+
)
5962
if response is None or not response["success"]:
6063
return []
6164
breakpoints = response["body"]["breakpoints"]
@@ -64,6 +67,20 @@ def set_source_breakpoints(self, source_path, lines, data=None):
6467
breakpoint_ids.append("%i" % (breakpoint["id"]))
6568
return breakpoint_ids
6669

70+
def set_source_breakpoints_assembly(self, source_reference, lines, data=None):
71+
response = self.dap_server.request_setBreakpoints(
72+
Source(source_reference=source_reference),
73+
lines,
74+
data,
75+
)
76+
if response is None:
77+
return []
78+
breakpoints = response["body"]["breakpoints"]
79+
breakpoint_ids = []
80+
for breakpoint in breakpoints:
81+
breakpoint_ids.append("%i" % (breakpoint["id"]))
82+
return breakpoint_ids
83+
6784
def set_function_breakpoints(self, functions, condition=None, hitCondition=None):
6885
"""Sets breakpoints by function name given an array of function names
6986
and returns an array of strings containing the breakpoint IDs
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
C_SOURCES := main.c
2+
3+
include Makefile.rules
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""
2+
Test lldb-dap setBreakpoints request in assembly source references.
3+
"""
4+
5+
6+
from lldbsuite.test.decorators import *
7+
from dap_server import Source
8+
import lldbdap_testcase
9+
10+
11+
# @skipIfWindows
12+
class TestDAP_setBreakpointsAssembly(lldbdap_testcase.DAPTestCaseBase):
13+
def test_can_break_in_source_references(self):
14+
"""Tests hitting assembly source breakpoints"""
15+
program = self.getBuildArtifact("a.out")
16+
self.build_and_launch(program)
17+
18+
assmebly_func_breakpoints = self.set_function_breakpoints(["assembly_func"])
19+
self.continue_to_breakpoints(assmebly_func_breakpoints)
20+
21+
assembly_func_frame = self.get_stackFrames()[0]
22+
self.assertIn(
23+
"sourceReference",
24+
assembly_func_frame.get("source"),
25+
"Expected assembly source frame",
26+
)
27+
28+
line = assembly_func_frame["line"]
29+
30+
# Set an assembly breakpoint in the next line and check that it's hit
31+
source_reference = assembly_func_frame["source"]["sourceReference"]
32+
assembly_breakpoint_ids = self.set_source_breakpoints_assembly(
33+
source_reference, [line + 1]
34+
)
35+
self.continue_to_breakpoints(assembly_breakpoint_ids)
36+
37+
# Continue again and verify it hits in the next function call
38+
self.continue_to_breakpoints(assmebly_func_breakpoints)
39+
self.continue_to_breakpoints(assembly_breakpoint_ids)
40+
41+
# Clear the breakpoint and then check that the assembly breakpoint does not hit next time
42+
self.set_source_breakpoints_assembly(source_reference, [])
43+
self.continue_to_breakpoints(assmebly_func_breakpoints)
44+
self.continue_to_exit()
45+
46+
def test_break_on_invalid_source_reference(self):
47+
"""Tests hitting assembly source breakpoints"""
48+
program = self.getBuildArtifact("a.out")
49+
self.build_and_launch(program)
50+
51+
# Verify that setting a breakpoint on an invalid source reference fails
52+
response = self.dap_server.request_setBreakpoints(
53+
Source(source_reference=-1), [1]
54+
)
55+
self.assertIsNotNone(response)
56+
breakpoints = response["body"]["breakpoints"]
57+
self.assertEqual(len(breakpoints), 1)
58+
breakpoint = breakpoints[0]
59+
self.assertFalse(
60+
breakpoint["verified"], "Expected breakpoint to not be verified"
61+
)
62+
self.assertIn("message", breakpoint, "Expected message to be present")
63+
self.assertEqual(
64+
breakpoint["message"],
65+
"Invalid sourceReference.",
66+
)
67+
68+
# Verify that setting a breakpoint on a source reference without a symbol also fails
69+
response = self.dap_server.request_setBreakpoints(
70+
Source(source_reference=0), [1]
71+
)
72+
self.assertIsNotNone(response)
73+
breakpoints = response["body"]["breakpoints"]
74+
self.assertEqual(len(breakpoints), 1)
75+
breakpoint = breakpoints[0]
76+
self.assertFalse(
77+
breakpoint["verified"], "Expected breakpoint to not be verified"
78+
)
79+
self.assertIn("message", breakpoint, "Expected message to be present")
80+
self.assertEqual(
81+
breakpoint["message"],
82+
"Breakpoints in assembly without a valid symbol are not supported yet.",
83+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
__attribute__((nodebug)) int assembly_func(int n) {
2+
n += 1;
3+
n += 2;
4+
n += 3;
5+
6+
return n;
7+
}
8+
9+
int main(int argc, char const *argv[]) {
10+
assembly_func(10);
11+
assembly_func(20);
12+
assembly_func(30);
13+
return 0;
14+
}

lldb/test/API/tools/lldb-dap/breakpoint-events/TestDAP_breakpointEvents.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Test lldb-dap setBreakpoints request
33
"""
44

5-
import dap_server
5+
from dap_server import Source
66
from lldbsuite.test.decorators import *
77
from lldbsuite.test.lldbtest import *
88
from lldbsuite.test import lldbutil
@@ -58,7 +58,7 @@ def test_breakpoint_events(self):
5858
# Set breakpoints and verify that they got set correctly
5959
dap_breakpoint_ids = []
6060
response = self.dap_server.request_setBreakpoints(
61-
main_source_path, [main_bp_line]
61+
Source(main_source_path), [main_bp_line]
6262
)
6363
self.assertTrue(response["success"])
6464
breakpoints = response["body"]["breakpoints"]
@@ -70,7 +70,7 @@ def test_breakpoint_events(self):
7070
)
7171

7272
response = self.dap_server.request_setBreakpoints(
73-
foo_source_path, [foo_bp1_line]
73+
Source(foo_source_path), [foo_bp1_line]
7474
)
7575
self.assertTrue(response["success"])
7676
breakpoints = response["body"]["breakpoints"]

lldb/test/API/tools/lldb-dap/breakpoint/TestDAP_setBreakpoints.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44

55

6-
import dap_server
6+
from dap_server import Source
77
import shutil
88
from lldbsuite.test.decorators import *
99
from lldbsuite.test.lldbtest import *
@@ -58,7 +58,9 @@ def test_source_map(self):
5858
self.launch(program, sourceMap=source_map)
5959

6060
# breakpoint in main.cpp
61-
response = self.dap_server.request_setBreakpoints(new_main_path, [main_line])
61+
response = self.dap_server.request_setBreakpoints(
62+
Source(new_main_path), [main_line]
63+
)
6264
breakpoints = response["body"]["breakpoints"]
6365
self.assertEqual(len(breakpoints), 1)
6466
breakpoint = breakpoints[0]
@@ -68,7 +70,9 @@ def test_source_map(self):
6870
self.assertEqual(new_main_path, breakpoint["source"]["path"])
6971

7072
# 2nd breakpoint, which is from a dynamically loaded library
71-
response = self.dap_server.request_setBreakpoints(new_other_path, [other_line])
73+
response = self.dap_server.request_setBreakpoints(
74+
Source(new_other_path), [other_line]
75+
)
7276
breakpoints = response["body"]["breakpoints"]
7377
breakpoint = breakpoints[0]
7478
self.assertEqual(breakpoint["line"], other_line)
@@ -81,7 +85,9 @@ def test_source_map(self):
8185
self.verify_breakpoint_hit([other_breakpoint_id])
8286

8387
# 2nd breakpoint again, which should be valid at this point
84-
response = self.dap_server.request_setBreakpoints(new_other_path, [other_line])
88+
response = self.dap_server.request_setBreakpoints(
89+
Source(new_other_path), [other_line]
90+
)
8591
breakpoints = response["body"]["breakpoints"]
8692
breakpoint = breakpoints[0]
8793
self.assertEqual(breakpoint["line"], other_line)
@@ -124,7 +130,7 @@ def test_set_and_clear(self):
124130
self.build_and_launch(program)
125131

126132
# Set 3 breakpoints and verify that they got set correctly
127-
response = self.dap_server.request_setBreakpoints(self.main_path, lines)
133+
response = self.dap_server.request_setBreakpoints(Source(self.main_path), lines)
128134
line_to_id = {}
129135
breakpoints = response["body"]["breakpoints"]
130136
self.assertEqual(
@@ -149,7 +155,7 @@ def test_set_and_clear(self):
149155
lines.remove(second_line)
150156
# Set 2 breakpoints and verify that the previous breakpoints that were
151157
# set above are still set.
152-
response = self.dap_server.request_setBreakpoints(self.main_path, lines)
158+
response = self.dap_server.request_setBreakpoints(Source(self.main_path), lines)
153159
breakpoints = response["body"]["breakpoints"]
154160
self.assertEqual(
155161
len(breakpoints),
@@ -194,7 +200,7 @@ def test_set_and_clear(self):
194200
# Now clear all breakpoints for the source file by passing down an
195201
# empty lines array
196202
lines = []
197-
response = self.dap_server.request_setBreakpoints(self.main_path, lines)
203+
response = self.dap_server.request_setBreakpoints(Source(self.main_path), lines)
198204
breakpoints = response["body"]["breakpoints"]
199205
self.assertEqual(
200206
len(breakpoints),
@@ -214,7 +220,7 @@ def test_set_and_clear(self):
214220
# Now set a breakpoint again in the same source file and verify it
215221
# was added.
216222
lines = [second_line]
217-
response = self.dap_server.request_setBreakpoints(self.main_path, lines)
223+
response = self.dap_server.request_setBreakpoints(Source(self.main_path), lines)
218224
if response:
219225
breakpoints = response["body"]["breakpoints"]
220226
self.assertEqual(
@@ -265,7 +271,7 @@ def test_clear_breakpoints_unset_breakpoints(self):
265271
self.build_and_launch(program)
266272

267273
# Set one breakpoint and verify that it got set correctly.
268-
response = self.dap_server.request_setBreakpoints(self.main_path, lines)
274+
response = self.dap_server.request_setBreakpoints(Source(self.main_path), lines)
269275
line_to_id = {}
270276
breakpoints = response["body"]["breakpoints"]
271277
self.assertEqual(
@@ -281,7 +287,7 @@ def test_clear_breakpoints_unset_breakpoints(self):
281287
# Now clear all breakpoints for the source file by not setting the
282288
# lines array.
283289
lines = None
284-
response = self.dap_server.request_setBreakpoints(self.main_path, lines)
290+
response = self.dap_server.request_setBreakpoints(Source(self.main_path), lines)
285291
breakpoints = response["body"]["breakpoints"]
286292
self.assertEqual(len(breakpoints), 0, "expect no source breakpoints")
287293

@@ -357,7 +363,9 @@ def test_column_breakpoints(self):
357363
# Set two breakpoints on the loop line at different columns.
358364
columns = [13, 39]
359365
response = self.dap_server.request_setBreakpoints(
360-
self.main_path, [loop_line, loop_line], list({"column": c} for c in columns)
366+
Source(self.main_path),
367+
[loop_line, loop_line],
368+
list({"column": c} for c in columns),
361369
)
362370

363371
# Verify the breakpoints were set correctly

lldb/test/API/tools/lldb-dap/instruction-breakpoint/TestDAP_instruction_breakpoint.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import dap_server
1+
from dap_server import Source
22
import shutil
33
from lldbsuite.test.decorators import *
44
from lldbsuite.test.lldbtest import *
@@ -33,7 +33,9 @@ def instruction_breakpoint_test(self):
3333
self.build_and_launch(program)
3434

3535
# Set source breakpoint 1
36-
response = self.dap_server.request_setBreakpoints(self.main_path, [main_line])
36+
response = self.dap_server.request_setBreakpoints(
37+
Source(self.main_path), [main_line]
38+
)
3739
breakpoints = response["body"]["breakpoints"]
3840
self.assertEqual(len(breakpoints), 1)
3941
breakpoint = breakpoints[0]

0 commit comments

Comments
 (0)