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"