Skip to content

Commit 97dae99

Browse files
authored
Adding support for data breakpoints (#443)
* Added catching errors for the different GDB/MI commands * Initial support for data breakpoints, supports only data breakpoints for simple global variables * Adding tests for data breakpoint support * Editing breakpoint tests to accommodate new source code being used for testing * Testing for remote and non-stop mode * Adding support for data breakpoints for addresses given the command palette * Handle case where address to break at is provided as a decimal * Accepting receiving addresses as decimals as well
1 parent 24fe0df commit 97dae99

File tree

5 files changed

+271
-0
lines changed

5 files changed

+271
-0
lines changed

src/gdb/GDBDebugSessionBase.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
307307
response.body.supportsSteppingGranularity = true;
308308
response.body.supportsInstructionBreakpoints = true;
309309
response.body.supportsTerminateRequest = true;
310+
response.body.supportsDataBreakpoints = true;
310311
response.body.breakpointModes = this.getBreakpointModes();
311312
this.sendResponse(response);
312313
}
@@ -559,6 +560,64 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
559560
}
560561
}
561562

563+
protected async dataBreakpointInfoRequest(
564+
response: DebugProtocol.DataBreakpointInfoResponse,
565+
args: DebugProtocol.DataBreakpointInfoArguments
566+
): Promise<void> {
567+
// The args.name is the expression to watch
568+
let varExpression = args.name;
569+
if (args.asAddress && args.bytes) {
570+
// Make sure the address is in hex format, if not convert it to hex
571+
if (!varExpression.startsWith('0x')) {
572+
varExpression = `0x${BigInt(varExpression).toString(16)}`;
573+
}
574+
// The expression is an address, so we can set a data breakpoint directly withouth checking if it's a global symbol
575+
response.body = {
576+
dataId:
577+
// Check if bytes to be watched are more than 1, if so add the offset to the expression
578+
args.bytes > 1
579+
? varExpression + `+0x${args.bytes}`
580+
: varExpression,
581+
description: `Data breakpoint for ${varExpression}`,
582+
accessTypes: ['read', 'write', 'readWrite'],
583+
canPersist: true,
584+
};
585+
} else {
586+
// Send the varExpression as a query to the symbol info variables command
587+
try {
588+
const symbols = await mi.sendSymbolInfoVars(this.gdb, {
589+
name: `^${varExpression}$`,
590+
});
591+
/** If there are debug symbols matching the varExpression, then we can set a data breakpoint.
592+
* We are currently supporting primitive expressions only. ie. no pointer dereferencing, no struct members, no arrays.
593+
* The plan for the forseeable future is to expand our support for arrays, struct/union data types, and classes.
594+
* Also a guard should be added to prevent setting data breakpoints on invalid expressions.
595+
*/
596+
597+
if (symbols.symbols.debug.length > 0) {
598+
response.body = {
599+
dataId: varExpression,
600+
description: `Data breakpoint for ${varExpression}`,
601+
accessTypes: ['read', 'write', 'readWrite'],
602+
canPersist: true,
603+
};
604+
} else {
605+
response.body = {
606+
dataId: null,
607+
description: `No data breakpoint for ${varExpression}`,
608+
};
609+
}
610+
} catch {
611+
// Silently failing, no data breakpoint can be set for the expression
612+
response.body = {
613+
dataId: null,
614+
description: `No data breakpoint for ${varExpression}`,
615+
};
616+
}
617+
}
618+
this.sendResponse(response);
619+
}
620+
562621
private async getInstructionBreakpointList(): Promise<
563622
mi.MIBreakpointInfo[]
564623
> {
@@ -572,6 +631,86 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
572631
return existingInstBreakpointsList;
573632
}
574633

