From f790bd3e3d3835f5aeeb6308230d52517d54379b Mon Sep 17 00:00:00 2001 From: Asim Gunes <39078160+asimgunes@users.noreply.github.com> Date: Tue, 10 Dec 2024 03:07:19 +0000 Subject: [PATCH] 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. --- package.json | 4 +- src/gdb/GDBDebugSessionBase.ts | 139 ++++---------- src/integration-tests/diassemble.spec.ts | 160 +++++++++++----- src/integration-tests/util.spec.ts | 218 ++++++++++++++++++++++ src/mi/data.ts | 6 +- src/util/calculateMemoryOffset.ts | 38 ++++ src/util/disassembly.ts | 222 +++++++++++++++++++++++ yarn.lock | 17 +- 8 files changed, 650 insertions(+), 154 deletions(-) create mode 100644 src/util/calculateMemoryOffset.ts create mode 100644 src/util/disassembly.ts diff --git a/package.json b/package.json index e84cd5da..ce4d8d7a 100644 --- a/package.json +++ b/package.json @@ -63,8 +63,8 @@ }, "homepage": "https://github.com/eclipse-cdt-cloud/cdt-gdb-adapter#readme", "dependencies": { - "@vscode/debugadapter": "^1.59.0", - "@vscode/debugprotocol": "^1.59.0", + "@vscode/debugadapter": "^1.68.0", + "@vscode/debugprotocol": "^1.68.0", "node-addon-api": "^4.3.0", "serialport": "11.0.0", "utf8": "^3.0.0" diff --git a/src/gdb/GDBDebugSessionBase.ts b/src/gdb/GDBDebugSessionBase.ts index e8c6632d..9266f842 100644 --- a/src/gdb/GDBDebugSessionBase.ts +++ b/src/gdb/GDBDebugSessionBase.ts @@ -39,6 +39,8 @@ import { CDTDisassembleArguments, } from '../types/session'; import { IGDBBackend, IGDBBackendFactory } from '../types/gdb'; +import { getInstructions } from '../util/disassembly'; +import { calculateMemoryOffset } from '../util/calculateMemoryOffset'; class ThreadWithStatus implements DebugProtocol.Thread { id: number; @@ -1344,115 +1346,50 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession { args: CDTDisassembleArguments ) { try { - const meanSizeOfInstruction = 4; - let startOffset = 0; - let lastStartOffset = -1; + if (!args.memoryReference) { + throw new Error('Target memory reference is not specified!'); + } + const instructionStartOffset = args.instructionOffset ?? 0; + const instructionEndOffset = + args.instructionCount + instructionStartOffset; const instructions: DebugProtocol.DisassembledInstruction[] = []; - let oneIterationOnly = false; - outer_loop: while ( - instructions.length < args.instructionCount && - !oneIterationOnly - ) { - if (startOffset === lastStartOffset) { - // We have stopped getting new instructions, give up - break outer_loop; - } - lastStartOffset = startOffset; - - const fetchSize = - (args.instructionCount - instructions.length) * - meanSizeOfInstruction; - - // args.memoryReference is an arbitrary expression, so let GDB do the - // math on resolving value rather than doing the addition in the adapter - try { - const stepStartAddress = `(${args.memoryReference})+${startOffset}`; - let stepEndAddress = `(${args.memoryReference})+${startOffset}+${fetchSize}`; - if (args.endMemoryReference && instructions.length === 0) { - // On the first call, if we have an end memory address use it instead of - // the approx size - stepEndAddress = args.endMemoryReference; - oneIterationOnly = true; - } - const result = await mi.sendDataDisassemble( - this.gdb, - stepStartAddress, - stepEndAddress - ); - for (const asmInsn of result.asm_insns) { - const line: number | undefined = asmInsn.line - ? parseInt(asmInsn.line, 10) - : undefined; - const source = { - name: asmInsn.file, - path: asmInsn.fullname, - } as DebugProtocol.Source; - for (const asmLine of asmInsn.line_asm_insn) { - let funcAndOffset: string | undefined; - if (asmLine['func-name'] && asmLine.offset) { - funcAndOffset = `${asmLine['func-name']}+${asmLine.offset}`; - } else if (asmLine['func-name']) { - funcAndOffset = asmLine['func-name']; - } else { - funcAndOffset = undefined; - } - const disInsn = { - address: asmLine.address, - instructionBytes: asmLine.opcodes, - instruction: asmLine.inst, - symbol: funcAndOffset, - location: source, - line, - } as DebugProtocol.DisassembledInstruction; - instructions.push(disInsn); - if (instructions.length === args.instructionCount) { - break outer_loop; - } + const memoryReference = + args.offset === undefined + ? args.memoryReference + : calculateMemoryOffset(args.memoryReference, args.offset); + + if (instructionStartOffset < 0) { + // Getting lower memory area + const list = await getInstructions( + this.gdb, + memoryReference, + instructionStartOffset + ); - const bytes = asmLine.opcodes.replace(/\s/g, ''); - startOffset += bytes.length; - } - } - } catch (err) { - // Failed to read instruction -- what best to do here? - // in other words, whose responsibility (adapter or client) - // to reissue reads in smaller chunks to find good memory - while (instructions.length < args.instructionCount) { - const badDisInsn = { - // TODO this should start at byte after last retrieved address - address: `0x${startOffset.toString(16)}`, - instruction: - err instanceof Error - ? err.message - : String(err), - } as DebugProtocol.DisassembledInstruction; - instructions.push(badDisInsn); - startOffset += 2; - } - break outer_loop; - } + // Add them to instruction list + instructions.push(...list); } - if (!args.endMemoryReference) { - while (instructions.length < args.instructionCount) { - const badDisInsn = { - // TODO this should start at byte after last retrieved address - address: `0x${startOffset.toString(16)}`, - instruction: 'failed to retrieve instruction', - } as DebugProtocol.DisassembledInstruction; - instructions.push(badDisInsn); - startOffset += 2; - } + if (instructionEndOffset > 0) { + // Getting higher memory area + const list = await getInstructions( + this.gdb, + memoryReference, + instructionEndOffset + ); + + // Add them to instruction list + instructions.push(...list); } - response.body = { instructions }; + response.body = { + instructions, + }; this.sendResponse(response); } catch (err) { - this.sendErrorResponse( - response, - 1, - err instanceof Error ? err.message : String(err) - ); + const message = err instanceof Error ? err.message : String(err); + this.sendEvent(new OutputEvent(`Error: ${message}`)); + this.sendErrorResponse(response, 1, message); } } diff --git a/src/integration-tests/diassemble.spec.ts b/src/integration-tests/diassemble.spec.ts index ed427ea6..05c55e2e 100644 --- a/src/integration-tests/diassemble.spec.ts +++ b/src/integration-tests/diassemble.spec.ts @@ -13,6 +13,7 @@ import * as path from 'path'; import { DebugProtocol } from '@vscode/debugprotocol/lib/debugProtocol'; import { CdtDebugClient } from './debugClient'; import { fillDefaults, standardBeforeEach, testProgramsDir } from './utils'; +import { assert } from 'sinon'; describe('Disassembly Test Suite', function () { let dc: CdtDebugClient; @@ -20,6 +21,28 @@ describe('Disassembly Test Suite', function () { const disProgram = path.join(testProgramsDir, 'disassemble'); const disSrc = path.join(testProgramsDir, 'disassemble.c'); + const expectsGeneralDisassemble = ( + disassemble: DebugProtocol.DisassembleResponse, + length: number, + ignoreEmptyInstructions?: boolean + ) => { + expect(disassemble).not.eq(undefined); + expect(disassemble.body).not.eq(undefined); + if (disassemble.body) { + const instructions = disassemble.body.instructions; + expect(instructions).to.have.lengthOf(length); + // the contents of the instructions are platform dependent, so instead + // make sure we have read fully + for (const i of instructions) { + expect(i.address).to.have.lengthOf.greaterThan(0); + expect(i.instruction).to.have.lengthOf.greaterThan(0); + if (!ignoreEmptyInstructions) { + expect(i.instructionBytes).to.have.lengthOf.greaterThan(0); + } + } + } + }; + beforeEach(async function () { dc = await standardBeforeEach(); @@ -60,19 +83,8 @@ describe('Disassembly Test Suite', function () { memoryReference: 'main', instructionCount: 100, })) as DebugProtocol.DisassembleResponse; - expect(disassemble).not.eq(undefined); - expect(disassemble.body).not.eq(undefined); - if (disassemble.body) { - const instructions = disassemble.body.instructions; - expect(instructions).to.have.lengthOf(100); - // the contents of the instructions are platform dependent, so instead - // make sure we have read fully - for (const i of instructions) { - expect(i.address).to.have.lengthOf.greaterThan(0); - expect(i.instructionBytes).to.have.lengthOf.greaterThan(0); - expect(i.instruction).to.have.lengthOf.greaterThan(0); - } - } + + expectsGeneralDisassemble(disassemble, 100); }); it('can disassemble with no source references', async function () { @@ -82,38 +94,102 @@ describe('Disassembly Test Suite', function () { memoryReference: 'main+1000', instructionCount: 100, })) as DebugProtocol.DisassembleResponse; - expect(disassemble).not.eq(undefined); - expect(disassemble.body).not.eq(undefined); - if (disassemble.body) { - const instructions = disassemble.body.instructions; - expect(instructions).to.have.lengthOf(100); - // the contents of the instructions are platform dependent, so instead - // make sure we have read fully - for (const i of instructions) { - expect(i.address).to.have.lengthOf.greaterThan(0); - expect(i.instructionBytes).to.have.lengthOf.greaterThan(0); - expect(i.instruction).to.have.lengthOf.greaterThan(0); - } - } + + expectsGeneralDisassemble(disassemble, 100); }); - it('can handle disassemble at bad address', async function () { + it('can disassemble with negative offsets', async function () { const disassemble = (await dc.send('disassemble', { - memoryReference: '0x0', - instructionCount: 10, - })) as DebugProtocol.DisassembleResponse; - expect(disassemble).not.eq(undefined); - expect(disassemble.body).not.eq(undefined); - if (disassemble.body) { - const instructions = disassemble.body.instructions; - expect(instructions).to.have.lengthOf(10); - // the contens of the instructions are platform dependent, so instead - // make sure we have read fully - for (const i of instructions) { - expect(i.address).to.have.lengthOf.greaterThan(0); - expect(i.instruction).to.have.lengthOf.greaterThan(0); - expect(i.instructionBytes).eq(undefined); - } + memoryReference: 'main', + instructionOffset: -20, + instructionCount: 20, + } as DebugProtocol.DisassembleArguments)) as DebugProtocol.DisassembleResponse; + + expectsGeneralDisassemble(disassemble, 20, true); + }); + + it('send error response handle on empty memory reference', async function () { + try { + await dc.send('disassemble', { + memoryReference: '', + instructionOffset: -20, + instructionCount: 20, + } as DebugProtocol.DisassembleArguments); + assert.fail('Should throw error!'); + } catch (e) { + expect(e).to.be.deep.equal( + new Error('Target memory reference is not specified!') + ); + } + }); + + it('can disassemble with correct boundries', async function () { + const get = ( + disassemble: DebugProtocol.DisassembleResponse, + offset: number + ) => { + const instruction = disassemble.body?.instructions[offset]; + expect(instruction).not.eq(undefined); + // Instruction undefined already checked. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return instruction!; + }; + + const expectsInstructionEquals = ( + instruction1: DebugProtocol.DisassembledInstruction, + instruction2: DebugProtocol.DisassembledInstruction, + message?: string + ) => { + expect(instruction1.address).to.eq(instruction2.address, message); + }; + + const disassembleLower = (await dc.send('disassemble', { + memoryReference: 'main', + instructionOffset: -20, + instructionCount: 20, + } as DebugProtocol.DisassembleArguments)) as DebugProtocol.DisassembleResponse; + const disassembleMiddle = (await dc.send('disassemble', { + memoryReference: 'main', + instructionOffset: -10, + instructionCount: 20, + } as DebugProtocol.DisassembleArguments)) as DebugProtocol.DisassembleResponse; + const disassembleHigher = (await dc.send('disassemble', { + memoryReference: 'main', + instructionOffset: 0, + instructionCount: 20, + } as DebugProtocol.DisassembleArguments)) as DebugProtocol.DisassembleResponse; + + expectsGeneralDisassemble(disassembleLower, 20, true); + expectsGeneralDisassemble(disassembleMiddle, 20, true); + expectsGeneralDisassemble(disassembleHigher, 20, true); + + // Current implementation have known edge cases, possibly instruction misaligning while + // handling the negative offsets, please refer to the discussion at the following link: + // https://github.com/eclipse-cdt-cloud/cdt-gdb-adapter/pull/341#discussion_r1857422980 + expectsInstructionEquals( + get(disassembleLower, 15), + get(disassembleMiddle, 5), + 'lower[15] should be same with middle[5]' + ); + + expectsInstructionEquals( + get(disassembleMiddle, 15), + get(disassembleHigher, 5), + 'middle[15] should be same with higher[5]' + ); + }); + + it('return error at bad address', async function () { + try { + await dc.send('disassemble', { + memoryReference: '0x0', + instructionCount: 10, + } as DebugProtocol.DisassembleArguments); + assert.fail('Should throw error!'); + } catch (e) { + expect(e).to.be.deep.equal( + new Error('Cannot access memory at address 0x0') + ); } }); }); diff --git a/src/integration-tests/util.spec.ts b/src/integration-tests/util.spec.ts index e826e3f2..d5fdf334 100644 --- a/src/integration-tests/util.spec.ts +++ b/src/integration-tests/util.spec.ts @@ -12,6 +12,13 @@ import { parseGdbVersionOutput } from '../util/parseGdbVersionOutput'; import { createEnvValues } from '../util/createEnvValues'; import { expect } from 'chai'; import * as os from 'os'; +import { calculateMemoryOffset } from '../util/calculateMemoryOffset'; +import { MIDataDisassembleAsmInsn } from '../mi'; +import { DebugProtocol } from '@vscode/debugprotocol'; +import { + getDisassembledInstruction, + getEmptyInstructions, +} from '../util/disassembly'; describe('util', async () => { it('compareVersions', async () => { @@ -140,3 +147,214 @@ describe('createEnvValues', () => { expect(result).to.deep.equals(expectedResult); }); }); + +describe('calculateMemoryOffset', () => { + it('should expect to calculate basic operations', () => { + expect(calculateMemoryOffset('0x0000ff00', 2)).to.eq('0x0000ff02'); + expect(calculateMemoryOffset('0x0000ff00', 8)).to.eq('0x0000ff08'); + expect(calculateMemoryOffset('0x0000ff00', 64)).to.eq('0x0000ff40'); + expect(calculateMemoryOffset('0x0000ff00', -2)).to.eq('0x0000fefe'); + expect(calculateMemoryOffset('0x0000ff00', -8)).to.eq('0x0000fef8'); + expect(calculateMemoryOffset('0x0000ff00', -64)).to.eq('0x0000fec0'); + }); + + it('should expect to handle 64bit address operations ', () => { + expect(calculateMemoryOffset('0x0000ff00', '0xff')).to.eq('0x0000ffff'); + expect(calculateMemoryOffset('0x0000ff00', '0x0100')).to.eq( + '0x00010000' + ); + }); + + it('should expect to handle reference address operations ', () => { + expect(calculateMemoryOffset('main', 2)).to.eq('main+2'); + expect(calculateMemoryOffset('main', -2)).to.eq('main-2'); + expect(calculateMemoryOffset('main+4', 6)).to.eq('main+10'); + expect(calculateMemoryOffset('main+4', -6)).to.eq('main-2'); + expect(calculateMemoryOffset('main+4', 6)).to.eq('main+10'); + expect(calculateMemoryOffset('main-4', -6)).to.eq('main-10'); + expect(calculateMemoryOffset('main-4', 6)).to.eq('main+2'); + }); + + it('should expect to handle 64bit address operations ', () => { + expect(calculateMemoryOffset('0xffeeddcc0000ff00', '0xff')).to.eq( + '0xffeeddcc0000ffff' + ); + expect(calculateMemoryOffset('0xffeeddcc0000ff00', '0x0100')).to.eq( + '0xffeeddcc00010000' + ); + expect( + calculateMemoryOffset('0xefeeddcc0000ff00', '0x10000000000000ff') + ).to.eq('0xffeeddcc0000ffff'); + expect( + calculateMemoryOffset('0xefeeddcc0000ff00', '0x1000000000000100') + ).to.eq('0xffeeddcc00010000'); + }); +}); + +describe('getDisassembledInstruction', () => { + it('should map properly', () => { + const asmInst: MIDataDisassembleAsmInsn = { + 'func-name': 'fn_test', + offset: '2', + address: '0x1fff', + inst: 'mov r10, r6', + opcodes: 'b2 46', + }; + const expected: DebugProtocol.DisassembledInstruction = { + address: '0x1fff', + instructionBytes: 'b2 46', + instruction: 'mov r10, r6', + symbol: 'fn_test+2', + }; + + const result = getDisassembledInstruction(asmInst); + expect(result).to.deep.equal(expected); + }); + it('should work without offset', () => { + const asmInst: MIDataDisassembleAsmInsn = { + 'func-name': 'fn_test', + address: '0x1fff', + inst: 'mov r10, r6', + opcodes: 'b2 46', + } as unknown as MIDataDisassembleAsmInsn; + const expected: DebugProtocol.DisassembledInstruction = { + address: '0x1fff', + instructionBytes: 'b2 46', + instruction: 'mov r10, r6', + symbol: 'fn_test', + }; + + const result = getDisassembledInstruction(asmInst); + expect(result).to.deep.equal(expected); + }); + + it('should work without function name', () => { + const asmInst: MIDataDisassembleAsmInsn = { + address: '0x1fff', + inst: 'mov r10, r6', + opcodes: 'b2 46', + } as unknown as MIDataDisassembleAsmInsn; + const expected: DebugProtocol.DisassembledInstruction = { + address: '0x1fff', + instructionBytes: 'b2 46', + instruction: 'mov r10, r6', + }; + + const result = getDisassembledInstruction(asmInst); + expect(result).to.deep.equal(expected); + }); +}); + +describe('getEmptyInstructions', () => { + it('should return forward instructions', () => { + const instructions = getEmptyInstructions('0x0000f000', 10, 4); + expect(instructions.length).to.eq(10); + instructions.forEach((instruction, ix) => { + expect(instruction.address).to.eq( + calculateMemoryOffset('0x0000f000', ix * 4) + ); + expect(instruction.instruction).to.eq( + 'failed to retrieve instruction' + ); + expect(instruction.presentationHint).to.eq('invalid'); + }); + }); + + it('should return reverse instructions', () => { + const instructions = getEmptyInstructions('0x0000f000', 10, -4); + expect(instructions.length).to.eq(10); + instructions.forEach((instruction, ix) => { + expect(instruction.address).to.eq( + calculateMemoryOffset('0x0000f000', ix * 4 - 40) + ); + expect(instruction.instruction).to.eq( + 'failed to retrieve instruction' + ); + expect(instruction.presentationHint).to.eq('invalid'); + }); + }); + + it('should return forward instructions with function reference', () => { + const instructions = getEmptyInstructions('main', 10, 4); + expect(instructions.length).to.eq(10); + instructions.forEach((instruction, ix) => { + expect(instruction.address).to.eq( + ix === 0 ? 'main' : calculateMemoryOffset('main', ix * 4) + ); + expect(instruction.instruction).to.eq( + 'failed to retrieve instruction' + ); + expect(instruction.presentationHint).to.eq('invalid'); + }); + }); + + it('should return reverse instructions with function reference', () => { + const instructions = getEmptyInstructions('main', 10, -4); + expect(instructions.length).to.eq(10); + instructions.forEach((instruction, ix) => { + expect(instruction.address).to.eq( + calculateMemoryOffset('main', ix * 4 - 40) + ); + expect(instruction.instruction).to.eq( + 'failed to retrieve instruction' + ); + expect(instruction.presentationHint).to.eq('invalid'); + }); + }); + + it('should return forward instructions with function reference and positive offset', () => { + const instructions = getEmptyInstructions('main+20', 10, 4); + expect(instructions.length).to.eq(10); + instructions.forEach((instruction, ix) => { + expect(instruction.address).to.eq( + calculateMemoryOffset('main+20', ix * 4) + ); + expect(instruction.instruction).to.eq( + 'failed to retrieve instruction' + ); + expect(instruction.presentationHint).to.eq('invalid'); + }); + }); + + it('should return reverse instructions with function reference and positive offset', () => { + const instructions = getEmptyInstructions('main+20', 10, -4); + expect(instructions.length).to.eq(10); + instructions.forEach((instruction, ix) => { + expect(instruction.address).to.eq( + calculateMemoryOffset('main+20', ix * 4 - 40) + ); + expect(instruction.instruction).to.eq( + 'failed to retrieve instruction' + ); + expect(instruction.presentationHint).to.eq('invalid'); + }); + }); + + it('should return forward instructions with function reference and negative offset', () => { + const instructions = getEmptyInstructions('main-20', 10, 4); + expect(instructions.length).to.eq(10); + instructions.forEach((instruction, ix) => { + expect(instruction.address).to.eq( + calculateMemoryOffset('main-20', ix * 4) + ); + expect(instruction.instruction).to.eq( + 'failed to retrieve instruction' + ); + expect(instruction.presentationHint).to.eq('invalid'); + }); + }); + + it('should return reverse instructions with function reference and negative offset', () => { + const instructions = getEmptyInstructions('main-20', 10, -4); + expect(instructions.length).to.eq(10); + instructions.forEach((instruction, ix) => { + expect(instruction.address).to.eq( + calculateMemoryOffset('main-20', ix * 4 - 40) + ); + expect(instruction.instruction).to.eq( + 'failed to retrieve instruction' + ); + expect(instruction.presentationHint).to.eq('invalid'); + }); + }); +}); diff --git a/src/mi/data.ts b/src/mi/data.ts index fa12bb5d..1e2c8af1 100644 --- a/src/mi/data.ts +++ b/src/mi/data.ts @@ -19,7 +19,7 @@ interface MIDataReadMemoryBytesResponse { contents: string; }>; } -interface MIDataDisassembleAsmInsn { +export interface MIDataDisassembleAsmInsn { address: string; // func-name in MI 'func-name': string; @@ -28,13 +28,13 @@ interface MIDataDisassembleAsmInsn { inst: string; } -interface MIDataDisassembleSrcAndAsmLine { +export interface MIDataDisassembleSrcAndAsmLine { line: string; file: string; fullname: string; line_asm_insn: MIDataDisassembleAsmInsn[]; } -interface MIDataDisassembleResponse { +export interface MIDataDisassembleResponse { asm_insns: MIDataDisassembleSrcAndAsmLine[]; } diff --git a/src/util/calculateMemoryOffset.ts b/src/util/calculateMemoryOffset.ts new file mode 100644 index 00000000..441f458b --- /dev/null +++ b/src/util/calculateMemoryOffset.ts @@ -0,0 +1,38 @@ +/********************************************************************* + * Copyright (c) 2024 Renesas Electronics Corporation and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ + +/** + * This method calculates the memory offset arithmetics on string hexadecimal address value + * + * @param address + * Reference address to perform the operation for example '0x0000FF00', 'main', 'main+200' + * @param offset + * Offset (in bytes) to be applied to the reference location before disassembling. Can be negative. + * @return + * Returns the calculated address. Keeping the address length same. + */ +export const calculateMemoryOffset = ( + address: string, + offset: string | number | bigint +): string => { + if (address.startsWith('0x')) { + const addressLength = address.length - 2; + const newAddress = BigInt(address) + BigInt(offset); + return `0x${newAddress.toString(16).padStart(addressLength, '0')}`; + } else { + const addrParts = /^([^+-]*)([+-]\d+)?$/g.exec(address); + const addrReference = addrParts?.[1]; + const addrOffset = BigInt(addrParts?.[2] ?? 0); + const calcOffset = BigInt(offset) + addrOffset; + return `${addrReference}${calcOffset < 0 ? '-' : '+'}${ + calcOffset < 0 ? -calcOffset : calcOffset + }`; + } +}; diff --git a/src/util/disassembly.ts b/src/util/disassembly.ts new file mode 100644 index 00000000..a6756f38 --- /dev/null +++ b/src/util/disassembly.ts @@ -0,0 +1,222 @@ +/********************************************************************* + * Copyright (c) 2024 Renesas Electronics Corporation and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ +import { DebugProtocol } from '@vscode/debugprotocol'; +import { MIDataDisassembleAsmInsn, sendDataDisassemble } from '../mi'; +import { IGDBBackend } from '../types/gdb'; +import { calculateMemoryOffset } from './calculateMemoryOffset'; + +/** + * Converts the MIDataDisassembleAsmInsn object to DebugProtocol.DisassembledInstruction + * + * @param asmInstruction + * MI instruction object + * @return + * Returns the DebugProtocol.DisassembledInstruction object + */ +export const getDisassembledInstruction = ( + asmInstruction: MIDataDisassembleAsmInsn +): DebugProtocol.DisassembledInstruction => { + let symbol: string | undefined; + if (asmInstruction['func-name'] && asmInstruction.offset) { + symbol = `${asmInstruction['func-name']}+${asmInstruction.offset}`; + } else if (asmInstruction['func-name']) { + symbol = asmInstruction['func-name']; + } else { + symbol = undefined; + } + return { + address: asmInstruction.address, + instructionBytes: asmInstruction.opcodes, + instruction: asmInstruction.inst, + ...(symbol ? { symbol } : {}), + } as DebugProtocol.DisassembledInstruction; +}; + +/** + * Returns a sequence of empty instructions to fill the gap in DisassembleRequest + * + * @param startAddress + * The starting address of the sequence + * @param count + * The number of the instructions to return back + * @param step + * Memory step to calculate the next instructions address. It can be negative. + * @return + * Returns sequence of empty instructions + */ +export const getEmptyInstructions = ( + startAddress: string, + count: number, + step: number +) => { + const badDisInsn = ( + address: string + ): DebugProtocol.DisassembledInstruction => ({ + address, + instruction: 'failed to retrieve instruction', + presentationHint: 'invalid', + }); + + const list: DebugProtocol.DisassembledInstruction[] = []; + let address = startAddress; + for (let ix = 0; ix < count; ix++) { + if (step < 0) { + address = calculateMemoryOffset(address, step); + list.unshift(badDisInsn(address)); + } else { + list.push(badDisInsn(address)); + address = calculateMemoryOffset(address, step); + } + } + return list; +}; + +/** + * Gets the instructions from the memory according to the given reference values. + * + * For example: + * If you like to return 100 instructions starting from the 0x00001F00 address, + * you can use the method like below: + * + * const instructions = await memoryReference('0x00001F00', 100); + * + * To return lower memory areas, (handling the negative offset), + * you can use negative length value: + * + * const instructions = await memoryReference('0x00001F00', -100); + * + * Method returns the expected length of the instructions, if cannot read expected + * length (can be due to memory bounds), empty instructions will be filled. + * + * @param gdb + * GDB Backend instance + * @param memoryReference + * Starting memory address for the operation + * @param length + * The count of the instructions to fetch, can be negative if wanted to return negative offset + * @return + * Returns the given amount of instructions + */ +export const getInstructions = async ( + gdb: IGDBBackend, + memoryReference: string, + length: number +) => { + const list: DebugProtocol.DisassembledInstruction[] = []; + const meanSizeOfInstruction = 4; + const isReverseFetch = length < 0; + const absLength = Math.abs(length); + + const formatMemoryAddress = (offset: number) => { + return `(${memoryReference})${offset < 0 ? '-' : '+'}${Math.abs( + offset + )}`; + }; + + const sendDataDisassembleWrapper = async (lower: number, upper: number) => { + const list: DebugProtocol.DisassembledInstruction[] = []; + + const result = await sendDataDisassemble( + gdb, + formatMemoryAddress(lower), + formatMemoryAddress(upper) + ); + for (const asmInsn of result.asm_insns) { + const line: number | undefined = asmInsn.line + ? parseInt(asmInsn.line, 10) + : undefined; + const location = { + name: asmInsn.file, + path: asmInsn.fullname, + } as DebugProtocol.Source; + for (const asmLine of asmInsn.line_asm_insn) { + list.push({ + ...getDisassembledInstruction(asmLine), + location, + line, + }); + } + } + return list; + }; + + const target = { lower: 0, higher: 0 }; + const recalculateTargetBounds = (length: number) => { + if (isReverseFetch) { + target.higher = target.lower; + target.lower += length * meanSizeOfInstruction; + } else { + target.lower = target.higher; + target.higher += length * meanSizeOfInstruction; + } + }; + const remainingLength = () => + Math.sign(length) * Math.max(absLength - list.length, 0); + const pushToList = ( + instructions: DebugProtocol.DisassembledInstruction[] + ) => { + if (isReverseFetch) { + list.unshift(...instructions); + } else { + list.push(...instructions); + } + }; + try { + while (absLength > list.length) { + recalculateTargetBounds(remainingLength()); + const result = await sendDataDisassembleWrapper( + target.lower, + target.higher + ); + if (result.length === 0) { + // If cannot retrieve more instructions, break the loop, go to catch + // and fill the remaining instructions with empty instruction information + break; + } + pushToList(result); + } + } catch (e) { + // If error occured in the first iteration and no items can be read + // throw the original error, otherwise continue and fill the empty instructions. + if (list.length === 0) { + throw e; + } + } + + if (absLength < list.length) { + if (length < 0) { + // Remove the heading, if necessary + list.splice(0, list.length - absLength); + } else { + // Remove the tail, if necessary + list.splice(absLength, list.length - absLength); + } + } + + // Fill with empty instructions in case couldn't read desired length + if (absLength > list.length) { + if (list.length === 0) { + // In case of memory read error, where no instructions read before you cannot be sure about the memory offsets + // Avoid sending empty instructions, which is overriding the previous disassembled instructions in the VSCode + // Instead, send error message and fail the request. + throw new Error(`Cannot retrieve instructions!`); + } + const lastMemoryAddress = + list[isReverseFetch ? 0 : list.length - 1].address; + const emptyInstuctions = getEmptyInstructions( + lastMemoryAddress, + absLength - list.length, + Math.sign(length) * 2 + ); + pushToList(emptyInstuctions); + } + + return list; +}; diff --git a/yarn.lock b/yarn.lock index fcaca53e..24b2389f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -790,18 +790,23 @@ dependencies: "@vscode/debugprotocol" "1.59.0" -"@vscode/debugadapter@^1.59.0": - version "1.59.0" - resolved "https://registry.yarnpkg.com/@vscode/debugadapter/-/debugadapter-1.59.0.tgz#ed89afe9b50e28f81c642e635634076f9ca8b6d4" - integrity sha512-KfrQ/9QhTxBumxkqIWs9rsFLScdBIqEXx5pGbTXP7V9I3IIcwgdi5N55FbMxQY9tq6xK3KfJHAZLIXDwO7YfVg== +"@vscode/debugadapter@^1.68.0": + version "1.68.0" + resolved "https://registry.yarnpkg.com/@vscode/debugadapter/-/debugadapter-1.68.0.tgz#abb23463cb750ca4a6f0834c5d4db659258dc159" + integrity sha512-D6gk5Fw2y4FV8oYmltoXpj+VAZexxJFopN/mcZ6YcgzQE9dgq2L45Aj3GLxScJOD6GeLILcxJIaA8l3v11esGg== dependencies: - "@vscode/debugprotocol" "1.59.0" + "@vscode/debugprotocol" "1.68.0" -"@vscode/debugprotocol@1.59.0", "@vscode/debugprotocol@^1.59.0": +"@vscode/debugprotocol@1.59.0": version "1.59.0" resolved "https://registry.yarnpkg.com/@vscode/debugprotocol/-/debugprotocol-1.59.0.tgz#f173ff725f60e4ff1002f089105634900c88bd77" integrity sha512-Ks8NiZrCvybf9ebGLP8OUZQbEMIJYC8X0Ds54Q/szpT/SYEDjTksPvZlcWGTo7B9t5abjvbd0jkNH3blYaSuVw== +"@vscode/debugprotocol@1.68.0", "@vscode/debugprotocol@^1.68.0": + version "1.68.0" + resolved "https://registry.yarnpkg.com/@vscode/debugprotocol/-/debugprotocol-1.68.0.tgz#e558ba6affe1be7aff4ec824599f316b61d9a69d" + integrity sha512-2J27dysaXmvnfuhFGhfeuxfHRXunqNPxtBoR3koiTOA9rdxWNDTa1zIFLCFMSHJ9MPTPKFcBeblsyaCJCIlQxg== + JSONStream@^1.0.3: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"