Skip to content

Commit f790bd3

Browse files
authored
Improving the DisassembleRequest logic (#341)
This is an update for the disassembleRequest logic in the debug adapter. This implementation is covering the scenarios discussed at the #340 . Notes: - The previous implementation was covering a scenario for the endMemoryReference argument. I couldn't find the argument in the DAP specifications. Since this argument doesn't exists on the current DAP protocol and only existed for the now deleted Eclipse CDT front end, this field support has been removed. - The main thing this code changes is handling negative offsets from the memory reference, something the original code did not do and wasn't used by vscode when the original code was written.
1 parent 9272451 commit f790bd3

8 files changed

+650
-154
lines changed

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@
6363
},
6464
"homepage": "https://github.com/eclipse-cdt-cloud/cdt-gdb-adapter#readme",
6565
"dependencies": {
66-
"@vscode/debugadapter": "^1.59.0",
67-
"@vscode/debugprotocol": "^1.59.0",
66+
"@vscode/debugadapter": "^1.68.0",
67+
"@vscode/debugprotocol": "^1.68.0",
6868
"node-addon-api": "^4.3.0",
6969
"serialport": "11.0.0",
7070
"utf8": "^3.0.0"

src/gdb/GDBDebugSessionBase.ts

+38-101
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ import {
3939
CDTDisassembleArguments,
4040
} from '../types/session';
4141
import { IGDBBackend, IGDBBackendFactory } from '../types/gdb';
42+
import { getInstructions } from '../util/disassembly';
43+
import { calculateMemoryOffset } from '../util/calculateMemoryOffset';
4244

4345
class ThreadWithStatus implements DebugProtocol.Thread {
4446
id: number;
@@ -1344,115 +1346,50 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
13441346
args: CDTDisassembleArguments
13451347
) {
13461348
try {
1347-
const meanSizeOfInstruction = 4;
1348-
let startOffset = 0;
1349-
let lastStartOffset = -1;
1349+
if (!args.memoryReference) {
1350+
throw new Error('Target memory reference is not specified!');
1351+
}
1352+
const instructionStartOffset = args.instructionOffset ?? 0;
1353+
const instructionEndOffset =
1354+
args.instructionCount + instructionStartOffset;
13501355
const instructions: DebugProtocol.DisassembledInstruction[] = [];
1351-
let oneIterationOnly = false;
1352-
outer_loop: while (
1353-
instructions.length < args.instructionCount &&
1354-
!oneIterationOnly
1355-
) {
1356-
if (startOffset === lastStartOffset) {
1357-
// We have stopped getting new instructions, give up
1358-
break outer_loop;
1359-
}
1360-
lastStartOffset = startOffset;
1361-
1362-
const fetchSize =
1363-
(args.instructionCount - instructions.length) *
1364-
meanSizeOfInstruction;
1365-
1366-
// args.memoryReference is an arbitrary expression, so let GDB do the
1367-
// math on resolving value rather than doing the addition in the adapter
1368-
try {
1369-
const stepStartAddress = `(${args.memoryReference})+${startOffset}`;
1370-
let stepEndAddress = `(${args.memoryReference})+${startOffset}+${fetchSize}`;
1371-
if (args.endMemoryReference && instructions.length === 0) {
1372-
// On the first call, if we have an end memory address use it instead of
1373-
// the approx size
1374-
stepEndAddress = args.endMemoryReference;
1375-
oneIterationOnly = true;
1376-
}
1377-
const result = await mi.sendDataDisassemble(
1378-
this.gdb,
1379-
stepStartAddress,
1380-
stepEndAddress
1381-
);
1382-
for (const asmInsn of result.asm_insns) {
1383-
const line: number | undefined = asmInsn.line
1384-
? parseInt(asmInsn.line, 10)
1385-
: undefined;
1386-
const source = {
1387-
name: asmInsn.file,
1388-
path: asmInsn.fullname,
1389-
} as DebugProtocol.Source;
1390-
for (const asmLine of asmInsn.line_asm_insn) {
1391-
let funcAndOffset: string | undefined;
1392-
if (asmLine['func-name'] && asmLine.offset) {
1393-
funcAndOffset = `${asmLine['func-name']}+${asmLine.offset}`;
1394-
} else if (asmLine['func-name']) {
1395-
funcAndOffset = asmLine['func-name'];
1396-
} else {
1397-
funcAndOffset = undefined;
1398-
}
1399-
const disInsn = {
1400-
address: asmLine.address,
1401-
instructionBytes: asmLine.opcodes,
1402-
instruction: asmLine.inst,
1403-
symbol: funcAndOffset,
1404-
location: source,
1405-
line,
1406-
} as DebugProtocol.DisassembledInstruction;
1407-
instructions.push(disInsn);
1408-
if (instructions.length === args.instructionCount) {
1409-
break outer_loop;
1410-
}
1356+
const memoryReference =
1357+
args.offset === undefined
1358+
? args.memoryReference
1359+
: calculateMemoryOffset(args.memoryReference, args.offset);
1360+
1361+
if (instructionStartOffset < 0) {
1362+
// Getting lower memory area
1363+
const list = await getInstructions(
1364+
this.gdb,
1365+
memoryReference,
1366+
instructionStartOffset
1367+
);
14111368

1412-
const bytes = asmLine.opcodes.replace(/\s/g, '');
1413-
startOffset += bytes.length;
1414-
}
1415-
}
1416-
} catch (err) {
1417-
// Failed to read instruction -- what best to do here?
1418-
// in other words, whose responsibility (adapter or client)
1419-
// to reissue reads in smaller chunks to find good memory
1420-
while (instructions.length < args.instructionCount) {
1421-
const badDisInsn = {
1422-
// TODO this should start at byte after last retrieved address
1423-
address: `0x${startOffset.toString(16)}`,
1424-
instruction:
1425-
err instanceof Error
1426-
? err.message
1427-
: String(err),
1428-
} as DebugProtocol.DisassembledInstruction;
1429-
instructions.push(badDisInsn);
1430-
startOffset += 2;
1431-
}
1432-
break outer_loop;
1433-
}
1369+
// Add them to instruction list
1370+
instructions.push(...list);
14341371
}
14351372

1436-
if (!args.endMemoryReference) {
1437-
while (instructions.length < args.instructionCount) {
1438-
const badDisInsn = {
1439-
// TODO this should start at byte after last retrieved address
1440-
address: `0x${startOffset.toString(16)}`,
1441-
instruction: 'failed to retrieve instruction',
1442-
} as DebugProtocol.DisassembledInstruction;
1443-
instructions.push(badDisInsn);
1444-
startOffset += 2;
1445-
}
1373+
if (instructionEndOffset > 0) {
1374+
// Getting higher memory area
1375+
const list = await getInstructions(
1376+
this.gdb,
1377+
memoryReference,
1378+
instructionEndOffset
1379+
);
1380+
1381+
// Add them to instruction list
1382+
instructions.push(...list);
14461383
}
14471384

1448-
response.body = { instructions };
1385+
response.body = {
1386+
instructions,
1387+
};
14491388
this.sendResponse(response);
14501389
} catch (err) {
1451-
this.sendErrorResponse(
1452-
response,
1453-
1,
1454-
err instanceof Error ? err.message : String(err)
1455-
);
1390+
const message = err instanceof Error ? err.message : String(err);
1391+
this.sendEvent(new OutputEvent(`Error: ${message}`));
1392+
this.sendErrorResponse(response, 1, message);
14561393
}
14571394
}
14581395

src/integration-tests/diassemble.spec.ts

+118-42
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,36 @@ import * as path from 'path';
1313
import { DebugProtocol } from '@vscode/debugprotocol/lib/debugProtocol';
1414
import { CdtDebugClient } from './debugClient';
1515
import { fillDefaults, standardBeforeEach, testProgramsDir } from './utils';
16+
import { assert } from 'sinon';
1617

1718
describe('Disassembly Test Suite', function () {
1819
let dc: CdtDebugClient;
1920
let frame: DebugProtocol.StackFrame;
2021
const disProgram = path.join(testProgramsDir, 'disassemble');
2122
const disSrc = path.join(testProgramsDir, 'disassemble.c');
2223

24+
const expectsGeneralDisassemble = (
25+
disassemble: DebugProtocol.DisassembleResponse,
26+
length: number,
27+
ignoreEmptyInstructions?: boolean
28+
) => {
29+
expect(disassemble).not.eq(undefined);
30+
expect(disassemble.body).not.eq(undefined);
31+
if (disassemble.body) {
32+
const instructions = disassemble.body.instructions;
33+
expect(instructions).to.have.lengthOf(length);
34+
// the contents of the instructions are platform dependent, so instead
35+
// make sure we have read fully
36+
for (const i of instructions) {
37+
expect(i.address).to.have.lengthOf.greaterThan(0);
38+
expect(i.instruction).to.have.lengthOf.greaterThan(0);
39+
if (!ignoreEmptyInstructions) {
40+
expect(i.instructionBytes).to.have.lengthOf.greaterThan(0);
41+
}
42+
}
43+
}
44+
};
45+
2346
beforeEach(async function () {
2447
dc = await standardBeforeEach();
2548

@@ -60,19 +83,8 @@ describe('Disassembly Test Suite', function () {
6083
memoryReference: 'main',
6184
instructionCount: 100,
6285
})) as DebugProtocol.DisassembleResponse;
63-
expect(disassemble).not.eq(undefined);
64-
expect(disassemble.body).not.eq(undefined);
65-
if (disassemble.body) {
66-
const instructions = disassemble.body.instructions;
67-
expect(instructions).to.have.lengthOf(100);
68-
// the contents of the instructions are platform dependent, so instead
69-
// make sure we have read fully
70-
for (const i of instructions) {
71-
expect(i.address).to.have.lengthOf.greaterThan(0);
72-
expect(i.instructionBytes).to.have.lengthOf.greaterThan(0);
73-
expect(i.instruction).to.have.lengthOf.greaterThan(0);
74-
}
75-
}
86+
87+
expectsGeneralDisassemble(disassemble, 100);
7688
});
7789

7890
it('can disassemble with no source references', async function () {
@@ -82,38 +94,102 @@ describe('Disassembly Test Suite', function () {
8294
memoryReference: 'main+1000',
8395
instructionCount: 100,
8496
})) as DebugProtocol.DisassembleResponse;
85-
expect(disassemble).not.eq(undefined);
86-
expect(disassemble.body).not.eq(undefined);
87-
if (disassemble.body) {
88-
const instructions = disassemble.body.instructions;
89-
expect(instructions).to.have.lengthOf(100);
90-
// the contents of the instructions are platform dependent, so instead
91-
// make sure we have read fully
92-
for (const i of instructions) {
93-
expect(i.address).to.have.lengthOf.greaterThan(0);
94-
expect(i.instructionBytes).to.have.lengthOf.greaterThan(0);
95-
expect(i.instruction).to.have.lengthOf.greaterThan(0);
96-
}
97-
}
97+
98+
expectsGeneralDisassemble(disassemble, 100);
9899
});
99100

100-
it('can handle disassemble at bad address', async function () {
101+
it('can disassemble with negative offsets', async function () {
101102
const disassemble = (await dc.send('disassemble', {
102-
memoryReference: '0x0',
103-
instructionCount: 10,
104-
})) as DebugProtocol.DisassembleResponse;
105-
expect(disassemble).not.eq(undefined);
106-
expect(disassemble.body).not.eq(undefined);
107-
if (disassemble.body) {
108-
const instructions = disassemble.body.instructions;
109-
expect(instructions).to.have.lengthOf(10);
110-
// the contens of the instructions are platform dependent, so instead
111-
// make sure we have read fully
112-
for (const i of instructions) {
113-
expect(i.address).to.have.lengthOf.greaterThan(0);
114-
expect(i.instruction).to.have.lengthOf.greaterThan(0);
115-
expect(i.instructionBytes).eq(undefined);
116-
}
103+
memoryReference: 'main',
104+
instructionOffset: -20,
105+
instructionCount: 20,
106+
} as DebugProtocol.DisassembleArguments)) as DebugProtocol.DisassembleResponse;
107+
108+
expectsGeneralDisassemble(disassemble, 20, true);
109+
});
110+
111+
it('send error response handle on empty memory reference', async function () {
112+
try {
113+
await dc.send('disassemble', {
114+
memoryReference: '',
115+
instructionOffset: -20,
116+
instructionCount: 20,
117+
} as DebugProtocol.DisassembleArguments);
118+
assert.fail('Should throw error!');
119+
} catch (e) {
120+
expect(e).to.be.deep.equal(
121+
new Error('Target memory reference is not specified!')
122+
);
123+
}
124+
});
125+
126+
it('can disassemble with correct boundries', async function () {
127+
const get = (
128+
disassemble: DebugProtocol.DisassembleResponse,
129+
offset: number
130+
) => {
131+
const instruction = disassemble.body?.instructions[offset];
132+
expect(instruction).not.eq(undefined);
133+
// Instruction undefined already checked.
134+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
135+
return instruction!;
136+
};
137+
138+
const expectsInstructionEquals = (
139+
instruction1: DebugProtocol.DisassembledInstruction,
140+
instruction2: DebugProtocol.DisassembledInstruction,
141+
message?: string
142+
) => {
143+
expect(instruction1.address).to.eq(instruction2.address, message);
144+
};
145+
146+
const disassembleLower = (await dc.send('disassemble', {
147+
memoryReference: 'main',
148+
instructionOffset: -20,
149+
instructionCount: 20,
150+
} as DebugProtocol.DisassembleArguments)) as DebugProtocol.DisassembleResponse;
151+
const disassembleMiddle = (await dc.send('disassemble', {
152+
memoryReference: 'main',
153+
instructionOffset: -10,
154+
instructionCount: 20,
155+
} as DebugProtocol.DisassembleArguments)) as DebugProtocol.DisassembleResponse;
156+
const disassembleHigher = (await dc.send('disassemble', {
157+
memoryReference: 'main',
158+
instructionOffset: 0,
159+
instructionCount: 20,
160+
} as DebugProtocol.DisassembleArguments)) as DebugProtocol.DisassembleResponse;
161+
162+
expectsGeneralDisassemble(disassembleLower, 20, true);
163+
expectsGeneralDisassemble(disassembleMiddle, 20, true);
164+
expectsGeneralDisassemble(disassembleHigher, 20, true);
165+
166+
// Current implementation have known edge cases, possibly instruction misaligning while
167+
// handling the negative offsets, please refer to the discussion at the following link:
168+
// https://github.com/eclipse-cdt-cloud/cdt-gdb-adapter/pull/341#discussion_r1857422980
169+
expectsInstructionEquals(
170+
get(disassembleLower, 15),
171+
get(disassembleMiddle, 5),
172+
'lower[15] should be same with middle[5]'
173+
);
174+
175+
expectsInstructionEquals(
176+
get(disassembleMiddle, 15),
177+
get(disassembleHigher, 5),
178+
'middle[15] should be same with higher[5]'
179+
);
180+
});
181+
182+
it('return error at bad address', async function () {
183+
try {
184+
await dc.send('disassemble', {
185+
memoryReference: '0x0',
186+
instructionCount: 10,
187+
} as DebugProtocol.DisassembleArguments);
188+
assert.fail('Should throw error!');
189+
} catch (e) {
190+
expect(e).to.be.deep.equal(
191+
new Error('Cannot access memory at address 0x0')
192+
);
117193
}
118194
});
119195
});

0 commit comments

Comments
 (0)