634+
private async getWatchpointList(): Promise<mi.MIBreakpointInfo[]> {
635+
// Get a list of existing watchpoints, using gdb-mi command -break-list
636+
const fullBreakpointsList = await mi.sendBreakList(this.gdb);
637+
// Filter out all watchpoints
638+
const existingWatchpointsList =
639+
fullBreakpointsList.BreakpointTable.body.filter((bp) =>
640+
bp['type'].includes('watchpoint')
641+
);
642+
return existingWatchpointsList;
643+
}
644+
645+
protected async setDataBreakpointsRequest(
646+
response: DebugProtocol.SetDataBreakpointsResponse,
647+
args: DebugProtocol.SetDataBreakpointsArguments
648+
): Promise<void> {
649+
await this.pauseIfNeeded();
650+
// Get existing GDB watchpoints
651+
let existingGDBWatchpointsList = await this.getWatchpointList();
652+
// Get the list of watchpoints from vscode
653+
const vscodeWatchpointsList = args.breakpoints;
654+
// Filter out watchpoints to be deleted, vscode is the golden reference
655+
const watchpointsToDelete = existingGDBWatchpointsList.filter(
656+
(bp) =>
657+
!vscodeWatchpointsList.some((vsbp) => {
658+
if (bp.what?.startsWith('*')) {
659+
return (
660+
vsbp.dataId === bp.what.slice(2, bp.what.length - 1)
661+
);
662+
} else {
663+
return vsbp.dataId === bp.what;
664+
}
665+
})
666+
);
667+
// Delete watchpoints to be deleted from GDB
668+
if (watchpointsToDelete.length > 0) {
669+
await mi.sendBreakDelete(this.gdb, {
670+
breakpoints: watchpointsToDelete.map((bp) => bp.number),
671+
});
672+
// update GDB watchpoint list
673+
existingGDBWatchpointsList = await this.getWatchpointList();
674+
}
675+
// Create a list of watchpoints to be created
676+
const watchpointsToBeCreated = vscodeWatchpointsList.filter(
677+
(bp) =>
678+
!existingGDBWatchpointsList.some((gdbbp) => {
679+
if (gdbbp.what?.startsWith('*')) {
680+
return (
681+
bp.dataId ===
682+
gdbbp.what.slice(2, gdbbp.what.length - 1)
683+
);
684+
} else {
685+
return bp.dataId === gdbbp.what;
686+
}
687+
})
688+
);
689+
// Create watchpoints in GDB
690+
for (const bp of watchpointsToBeCreated) {
691+
// If the dataId is an address, written in decimal or hex, then add the dereferencing operator before sending it to GDB
692+
if (numberRegex.test(bp.dataId) || bp.dataId.startsWith('0x')) {
693+
bp.dataId = `*(${bp.dataId})`;
694+
}
695+
await mi.sendBreakWatchpoint(this.gdb, bp.dataId, bp.accessType);
696+
}
697+
// Get the updated list of GDB watchpoints
698+
const gdbWatchpoints = await this.getWatchpointList();
699+
// Prepare response
700+
const actual: DebugProtocol.Breakpoint[] = gdbWatchpoints.map((bp) => {
701+
const responseBp: DebugProtocol.Breakpoint = {
702+
verified: bp.enabled === 'y',
703+
id: parseInt(bp.number, 10),
704+
};
705+
return responseBp;
706+
});
707+
response.body = {
708+
breakpoints: actual,
709+
};
710+
this.sendResponse(response);
711+
await this.continueIfNeeded();
712+
}
713+
575714
protected async setInstructionBreakpointsRequest(
576715
response: DebugProtocol.SetInstructionBreakpointsResponse,
577716
args: DebugProtocol.SetInstructionBreakpointsArguments

src/integration-tests/breakpoints.spec.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,66 @@ describe('breakpoints', async function () {
234234
expect(stoppedEvent.body).to.have.property('reason', 'breakpoint');
235235
});
236236

237+
it('sets a data breakpoint for a symbol', async function () {
238+
const isEligible = await dc.dataBreakpointInfoRequest({
239+
name: 'g_variable',
240+
});
241+
expect(isEligible.body).not.eq(undefined);
242+
expect(isEligible.body.dataId).eq('g_variable');
243+
const bpResp = await dc.setDataBreakpointsRequest({
244+
breakpoints: [
245+
{
246+
dataId: 'g_variable',
247+
accessType: 'read',
248+
},
249+
],
250+
});
251+
expect(bpResp.body.breakpoints.length).eq(1);
252+
});
253+
254+
it('sets a data breakpoint for an address', async function () {
255+
await dc.setBreakpointsRequest({
256+
source: {
257+
name: 'count.c',
258+
path: path.join(testProgramsDir, 'count.c'),
259+
},
260+
breakpoints: [
261+
{
262+
column: 1,
263+
line: 4,
264+
},
265+
],
266+
});
267+
await Promise.all([
268+
dc.waitForEvent('stopped'),
269+
dc.configurationDoneRequest(),
270+
]);
271+
const scope = await getScopes(dc);
272+
const evalRequestOutput = await dc.evaluateRequest({
273+
expression: '&g_variable',
274+
context: 'repl',
275+
frameId: scope.frame.id,
276+
});
277+
const addr = evalRequestOutput.body.result.split(' ')[0];
278+
const isEligible = await dc.dataBreakpointInfoRequest({
279+
name: addr,
280+
asAddress: true,
281+
bytes: 4,
282+
});
283+
expect(isEligible.body).not.eq(undefined);
284+
expect(isEligible.body.dataId).eq(addr + '+0x4');
285+
// Now set the data breakpoint
286+
const bpResp = await dc.setDataBreakpointsRequest({
287+
breakpoints: [
288+
{
289+
dataId: addr,
290+
accessType: 'read',
291+
},
292+
],
293+
});
294+
expect(bpResp.body.breakpoints.length).eq(1);
295+
});
296+
237297
it('set an instruction breakpoint', async function () {
238298
if (gdbNonStop && isRemoteTest) {
239299
this.skip();

src/integration-tests/test-programs/count.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ int main() {
77
}
88
return 0;
99
}
10+
volatile int g_variable = 0xDEADBEEF;

src/mi/breakpoint.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ export interface MIBreakInsertResponse extends MIResponse {
2929
multiple?: MIBreakpointInfo[];
3030
}
3131

32+
// Watchpoint response
33+
export interface MIBreakWatchResponse extends MIResponse {
34+
number: string;
35+
exp: string;
36+
}
37+
38+
// Watchpoint options
39+
type MIWatchpointInsertOptions = 'read' | 'write' | 'readWrite';
40+
3241
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
3342
export interface MIBreakDeleteRequest {}
3443

@@ -171,6 +180,17 @@ export async function sendBreakpointInsert(
171180
return clean;
172181
}
173182

183+
export async function sendBreakWatchpoint(
184+
gdb: IGDBBackend,
185+
expression: string,
186+
options?: MIWatchpointInsertOptions
187+
): Promise<MIBreakWatchResponse> {
188+
const watchPointType =
189+
options === 'read' ? '-r ' : options === 'readWrite' ? '-a ' : '';
190+
const command = `-break-watch ${watchPointType}${expression}`;
191+
return gdb.sendCommand(command);
192+
}
193+
174194
export function sendBreakDelete(
175195
gdb: IGDBBackend,
176196
request: {

src/mi/var.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export interface MIVarUpdateResponse {
5252
in_scope: string;
5353
type_changed: string;
5454
has_more: string;
55+
new_type: string;
56+
new_num_children: string;
5557
}>;
5658
}
5759

@@ -67,6 +69,28 @@ export interface MIVarPathInfoResponse {
6769
path_expr: string;
6870
}
6971

72+
export interface MIDebugSymbol {
73+
line: string;
74+
name: string;
75+
type: string;
76+
description: string;
77+
}
78+
79+
export interface MINonDebugSymbol {
80+
address: string;
81+
name: string;
82+
}
83+
export interface MISymbolInfoVarsDebug {
84+
filename: string;
85+
fullname: string;
86+
symbols: MIDebugSymbol[];
87+
}
88+
export interface MISymbolInfoVarsResponse {
89+
symbols: {
90+
debug: MISymbolInfoVarsDebug[];
91+
nondebug: MINonDebugSymbol[];
92+
};
93+
}
7094
function quote(expression: string) {
7195
return `"${expression}"`;
7296
}
@@ -204,3 +228,30 @@ export function sendVarSetFormatToHex(
204228
const command = `-var-set-format ${name} hexadecimal`;
205229
return gdb.sendCommand(command);
206230
}
231+
232+
export function sendSymbolInfoVars(
233+
gdb: IGDBBackend,
234+
params?: {
235+
name?: string;
236+
type?: string;
237+
max_result?: string;
238+
non_debug?: boolean;
239+
}
240+
): Promise<MISymbolInfoVarsResponse> {
241+
let command = '-symbol-info-variables';
242+
if (params) {
243+
if (params.name) {
244+
command += ` --name ${params.name}`;
245+
}
246+
if (params.type) {
247+
command += ` --type ${params.type}`;
248+
}
249+
if (params.max_result) {
250+
command += ` --max-result ${params.max_result}`;
251+
}
252+
if (params.non_debug) {
253+
command += ' --include-nondebug';
254+
}
255+
}
256+
return gdb.sendCommand(command);
257+
}

0 commit comments

Comments
 (0)