diff --git a/.gitignore b/.gitignore index 1262404..b3eb14f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ out node_modules *.vsix -.vscode-test \ No newline at end of file +.vscode-test +package-lock.json diff --git a/package-lock.json b/package-lock.json index 416ec04..bf39802 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,18 @@ { "name": "debug", - "version": "0.26.0", + "version": "0.26.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "debug", - "version": "0.26.0", + "version": "0.26.1", "license": "public domain", "dependencies": { + "json-stream-stringify": "^2.0.4", + "node-interval-tree": "^1.3.3", "ssh2": "^1.6.0", + "stream-json": "^1.7.3", "vscode-debugadapter": "^1.45.0", "vscode-debugprotocol": "^1.45.0" }, @@ -719,6 +722,11 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-stream-stringify": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/json-stream-stringify/-/json-stream-stringify-2.0.4.tgz", + "integrity": "sha512-gIPoa6K5w6j/RnQ3fOtmvICKNJGViI83A7dnTIL+0QJ/1GKuNvCPFvbFWxt0agruF4iGgDFJvge4Gua4ZoiggQ==" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -977,6 +985,17 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-interval-tree": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/node-interval-tree/-/node-interval-tree-1.3.3.tgz", + "integrity": "sha512-K9vk96HdTK5fEipJwxSvIIqwTqr4e3HRJeJrNxBSeVMNSC/JWARRaX7etOLOuTmrRMeOI/K5TCJu3aWIwZiNTw==", + "dependencies": { + "shallowequal": "^1.0.2" + }, + "engines": { + "node": ">= 7.6.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1159,6 +1178,11 @@ "randombytes": "^2.1.0" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -1182,6 +1206,19 @@ "nan": "^2.15.0" } }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmmirror.com/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==" + }, + "node_modules/stream-json": { + "version": "1.7.4", + "resolved": "https://registry.npmmirror.com/stream-json/-/stream-json-1.7.4.tgz", + "integrity": "sha512-ja2dde1v7dOlx5/vmavn8kLrxvNfs7r2oNc5DYmNJzayDDdudyCSuTB1gFjH4XBVTIwxiMxL4i059HX+ZiouXg==", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2053,6 +2090,11 @@ "esprima": "^4.0.0" } }, + "json-stream-stringify": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/json-stream-stringify/-/json-stream-stringify-2.0.4.tgz", + "integrity": "sha512-gIPoa6K5w6j/RnQ3fOtmvICKNJGViI83A7dnTIL+0QJ/1GKuNvCPFvbFWxt0agruF4iGgDFJvge4Gua4ZoiggQ==" + }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2243,6 +2285,14 @@ "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", "dev": true }, + "node-interval-tree": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/node-interval-tree/-/node-interval-tree-1.3.3.tgz", + "integrity": "sha512-K9vk96HdTK5fEipJwxSvIIqwTqr4e3HRJeJrNxBSeVMNSC/JWARRaX7etOLOuTmrRMeOI/K5TCJu3aWIwZiNTw==", + "requires": { + "shallowequal": "^1.0.2" + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2369,6 +2419,11 @@ "randombytes": "^2.1.0" } }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -2386,6 +2441,19 @@ "nan": "^2.15.0" } }, + "stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmmirror.com/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==" + }, + "stream-json": { + "version": "1.7.4", + "resolved": "https://registry.npmmirror.com/stream-json/-/stream-json-1.7.4.tgz", + "integrity": "sha512-ja2dde1v7dOlx5/vmavn8kLrxvNfs7r2oNc5DYmNJzayDDdudyCSuTB1gFjH4XBVTIwxiMxL4i059HX+ZiouXg==", + "requires": { + "stream-chain": "^2.2.5" + } + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", diff --git a/package.json b/package.json index 1cf66b9..d3cee76 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "debug" ], "license": "public domain", - "version": "0.26.0", + "version": "0.26.1", "publisher": "webfreak", "icon": "images/icon.png", "engines": { @@ -1049,7 +1049,10 @@ "dependencies": { "ssh2": "^1.6.0", "vscode-debugadapter": "^1.45.0", - "vscode-debugprotocol": "^1.45.0" + "vscode-debugprotocol": "^1.45.0", + "node-interval-tree": "^1.3.3", + "json-stream-stringify": "^2.0.4", + "stream-json": "^1.7.3" }, "devDependencies": { "@types/mocha": "^5.2.6", diff --git a/src/backend/backend.ts b/src/backend/backend.ts index 10532a4..ebb579a 100644 --- a/src/backend/backend.ts +++ b/src/backend/backend.ts @@ -11,6 +11,11 @@ export interface Breakpoint { countCondition?: string; } +export interface OurInstructionBreakpoint extends DebugProtocol.InstructionBreakpoint { + address: number; + number: number; +} + export interface Thread { id: number; targetId: string; @@ -64,19 +69,23 @@ export interface IBackend { stepOut(): Thenable; loadBreakPoints(breakpoints: Breakpoint[]): Thenable<[boolean, Breakpoint][]>; addBreakPoint(breakpoint: Breakpoint): Thenable<[boolean, Breakpoint]>; + addInstrBreakPoint(breakpoint: OurInstructionBreakpoint): Promise; removeBreakPoint(breakpoint: Breakpoint): Thenable; clearBreakPoints(source?: string): Thenable; getThreads(): Thenable; getStack(startFrame: number, maxLevels: number, thread: number): Thenable; + getRegisters(): Promise; getStackVariables(thread: number, frame: number): Thenable; - evalExpression(name: string, thread: number, frame: number): Thenable; + evalExpression(name: string, thread: number, frame: number, suppressFailure:boolean): Thenable; isReady(): boolean; changeVariable(name: string, rawValue: string): Thenable; examineMemory(from: number, to: number): Thenable; + record(): Thenable; } export class VariableObject { name: string; + evaluateName:string; exp: string; numchild: number; type: string; @@ -89,6 +98,7 @@ export class VariableObject { id: number; constructor(node: any) { this.name = MINode.valueOf(node, "name"); + this.evaluateName = this.name; this.exp = MINode.valueOf(node, "exp"); this.numchild = parseInt(MINode.valueOf(node, "numchild")); this.type = MINode.valueOf(node, "type"); diff --git a/src/backend/common.ts b/src/backend/common.ts new file mode 100644 index 0000000..25e8058 --- /dev/null +++ b/src/backend/common.ts @@ -0,0 +1,369 @@ +import { DebugProtocol } from 'vscode-debugprotocol'; +import * as childProcess from 'child_process'; +import { EventEmitter } from 'events'; +import * as stream from 'stream'; +import * as fs from 'fs'; +const readline = require('readline'); +import { SSHArguments, ValuesFormattingMode } from './backend'; + +export interface DisassemblyInstruction { + address: string; + functionName: string; + offset: number; + instruction: string; + opcodes: string; +} + +export enum ADAPTER_DEBUG_MODE { + NONE = 'none', + PARSED = 'parsed', + BOTH = 'both', + RAW = 'raw', + VSCODE = 'vscode' +} + +export interface ElfSection { + name: string; + address: number; // New base address + addressOrig: number; // original base address in Elf file +} +export interface SymbolFile { + file: string; + offset?: number; + textaddress?: number; + sections: ElfSection[]; + sectionMap: {[name: string]: ElfSection}; +} +export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { + cwd: string; + target: string; + gdbpath: string; + env: any; + //objdumpPath:string; + //symbolFiles: SymbolFile[]; + debugger_args: string[]; + pathSubstitutions: { [index: string]: string }; + arguments: string; + terminal: string; + autorun: string[]; + stopAtEntry: boolean | string; + ssh: SSHArguments; + valuesFormatting: ValuesFormattingMode; + printCalls: boolean; + showDevDebugOutput: boolean; +} + +export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments { + cwd: string; + target: string; + gdbpath: string; + env: any; + //objdumpPath:string; + //symbolFiles: SymbolFile[]; + debugger_args: string[]; + pathSubstitutions: { [index: string]: string }; + executable: string; + remote: boolean; + autorun: string[]; + stopAtConnect: boolean; + stopAtEntry: boolean | string; + ssh: SSHArguments; + valuesFormatting: ValuesFormattingMode; + printCalls: boolean; + showDevDebugOutput: boolean; +} + +// Helper function to create a symbolFile object properly with required elements +export function defSymbolFile(file: string): SymbolFile { + const ret: SymbolFile = { + file: file, + sections: [], + sectionMap: {} + }; + return ret; +} + +export function hexFormat(value: number, padding: number = 8, includePrefix: boolean = true): string { + let base = (value).toString(16); + base = base.padStart(padding, '0'); + return includePrefix ? '0x' + base : base; +} + +export interface ConfigurationArguments extends DebugProtocol.LaunchRequestArguments { + name: string; + request: string; + toolchainPath: string; + toolchainPrefix: string; + executable: string; + servertype: string; + serverpath: string; + gdbPath: string; + objdumpPath: string; + serverArgs: string[]; + serverCwd: string; + device: string; + loadFiles: string[]; + symbolFiles: SymbolFile[]; + debuggerArgs: string[]; + preLaunchCommands: string[]; + postLaunchCommands: string[]; + overrideLaunchCommands: string[]; + preAttachCommands: string[]; + postAttachCommands: string[]; + overrideAttachCommands: string[]; + preRestartCommands: string[]; + postRestartCommands: string[]; + overrideRestartCommands: string[]; + postStartSessionCommands: string[]; + postRestartSessionCommands: string[]; + overrideGDBServerStartedRegex: string; + breakAfterReset: boolean; + //svdFile: string; + //svdAddrGapThreshold: number; + //ctiOpenOCDConfig: CTIOpenOCDConfig; + //rttConfig: RTTConfiguration; + //swoConfig: SWOConfiguration; + graphConfig: any[]; + /// Triple slashes will cause the line to be ignored by the options-doc.py script + /// We don't expect the following to be in booleann form or have the value of 'none' after + /// The config provider has done the conversion. If it exists, it means output 'something' + showDevDebugOutput: ADAPTER_DEBUG_MODE; + showDevDebugTimestamps: boolean; + cwd: string; + extensionPath: string; + rtos: string; + //interface: 'jtag' | 'swd' | 'cjtag'; + targetId: string | number; + runToMain: boolean; // Deprecated: kept here for backwards compatibility + runToEntryPoint: string; + registerUseNaturalFormat: boolean; + variableUseNaturalFormat: boolean; + //chainedConfigurations: ChainedConfigurations; + + // pvtRestartOrReset: boolean; + // pvtPorts: { [name: string]: number; }; + // pvtParent: ConfigurationArguments; + // pvtMyConfigFromParent: ChainedConfig; // My configuration coming from the parent + // pvtAvoidPorts: number[]; + // pvtVersion: string; // Version from package.json + + numberOfProcessors: number; + targetProcessor: number; + + + // QEMU Specific + cpu: string; + machine: string; + + // External + gdbTarget: string; +} + + +export class HrTimer { + private start: bigint; + constructor() { + this.start = process.hrtime.bigint(); + } + + public restart(): void { + this.start = process.hrtime.bigint(); + } + + public getStart(): bigint { + return this.start; + } + + public deltaNs(): string { + return (process.hrtime.bigint() - this.start).toString(); + } + + public deltaUs(): string { + return this.toStringWithRes(3); + } + + public deltaMs(): string { + return this.toStringWithRes(6); + } + + public createPaddedMs(padding: number): string { + const hrUs = this.deltaMs().padStart(padding, '0'); + // const hrUsPadded = (hrUs.length < padding) ? '0'.repeat(padding - hrUs.length) + hrUs : '' + hrUs ; + // return hrUsPadded; + return hrUs; + } + + public createDateTimestamp(): string { + const hrUs = this.createPaddedMs(6); + const date = new Date(); + const ret = `[${date.toISOString()}, +${hrUs}ms]`; + return ret; + } + + private toStringWithRes(res: number) { + const diff = process.hrtime.bigint() - this.start + BigInt((10 ** res) / 2); + let ret = diff.toString(); + ret = ret.length <= res ? '0' : ret.substr(0, ret.length - res); + return ret; + } +} + +// +// For callback `cb`, fatal = false when file exists but header does not match. fatal = true means +// we could not even read the file. Use `cb` to print what ever messages you want. It is optional. +// +// Returns true if the ELF header match the elf magic number, false in all other cases +// +export function validateELFHeader(exe: string, cb?: (str: string, fatal: boolean) => void): boolean { + try { + if (!fs.existsSync(exe)) { + if (cb) { + cb(`File not found "executable": "${exe}"`, true); + } + return false; + } + const buffer = Buffer.alloc(16); + const fd = fs.openSync(exe, 'r'); + const n = fs.readSync(fd, buffer, 0, 16, 0); + fs.closeSync(fd); + if (n !== 16) { + if (cb) { + cb(`Could not read 16 bytes from "executable": "${exe}"`, true); + } + return false; + } + // First four chars are 0x7f, 'E', 'L', 'F' + if ((buffer[0] !== 0x7f) || (buffer[1] !== 0x45) || (buffer[2] !== 0x4c) || (buffer[3] !== 0x46)) { + if (cb) { + cb(`Not a valid ELF file "executable": "${exe}". Many debug functions can fail or not work properly`, false); + } + return false; + } + return true; + } + catch (e) { + if (cb) { + cb(`Could not read file "executable": "${exe}" ${e ? e.toString() : ''}`, true); + } + return false; + } +} + +// +// You have two choices. +// 1. Get events that you subscribe to or +// 2. get immediate callback and you will not get events +// +// There are three events +// emit('error', err) -- only emit +// emit('close') and cb(null) +// emit('line', line) or cb(line) -- NOT both, line can be empty '' +// emit('exit', code, signal) -- Only emit, NA for a stream Readable +// +// Either way, you will get a promise though. On Error though no rejection is issued and instead, it will +// emit and error and resolve to false +// +// You can chose to change the callback anytime -- perhaps based on the state of your parser. The +// callback has to return true to continue reading or false to end reading +// +// On exit for program, you only get an event. No call back. +// +// Why? Stuff like objdump/nm can produce very large output and reading them into a mongo +// string is a disaster waiting to happen. It is slow and will fail at some point. On small +// output, it may be faster but not on large ones. Tried using temp files but that was also +// slow. In this mechanism we use streams and NodeJS readline to hook things up and read +// things line at a time. Most of that kind of output needs to be parsed line at a time anyways +// +// Another benefit was we can run two programs at the same time and get the output of both in +// the same time as running just one. NodeJS is amazing juggling stuff and although not-multi threaded +// it almost look like it +// +// Finally, you can also use a file or a stream to read instead of a program to run. +// +export class SpawnLineReader extends EventEmitter { + public callback: (line: string) => boolean; + private promise: Promise; + constructor() { + super(); + } + + public startWithProgram( + prog: string, args: readonly string[] = [], + spawnOpts: childProcess.SpawnOptions = {}, cb: (line: string) => boolean = null): Promise { + if (this.promise) { throw new Error('SpawnLineReader: can\'t reuse this object'); } + this.callback = cb; + this.promise = new Promise((resolve) => { + try { + const child = childProcess.spawn(prog, args, spawnOpts); + child.on('error', (err) => { + this.emit('error', err); + resolve(false); + }); + child.on('exit', (code: number, signal: string) => { + this.emit('exit', code, signal); + // read-line will resolve. Not us + }); + this.doReadline(child.stdout, resolve); + } + catch (e) { + this.emit('error', e); + } + }); + return this.promise; + } + + public startWithStream(rStream: stream.Readable, cb: (line: string) => boolean = null): Promise { + if (this.promise) { throw new Error('SpawnLineReader: can\'t reuse this object'); } + this.callback = cb; + this.promise = new Promise((resolve) => { + this.doReadline(rStream, resolve); + }); + return this.promise; + } + + public startWithFile(filename: fs.PathLike, options: string | any = null, cb: (line: string, err?: any) => boolean = null): Promise { + if (this.promise) { throw new Error('SpawnLineReader: can\'t reuse this object'); } + this.callback = cb; + this.promise = new Promise((resolve) => { + const readStream = fs.createReadStream(filename, options || {flags: 'r'}); + readStream.on('error', ((e) => { + this.emit('error', e); + resolve(false); + })); + readStream.on('open', (() => { + this.doReadline(readStream, resolve); + })); + }); + return this.promise; + } + + private doReadline(rStream: stream.Readable, resolve) { + try { + const rl = readline.createInterface({ + input: rStream, + crlfDelay: Infinity, + console: false + }); + rl.on('line', (line) => { + if (this.callback) { + if (!this.callback(line)) { + rl.close(); + } + } else { + this.emit('line', line); + } + }); + rl.once('close', () => { + if (this.callback) { + this.callback(null); + } + rStream.destroy(); + this.emit('close'); + resolve(true); + }); + } + catch (e) { + this.emit('error', e); + } + } +} \ No newline at end of file diff --git a/src/backend/disasm.ts b/src/backend/disasm.ts new file mode 100644 index 0000000..700a0da --- /dev/null +++ b/src/backend/disasm.ts @@ -0,0 +1,1050 @@ +import { Source } from 'vscode-debugadapter'; +import { DebugProtocol } from 'vscode-debugprotocol'; +//import { hexFormat } from '../frontend/utils'; +import { MI2, parseReadMemResults } from './mi2/mi2'; +import { MINode } from './mi_parse'; +import * as path from 'path'; +import { GDBDebugSession } from '../gdb'; +import { hexFormat } from './common'; +import { DisassemblyInstruction, LaunchRequestArguments, AttachRequestArguments, ADAPTER_DEBUG_MODE, HrTimer } from './common'; +import { SymbolInformation, SymbolType, MemoryRegion,SymbolNode } from '../backend/symbols'; +import { assert } from 'console'; + +enum TargetArchitecture { + X64, X86, ARM64, ARM, XTENSA, UNKNOWN +} + + +/* +** We currently have two disassembler interfaces. One that follows the DAP protocol and VSCode is the client +** for it. The other is the original that works on a function at a time and the client is our own extension. +** The former is new and unproven but has more features and not mature even for VSCode. The latter is more +** mature and limited in functionality +*/ +interface ProtocolInstruction extends DebugProtocol.DisassembledInstruction { + pvtAddress: number; + pvtInstructionBytes?: string; + pvtIsData?: boolean; +} + +interface DisasmRange { + qStart: number; + qEnd: number; + verify: number; + isKnownStart: boolean; // Set to true if this is a range has a known good start address + symNode?: SymbolNode; // Not really used. debugging aid +} + +interface DisasmRequest { + response: DebugProtocol.DisassembleResponse; + args: DebugProtocol.DisassembleArguments; + request?: DebugProtocol.Request; + resolve: any; + reject: any; +} + + + +class InstructionRange { + public startAddress: number; // Inclusive + public endAddress: number; // Exclusive + // Definition of start and end to be consistent with gdb + constructor( + public instructions: ProtocolInstruction[]) + { + this.instructions = Array.from(instructions); // Make a shallow copy + this.adjustBoundaries(); + } + + private adjustBoundaries() { + const last = this.instructions.length > 0 ? this.instructions[this.instructions.length - 1] : null; + if (last) { + // this.startAddress = Math.min(this.startAddress, this.instructions[0].pvtAddress); + // this.endAddress = Math.max(this.endAddress, last.pvtAddress + (last ? last.pvtInstructionBytes.length / 2 : 2)); + this.startAddress = this.instructions[0].pvtAddress; + assert((last.pvtInstructionBytes.length % 3) === 2); + this.endAddress = last.pvtAddress + (last.pvtInstructionBytes.length + 1) / 3; + } + } + + public get span(): number { + return this.endAddress - this.startAddress; + } + + public isInsideRange(startAddr: number, endAddr: number) { + if ((startAddr >= this.startAddress) && (endAddr <= this.endAddress)) { + return true; + } + return false; + } + + public isOverlappingRange(startAddress: number, endAddress: number) { // Touching is overlapping + const length = endAddress - startAddress; + const s = Math.min(this.startAddress, startAddress); + const e = Math.max(this.endAddress, endAddress); + const l = e - s; + if (l > (this.span + length)) { + // combined length is greather than the sum of two lengths + return false; + } + return true; + } + + public findInstrIndex(address: number): number { + const len = this.instructions.length; + for (let ix = 0; ix < len ; ix++ ) { + const instr = this.instructions[ix]; + if (instr.pvtAddress === address) { + return ix; + } else if (instr.pvtIsData) { + const endAddr = instr.pvtAddress + ((instr.pvtInstructionBytes.length + 1) / 3); + if ((address >= instr.pvtAddress) && (address < endAddr)) { + return ix; + } + } + } + return -1; + } + + public findNearbyLowerInstr(address: number, thresh: number): number { + const lowerAddress = Math.max(0, address - thresh); + for (let ix = this.instructions.length - 1; ix > 0 ; ix-- ) { + const instrAddr = this.instructions[ix].pvtAddress; + if ((instrAddr >= lowerAddress) && (instrAddr <= address)) { + return instrAddr; + } + } + return address; + } + + public tryMerge(other: InstructionRange): boolean { + if (!this.isOverlappingRange(other.startAddress, other.endAddress)) { + return false; + } + + // See if totally overlapping or adjacent + if ((this.span === other.span) && (this.startAddress === other.startAddress)) { + return true; // They are identical + } else if (this.endAddress === other.startAddress) { // adjacent at end of this + this.instructions = this.instructions.concat(other.instructions); + this.adjustBoundaries(); + return true; + } else if (other.endAddress === this.startAddress) { // adjacent at end of other + this.instructions = other.instructions.concat(this.instructions); + this.adjustBoundaries(); + return true; + } + + // They partially overlap + const left = (this.startAddress <= other.startAddress) ? this : other; + const right = (this.startAddress <= other.startAddress) ? other : this; + const lx = left.instructions.length - 1; + const leftEnd = left.instructions[lx].pvtAddress; + const numRight = right.instructions.length; + for (let ix = 0; ix < numRight; ix++) { + if (right.instructions[ix].pvtAddress === leftEnd) { + const rInstrs = right.instructions.slice(ix + 1); + left.instructions = left.instructions.concat(rInstrs); + + // Almost like a new item but modify in place + this.instructions = left.instructions; + this.adjustBoundaries(); + if (GdbDisassembler.debug) { + console.log('Merge @', this.instructions[lx - 1], this.instructions[lx], this.instructions[lx + 1]); + } + return true; + } + } + // start/end addresses are original search ranges. According to that, the ranges overlap. But + // the actual instructions may not overlap or even abut + return false; + } + + public shallowCopy(): InstructionRange { + return new InstructionRange(this.instructions); + } + + public forceMerge(other: InstructionRange) { + if (this.tryMerge(other)) { + return; + } + if (this.startAddress < other.startAddress) { + this.instructions = this.instructions.concat(other.instructions); + } else { + this.instructions = other.instructions.concat(this.instructions); + } + this.adjustBoundaries(); + } +} + +class DisassemblyReturn { + constructor(public instructions: ProtocolInstruction[], public foundAt: number, makeCopy = true) { + // We only want to return a copy so the caches are not corrupted + this.instructions = makeCopy ? Array.from(this.instructions) : this.instructions; + } +} + +export class GdbDisassembler { + public static debug: boolean = false; // TODO: Remove this once stable. Merge with showDevDebugOutput + public doTiming = true; + + public Architecture = TargetArchitecture.ARM; + private maxInstrSize = 4; // We only support ARM devices and that too 32-bit. But we got users with RISC, so need to check + private minInstrSize = 2; + private instrMultiple = 2; // granularity of instruction sizes, used to increment/decrement startAddr looking for instr. alignment + private isGetArch = false; + private cache: InstructionRange[] = []; + public memoryRegions: MemoryRegion[]; + private gdb_miDebugger: MI2; + private gdbSession: GDBDebugSession = null; + constructor(gdbSession: GDBDebugSession, showDevDebugOutput:boolean = false) { + if (showDevDebugOutput) { + GdbDisassembler.debug = true; // Can't turn it off, once enabled. Intentional + } + this.gdb_miDebugger = gdbSession.get_miDebugger(); + this.gdbSession = gdbSession; + } + + private handleMsg(type: string, str: string) { + this.gdbSession.handleMsg(type, str); + } + + // protected isRangeInValidMem(startAddress: number, endAddress: number): boolean { + // for (const region of this.gdbSession.symbolTable.memoryRegions) { + // if (region.inVmaRegion(startAddress) && region.inVmaRegion(endAddress)) { + // return true; + // } else if (region.inLmaRegion(startAddress) && region.inLmaRegion(endAddress)) { + // return true; + // } + // } + // return false; + // } + + // protected isValidAddr(addr: number) { + // for (const region of this.gdbSession.symbolTable.memoryRegions) { + // if (region.inVmaRegion(addr) || region.inLmaRegion(addr)) { + // return true; + // } + // } + // return false; + // } + + protected getMemFlagForAddr(addr: number) { + //return this.isValidAddr(addr) ? '' : '?? '; + } + + public async setArchitecture(): Promise { + const miNode:MINode = await this.gdb_miDebugger.sendCommand('interpreter-exec console "show architecture"', false); + const str = miNode.output; + let found = false; + if (!str){ + this.handleMsg('stderr', "Info: Untested architecture for disassembly: Gdb command \"show architecture\" shows it."); + return; + } + // Some of this copied from MIEngine. Of course nothing other Arm-32 was tested + for (const line of str.toLowerCase().split('\n')) { + if (line.includes('x86-64')) { + this.Architecture = TargetArchitecture.X64; + this.minInstrSize = 1; + this.maxInstrSize = 26; + this.instrMultiple = 1; + } else if (line.includes('i386')) { + this.Architecture = TargetArchitecture.X86; + this.minInstrSize = 1; + this.maxInstrSize = 20; + this.instrMultiple = 1; + } else if (line.includes('arm64')) { + this.Architecture = TargetArchitecture.ARM64; + this.minInstrSize = 2; + this.maxInstrSize = 8; + this.instrMultiple = 2; + } else if (line.includes('aarch64')) { + this.Architecture = TargetArchitecture.ARM64; + this.minInstrSize = 2; + this.maxInstrSize = 8; + this.instrMultiple = 2; + } else if (line.includes('arm')) { + this.Architecture = TargetArchitecture.ARM; + this.minInstrSize = 2; + this.maxInstrSize = 4; + this.instrMultiple = 2; + } else if (line.includes('xtensa')) { + this.Architecture = TargetArchitecture.XTENSA; + this.minInstrSize = 1; + this.maxInstrSize = 128 / 8; // Yes, ridiculously large due to their long instructions + this.instrMultiple = 1; + } else { + continue; + } + found = true; + this.isGetArch = true; + break; + } + if (!found) { + this.handleMsg('log', 'Warning: Unknown architecture for disassembly. Results may not be accurate at edge of memories\n' + + ` Gdb command "show architecture" shows "${str}"\n`); + this.Architecture = TargetArchitecture.UNKNOWN; + this.minInstrSize = 1; + this.maxInstrSize = 26; + this.instrMultiple = 1; + } else if (this.Architecture !== TargetArchitecture.ARM) { + this.handleMsg('log', `Info: Untested architecture for disassembly: Gdb command "show architecture" shows "${str}"\n`); + } + } + + private async getMemoryRegions() { + if (this.memoryRegions) { + return; + } + try { + await this.setArchitecture(); + this.memoryRegions = []; + const miNode = await this.gdb_miDebugger.sendCommand('interpreter-exec console "info mem"', false); + const str = miNode.output; + let match: RegExpExecArray; + const regex = RegExp(/^[0-9]+\s+([^\s])\s+(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+([^\r\n]*)/mgi); + // Num Enb Low Addr High Addr Attrs + // 1 y 0x10000000 0x10100000 flash blocksize 0x200 nocache + while (match = regex.exec(str)) { + const [flag, lowAddr, highAddr, attrsStr] = match.slice(1, 5); + if (flag === 'y') { + const nHighAddr = parseInt(highAddr); + const nlowAddr = parseInt(lowAddr); + const attrs = attrsStr.split(/\s+/g); + const name = `GdbInfo${this.memoryRegions.length}`; + this.memoryRegions.push(new MemoryRegion({ + name: match[1], + size: nHighAddr - nlowAddr, // size + vmaStart: nlowAddr, // vma + lmaStart: nlowAddr, // lma + vmaStartOrig: nlowAddr, + attrs: attrs + })); + } + } + } catch (e) { + this.handleMsg('log', `Error: ${e.toString()}`); + } + const fromGdb = this.memoryRegions.length; + // There is a caveat here. Adding regions from executables is not reliable when you have PIC + // (Position Independent Code) -- so far have not seen such a thing but it is possible + //this.memoryRegions = this.memoryRegions.concat(this.gdbSession.symbolTable.memoryRegions); + + if (this.memoryRegions.length > 0) { + this.handleMsg('log', 'Note: We detected the following memory regions as valid using gdb "info mem" and "objdump -h"\n'); + this.handleMsg('log', ' This information is used to adjust bounds only when normal disassembly fails.\n'); + const hdrs = ['Size', 'VMA Beg', 'VMA End', 'LMA Beg', 'LMA End'].map((x: string) => x.padStart(10)); + const line = ''.padEnd(80, '=') + '\n'; + this.handleMsg('stdout', line); + this.handleMsg('stdout', ' Using following memory regions for disassembly\n'); + this.handleMsg('stdout', line); + this.handleMsg('stdout', hdrs.join('') + ' Attributes\n'); + this.handleMsg('stdout', line); + let count = 0; + for (const r of this.memoryRegions) { + if (count++ === fromGdb) { + if (fromGdb === 0) { + this.handleMsg('stdout', ' Unfortunately, No memory information from gdb (or gdb-server). Will try to manage without\n'); + } + this.handleMsg('stdout', ' '.padEnd(80, '-') + '\n'); + } + const vals = [r.size, r.vmaStart, r.vmaEnd - 1, r.lmaStart, r.lmaEnd - 1].map((v) => hexFormat(v, 8, false).padStart(10)); + if (r.vmaStart === r.lmaStart) { + vals[3] = vals[4] = ' '.padEnd(10, '-'); + } + const attrs = ((count > fromGdb) ? `(${r.name}) ` : '') + r.attrs.join(' '); + this.handleMsg('stdout', vals.join('') + ' ' + attrs + '\n'); + } + this.handleMsg('stdout', line); + } + } + + private clipLow(base: number, addr: number): number { + for (const region of this.memoryRegions) { + if (region.inVmaRegion(base)) { + return region.inVmaRegion(addr) ? addr : region.vmaStart; + } + if (region.inLmaRegion(base)) { + return region.inLmaRegion(addr) ? addr : region.lmaStart; + } + } + return addr; + } + + private clipHigh(base: number, addr: number): number { + for (const region of this.memoryRegions) { + if (region.inVmaRegion(base)) { + return region.inVmaRegion(addr) ? addr : region.vmaEnd; + } + if (region.inLmaRegion(base)) { + return region.inLmaRegion(addr) ? addr : region.lmaEnd; + } + } + return addr; + } + + private formatSym(symName: string, offset: number): string { + if (!symName) { + return undefined; + } + const nm = (symName.length > 22 ? '..' + symName.substring(symName.length - 20) : symName); + return `<${nm}+${offset}>`; + } + + private parseDisassembleResults(result: MINode, validationAddr: number, entireRangeGood: boolean, cmd: string): DisassemblyReturn { + interface ParseSourceInfo { + source: Source; + startLine: number; + endLine: number; + } + + const parseIntruction = (miInstr: MINode, srcInfo?: ParseSourceInfo) => { + const address = MINode.valueOf(miInstr, 'address') as string || '0x????????'; + const fName = MINode.valueOf(miInstr, 'func-name') as string || undefined; + const offset = parseInt(MINode.valueOf(miInstr, 'offset') || '0'); + const ins = MINode.valueOf(miInstr, 'inst'); + const opcodes = MINode.valueOf(miInstr, 'opcodes') as string || ''; + const nAddress = parseInt(address); + // If entire range is valid, use that info but otherwise check specifically for this address + const flag = entireRangeGood ? '' : this.getMemFlagForAddr(nAddress); + //const useInstr = (opcodes.replace(/\s/g, '')).padEnd(2 * this.maxInstrSize + 2) + flag + ins; + const useInstr = opcodes.padEnd(2 * this.maxInstrSize + 2) + flag + ins; + const sym = this.formatSym(fName, offset); + + // const sym = fName ? '<' + (fName.length > 22 ? '..' + fName.substring(fName.length - 20) : fName) + `+${offset}>` : undefined; + const instr: ProtocolInstruction = { + address: address, + pvtAddress: nAddress, + instruction: useInstr, + // VSCode doesn't do anything with 'symbol' + symbol: fName, + // symbol: fName ? `<${fName}+${offset === undefined ? '??' : offset}>` : undefined, + // The UI is not good when we provide this using `instructionBytes` but we need it + pvtInstructionBytes: opcodes + }; + if (sym) { + instr.instructionBytes = sym; + } + if (srcInfo) { + instr.location = srcInfo.source; + instr.line = srcInfo.startLine; + instr.endLine = srcInfo.endLine; + } + + if (validationAddr === nAddress) { + foundIx = instructions.length; + } + + instructions.push(instr); + }; + + let srcCount = 0; + let asmCount = 0; + let foundIx = -1; + const instructions: ProtocolInstruction[] = []; + const asmInsns = result.result('asm_insns') || []; + // You can have all non-source instructions, all source instructions or a mix where within + // the source instructions, you can have instructions without source. I have not seen a mix + // of 'src_and_asm_line' and naked ones as if we did not ask for source info. But, I have + // seen records of 'src_and_asm_line' with no source info. Understandably, it can happen + // due to compiler optimizations and us asking for a random range where insructions from + // different object files are in the same area and compiled differently. None of this documented + // though. Looked at gdb-source and actually saw what i documented above. + let lastLine = 0; + let lastPath = ''; + for (const srcLineVal of asmInsns) { + if (srcLineVal[0] !== 'src_and_asm_line') { + // When there is no source/line information, then 'src_and_asm_line' don't + // exist and it will look like a request that was made without source information + // It is not clear that there will be a mix of plan instructions and ones with + // source info. Not documented. Even the fact that you ask for source info + // and you get something quite different in schema is not documented + // parseIntruction(srcLineVal, undefined, undefined); + parseIntruction(srcLineVal); + lastPath = ''; lastLine = 0; + asmCount++; + } else { + const props = srcLineVal[1]; + const file = MINode.valueOf(props, 'file'); + const fsPath = MINode.valueOf(props, 'fullname') || file; + const line = parseInt(MINode.valueOf(props, 'line') || '1'); + const insns = MINode.valueOf(props, 'line_asm_insn') || []; + const src = fsPath ? new Source(path.basename(fsPath), fsPath) : undefined; + const args: ParseSourceInfo = { + source: src, + startLine: line, + endLine: line + }; + if (fsPath && (lastPath === fsPath)) { + const gap = lastLine && (line > lastLine) ? Math.min(2, line - lastLine) : 0; + args.startLine = line - gap; + lastLine = line; + } else { + lastLine = 0; + lastPath = fsPath; + } + for (const miInstr of insns) { + if (src) { + srcCount++; + parseIntruction(miInstr, args); + } else { + asmCount++; + parseIntruction(miInstr); + } + } + } + } + if (this.doTiming) { + const total = srcCount + asmCount; + this.handleMsg('stdout', `Debug: ${cmd} => Found ${total} instructions. ${srcCount} with source code, ${asmCount} without\n`); + } + return new DisassemblyReturn(instructions, foundIx, false); + } + + protected getProtocolDisassembly(range: DisasmRange, args: DebugProtocol.DisassembleArguments): Promise { + let startAddress = range.qStart; + const endAddress = range.qEnd; + const validationAddr = range.verify; + // To annotate questionable instructions. Too lazy to do on per instruction basis + return new Promise(async (resolve) => { + let iter = 0; + const maxTries = Math.ceil((this.maxInstrSize - this.minInstrSize) / this.instrMultiple); + const doWork = () => { + const old = this.findInCache(startAddress, endAddress); + if (old) { + const foundIx = old.findInstrIndex(validationAddr); + if (foundIx < 0) { + const msg = `Bad instruction cache. Could not find address ${validationAddr} that should have been found`; + this.handleMsg('log', msg + '\n'); + resolve(new Error(msg)); + } else { + resolve(new DisassemblyReturn(old.instructions, foundIx)); + } + return; + } + + const entireRangeGood = range.isKnownStart ;//|| this.isRangeInValidMem(startAddress, endAddress); + const end = endAddress; + // const end = range.isData ? endAddress : this.clipHigh(endAddress, endAddress + this.maxInstrSize); // Get a bit more for functions + if (GdbDisassembler.debug) { + this.handleMsg('log',`startAddress: ${hexFormat(startAddress) }, endAddress:${hexFormat(endAddress)}\n`); + } + let cmd: string; + cmd = `data-disassemble -s ${hexFormat(startAddress)} -e ${hexFormat(end)} -- 5`; + if (this.doTiming) { + const symName = range.symNode ? ` (${range.symNode.symbol.name})` : ''; + const count = `${end - startAddress} bytes`.padStart(15); + this.handleMsg('log', `Debug: Gdb command: -${cmd}${count} ${symName}\n`); + } + this.gdb_miDebugger.sendCommand(cmd, false).then((result) => { + try { + const ret = this.parseDisassembleResults(result, validationAddr, entireRangeGood, cmd); + const foundIx = ret.foundAt; + + if (foundIx < 0) { + if (GdbDisassembler.debug) { + const msg = `Could not disassemble at this address Looking for ${hexFormat(validationAddr)}: ${cmd} `; + this.handleMsg('log', `${msg}, ${ret.instructions}`); + } + if ((startAddress >= this.instrMultiple) && (iter < maxTries)) { + iter++; + startAddress -= this.instrMultiple; // Try again with this address + doWork(); + } else { + const msg = `Error: Could not disassemble at this address ${hexFormat(validationAddr)} ` + JSON.stringify(args); + this.handleMsg('log', msg + '\n'); + resolve(new Error(msg)); + } + } else { + const instrRange = new InstructionRange(ret.instructions); + this.addToCache(instrRange); + resolve(ret); + } + } + catch (e) { + resolve(e); + } + }, (e) => { + this.handleMsg('log', `Error: GDB failed: ${e.toString()}\n`); + resolve(e); + }); + }; + doWork(); + }); + } + + private findInCache(startAddr: number, endAddr: number): InstructionRange { + for (const old of this.cache) { + if (old.isInsideRange(startAddr, endAddr)) { + if (GdbDisassembler.debug) { + this.handleMsg('log', `Instruction cache hit: , + {startAddr: ${hexFormat(startAddr)}, endAddr: ${hexFormat(endAddr)}}, ${old}`); + } + return old; + } + } + // TODO: We should also look for things that are partially overlapping and adjust for the start/end lookups + return null; + } + + private addToCache(arg: InstructionRange) { + for (let ix = 0; ix < this.cache.length;) { + const old = this.cache[ix++]; + if (old.tryMerge(arg)) { + // See if we can merge with next neighbor + if ((ix < this.cache.length) && old.tryMerge(this.cache[ix])) { + this.cache.splice(ix, 1); + } + return; + } + } + this.cache.push(arg); + this.cache.sort((a, b) => a.startAddress - b.startAddress); + } + + // + // This is not normal disassembly. We have to conform to what VSCode expects even beyond + // what the DAP spec says. This is how VSCode is working + // + // * They hinge off of the addresses reported during the stack trace that we gave them. Which btw, is a + // hex-string (memoryReference) + // * Initially, they ask for 400 instructions with 200 instructions before and 200 after the frame PC address + // * While it did (seem to) work if we return more than 400 instructions, that is violating the spec. and may not work + // so we have to return precisely the number of instruction demanded (not a request) + // * Since this is all based on strings (I don't think they interpret the address string). Yet another + // reason why we have to be careful + // * When you scroll just beyond the limits of what is being displayed, they make another request. They use + // the address string for the last (or first depending on direction) instruction previously returned by us + // as a base address for this request. Then they ask for +/- 50 instructions from that base address NOT + // including the base address. But we use the instruction at the baseAddress to validate what we are returning + // since we know that was valid. + // * All requests are in terms of instruction counts and not addresses (understandably from their POV) + // + // Other notes: We know that most ARM instructions are either 2 or 4 bytes. So we translate insruction counts + // multiple of 4 bytes as worst case. We can easily go beyond the boundaries of the memory and at this point, + // not sure what to do. Code can be anywhere in non-contiguous regions and we have no idea to tell what is even + // valid. + // + public disassembleProtocolRequest( + response: DebugProtocol.DisassembleResponse, + args: DebugProtocol.DisassembleArguments, + request?: DebugProtocol.Request): Promise + { + if (GdbDisassembler.debug) { + this.handleMsg('log',`diasam address: ${args.memoryReference}\n`); + } + if (args.memoryReference === undefined) { + // This is our own request. + return this.customDisassembleRequest(response, args); + } + const seq = request?.seq; + return new Promise((resolve, reject) => { + if (GdbDisassembler.debug) { + this.handleMsg('log', `Debug-${seq}: Enqueuing ${JSON.stringify(request)}\n`); + } + const req: DisasmRequest = { + response: response, + args: args, + request: request, + resolve: resolve, + reject: reject + }; + this.disasmRequestQueue.push(req); + if (!this.disasmBusy) { + this.runDisasmRequest(); + } else if (this.doTiming) { + this.handleMsg('log', `Debug-${seq}: ******** Waiting for previous request to complete\n`); + } + }); + } + + // VSCode as a client, frequently makes duplicate requests, back to back before results for the first one are ready + // As a result, older results are not in cache yet, we end up doing work that was not needed. It also happens + // windows get re-arranged, during reset because we have back to back stops and in other situations. So, we + // put things in a queue before starting work on the next item. Save quite a bit of work + private disasmRequestQueue: DisasmRequest[] = []; + private disasmBusy = false; + private runDisasmRequest() { + if (this.disasmRequestQueue.length > 0) { + this.disasmBusy = true; + const next = this.disasmRequestQueue.shift(); + this.disassembleProtocolRequest2(next.response, next.args, next.request).then(() => { + this.disasmBusy = false; + next.resolve(); + this.runDisasmRequest(); + }, (e) => { + this.disasmBusy = false; + next.reject(e); + this.runDisasmRequest(); + }); + } + } + + private disassembleProtocolRequest2( + response: DebugProtocol.DisassembleResponse, + args: DebugProtocol.DisassembleArguments, + request?: DebugProtocol.Request): Promise + { + return new Promise(async (resolve, reject) => { + try { + if (!this.isGetArch) + await this.setArchitecture(); + const seq = request?.seq; + if (GdbDisassembler.debug) { + this.handleMsg('log', `Debug-${seq}: Dequeuing...\n`); + this.handleMsg('log', `disassembleRequest: ${args}`); + } + + const baseAddress = parseInt(args.memoryReference); + const offset = args.offset || 0; + const instrOffset = args.instructionOffset || 0; + const timer = this.doTiming ? new HrTimer() : undefined; + + if (offset !== 0) { + throw (new Error('VSCode using non-zero disassembly offset? Don\'t know how to handle this yet. Please report this problem')); + } + const startAddr = Math.max(0, Math.min(baseAddress, baseAddress + (instrOffset * this.maxInstrSize))); + const endAddr = baseAddress + (args.instructionCount + instrOffset) * this.maxInstrSize; + // this.handleMsg('log', 'Start: ' + ([startAddr, baseAddress, baseAddress - startAddr].map((x) => hexFormat(x))).join(',') + '\n'); + // this.handleMsg('log', 'End : ' + ([baseAddress, endAddr, endAddr - baseAddress].map((x) => hexFormat(x))).join(',') + '\n'); + + //const ranges = await this.findDisasmRanges(startAddr, endAddr, baseAddress); + const range = await this.FindValidMemoryRange(baseAddress, startAddr, offset, args.instructionCount * this.maxInstrSize); + //const range = await this.FindValidMemoryRange(baseAddress, offset, args.instructionCount); + //const promises = ranges.map((r) => this.getProtocolDisassembly(r, args)); + const promises = [this.getProtocolDisassembly(range, args)]; + const instrRanges = await Promise.all(promises); + const orig = Array.from(instrRanges); + // Remove all Error items from front and back + while ((instrRanges.length > 0) && !(instrRanges[0] instanceof DisassemblyReturn)) { + instrRanges.shift(); + //ranges.shift(); + } + while ((instrRanges.length > 0) && !(instrRanges[instrRanges.length - 1] instanceof DisassemblyReturn)) { + instrRanges.pop(); + //ranges.pop(); + } + if (instrRanges.length === 0) { + throw new Error(`Disassembly failed completely for ${hexFormat(startAddr)} - ${hexFormat(endAddr)}`); + } + let all: InstructionRange; + for (const r of instrRanges) { + //const range = ranges.shift(); + if (!(r instanceof DisassemblyReturn)) { + throw new Error(`Disassembly failed completely for ${hexFormat(range.qStart)} - ${hexFormat(range.qEnd)}`); + } + const tmp = new InstructionRange((r as DisassemblyReturn).instructions); + if (!all) { + all = tmp; + } else { + all.forceMerge(tmp); + } + } + + let instrs = all.instructions; + let foundIx = all.findInstrIndex(baseAddress); + if (GdbDisassembler.debug) { + this.handleMsg('log', `Found ${instrs.length}. baseInstrIndex = ${foundIx}.`); + } + if (foundIx < 0) { + throw new Error('Could not find an instruction at the baseAddress. Something is not right. Please report this problem'); + } + // Spec says must have exactly `count` instructions. Kinda harsh but...gotta do it + // These are corner cases that are hard to test. This would happen if we are falling + // of an edge of a memory and VSCode is making requests we can't exactly honor. But, + // if we have a partial match, do the best we can by padding + let tmp = instrs.length > 0 ? instrs[0].pvtAddress : baseAddress; + let nPad = (-instrOffset) - foundIx; + const junk: ProtocolInstruction[] = []; + for (; nPad > 0; nPad--) { // Pad at the beginning + tmp -= this.minInstrSize; // Yes, this can go negative + junk.push(dummyInstr(tmp)); + } + if (junk.length > 0) { + instrs = junk.reverse().concat(instrs); + foundIx += junk.length; + } + + const extra = foundIx + instrOffset; + if (extra > 0) { // Front heavy + instrs.splice(0, extra); + foundIx -= extra; // Can go negative, thats okay + } + + tmp = instrs[instrs.length - 1].pvtAddress; + while (instrs.length < args.instructionCount) { + tmp += this.minInstrSize; + instrs.push(dummyInstr(tmp)); + } + if (instrs.length > args.instructionCount) { // Tail heavy + instrs.splice(args.instructionCount); + } + + if (GdbDisassembler.debug) { + this.handleMsg('log', `Returning ${instrs.length} instructions of ${all.instructions.length} queried. baseInstrIndex = ${foundIx}.`); + if ((foundIx >= 0) && (foundIx < instrs.length)) { + this.handleMsg('log', `${instrs[foundIx]}`); + } else if ((foundIx !== instrOffset) && (foundIx !== -instrOffset) && (foundIx !== (instrs.length + instrOffset))) { + this.handleMsg('stderr',`This may be a problem. Referenced index should be exactly ${instrOffset} off`); + this.handleMsg('log', `${instrs}`); + } + } + this.cleaupAndCheckInstructions(instrs); + assert(instrs.length === args.instructionCount, `Instruction count did not match. Please reports this problem ${JSON.stringify(request)}`); + response.body = { + instructions: instrs + }; + if (this.doTiming) { + const ms = timer.createPaddedMs(3); + this.handleMsg('log', `Debug-${seq}: Elapsed time for Disassembly Request: ${ms} ms\n`); + } + this.gdbSession.sendResponse(response); + resolve(); + } + catch (e) { + const msg = `Unable to disassemble: ${e.toString()}: ${JSON.stringify(request)}`; + if (GdbDisassembler.debug) { + this.handleMsg('log', msg + '\n'); + } + this.gdbSession.sendErrorResponsePub(response, 1, msg); + resolve(); + } + }); + + function dummyInstr(tmp: number): ProtocolInstruction { + return { + address: hexFormat(tmp), + instruction: '', + pvtAddress: tmp + }; + } + } + + private async FindValidMemoryRange(referenceAddress: number, startAddr:number, offset: number, count: number): Promise { + + const cmd = `data-read-memory-bytes -o ${offset} ${startAddr} ${count}`; + + let result:MINode = await this.gdb_miDebugger.sendCommand(cmd, false); + let trueStart = parseInt(result.result('memory[0].begin'), 16); + let trueEnd = parseInt(result.result('memory[0].end'), 16); + let range: DisasmRange = { + qStart: trueStart, + qEnd: trueEnd, + verify: referenceAddress, + isKnownStart: true + }; + if (GdbDisassembler.debug) + this.handleMsg('log', `range: ${JSON.stringify(range)}\n`); + return range; + } + // We would love to do disassembly on a whole range. But frequently, GDB gives wrong + // information when there are gaps between functions. There is also a problem with functions + // that do not have a size + private findDisasmRanges(trueStart: number, trueEnd: number, referenceAddress: number): DisasmRange[] { + const doDbgPrint = false; + const printFunc = (item: SymbolNode) => { + if (doDbgPrint) { + const file = item.symbol.file || ''; + const msg = `(${hexFormat(item.low)}, ${item.low}), (${hexFormat(item.high)}, ${item.high}) ${item.symbol.name} ${file}`; + this.handleMsg('stdout', msg + '\n'); + this.handleMsg('log', msg); + } + }; + + if (doDbgPrint) { + this.handleMsg('stdout', `${hexFormat(trueStart)}, ${hexFormat(trueEnd)} Search range\n`); + this.handleMsg('stdout', '-'.repeat(80) + '\n'); + } + trueStart = this.clipLow(referenceAddress, trueStart); + trueEnd = this.clipHigh(referenceAddress, trueEnd); + + const ret: DisasmRange[] = []; + //const functions = this.gdbSession.symbolTable.symbolsAsIntervalTree.search(trueStart, trueEnd); + let range: DisasmRange = { + qStart: Math.min(trueStart, referenceAddress), + qEnd: Math.max(trueEnd, referenceAddress + this.maxInstrSize), + verify: referenceAddress, + isKnownStart: false + }; + ret.push(range); + // if (functions.length > 0) { + // let prev = functions[0]; + // printFunc(prev); + // let high = prev.high + 1; + // range.qEnd = high; + // range.verify = range.qStart = prev.low; + // range.symNode = prev; + // range.isKnownStart = true; + // for (let ix = 1; ix < functions.length; ix++ ) { + // const item = functions[ix]; + // if ((prev.low !== item.low) || (prev.high !== item.high)) { // Yes, duplicates are possible + // const diff = item.low - high; + // high = item.high + 1; + // if (diff === 0) { + // range.qEnd = high; // extend the range + // } else { + // range.qEnd = Math.max(range.qEnd, item.low /*- 1*/); + // // If we want to deal with gaps between functions as if they are data, this is the place to do it + // range = { // Start a new range + // qStart: item.low, + // qEnd: high, + // verify: item.low, + // isKnownStart: true, + // symNode: item + // }; + // ret.push(range); + // } + // } + // printFunc(item); + // prev = item; + // } + // // For the last one, try to get until the end + // range.qEnd = Math.max(range.qEnd, trueEnd); + // } + console.table(ret); + return ret; + } + + // Remove location information for any consecutive instructions having the + // same location. This will remove lot of redundant source lines from presentation + private cleaupAndCheckInstructions(instrs: ProtocolInstruction[]) { + if (instrs.length > 0) { + let prev = null; + let count = 0; + for (let ix = 0; ix < instrs.length; ix++ ) { + const instr = instrs[ix]; + if (instr.pvtInstructionBytes && !instr.pvtIsData) { + const nBytes = (instr.pvtInstructionBytes.length + 1) / 3; + if ((nBytes < this.minInstrSize) || (nBytes > this.maxInstrSize)) { + throw new Error(`Bad/corrupted disassembly (too many/few bytes? Please report this problem ${instr.address} ${instr.instruction}`); + } + } + if (prev && (instr.line === prev.line) && instr.location && prev.location && (instr.location.path === prev.location.path)) { + // If you remove too many instructions because the source line is same, then VSCode + // does not display any source for any line. Real threshold may be more than 10 but + // even visually, doesn't hurt to repeat + if (count < 10) { + // Don't modify the original source as they also exist in the cache. produce a copy + const copy = Object.assign({}, instr); + count++; + delete copy.location; + delete copy.line; + instrs[ix] = copy; + } else { + count = 0; + } + } else { + count = 0; + } + prev = instr; + } + } + } + + public async customDisassembleRequest(response: DebugProtocol.Response, args: any): Promise { + if (args.function) { + try { + const funcInfo: SymbolInformation = await this.getDisassemblyForFunction(args.function, args.file); + response.body = { + instructions: funcInfo.instructions, + name: funcInfo.name, + file: funcInfo.file, + address: funcInfo.address, + length: funcInfo.length + }; + this.gdbSession.sendResponse(response); + } + catch (e) { + this.gdbSession.sendErrorResponsePub(response, 1, `Unable to disassemble: ${e.toString()}`); + } + return; + } + else if (args.startAddress) { + try { + let funcInfo = undefined;//= this.gdbSession.symbolTable.getFunctionAtAddress(args.startAddress); + if (funcInfo) { + funcInfo = await this.getDisassemblyForFunction(funcInfo.name, funcInfo.file as string); + response.body = { + instructions: funcInfo.instructions, + name: funcInfo.name, + file: funcInfo.file, + address: funcInfo.address, + length: funcInfo.length + }; + this.gdbSession.sendResponse(response); + } + else { + // tslint:disable-next-line:max-line-length + const instructions: DisassemblyInstruction[] = await this.getDisassemblyForAddresses(args.startAddress, args.length || 256); + response.body = { instructions: instructions }; + this.gdbSession.sendResponse(response); + } + } + catch (e) { + this.gdbSession.sendErrorResponsePub(response, 1, `Unable to disassemble: ${e.toString()}`); + } + return; + } + else { + this.gdbSession.sendErrorResponsePub(response, 1, 'Unable to disassemble; invalid parameters.'); + } + } + + public async getDisassemblyForFunction(functionName: string, file?: string): Promise { + return + // //const symbol: SymbolInformation = this.gdbSession.symbolTable.getFunctionByName(functionName, file); + + // if (!symbol) { throw new Error(`Unable to find function with name ${functionName}.`); } + + // if (symbol.instructions) { return symbol; } + + // const startAddress = symbol.address; + // const endAddress = symbol.address + symbol.length; + + // // tslint:disable-next-line:max-line-length + // const result = await this.gdb_miDebugger.sendCommand(`data-disassemble -s ${hexFormat(startAddress)} -e ${hexFormat(endAddress)} -- 2`); + // const rawInstructions = result.result('asm_insns'); + // const instructions: DisassemblyInstruction[] = rawInstructions.map((ri) => { + // const address = MINode.valueOf(ri, 'address'); + // const functionName = MINode.valueOf(ri, 'func-name'); + // const offset = parseInt(MINode.valueOf(ri, 'offset')); + // const inst = MINode.valueOf(ri, 'inst'); + // const opcodes = MINode.valueOf(ri, 'opcodes'); + + // return { + // address: address, + // functionName: functionName, + // offset: offset, + // instruction: inst, + // opcodes: opcodes + // }; + // }); + // symbol.instructions = instructions; + // return symbol; + } + + private async getDisassemblyForAddresses(startAddress: number, length: number): Promise { + const endAddress = startAddress + length; + + // tslint:disable-next-line:max-line-length + const result = await this.gdb_miDebugger.sendCommand(`data-disassemble -s ${hexFormat(startAddress)} -e ${hexFormat(endAddress)} -- 2`); + const rawInstructions = result.result('asm_insns'); + const instructions: DisassemblyInstruction[] = rawInstructions.map((ri) => { + const address = MINode.valueOf(ri, 'address'); + const functionName = MINode.valueOf(ri, 'func-name'); + const offset = parseInt(MINode.valueOf(ri, 'offset')); + const inst = MINode.valueOf(ri, 'inst'); + const opcodes = MINode.valueOf(ri, 'opcodes'); + + return { + address: address, + functionName: functionName, + offset: offset, + instruction: inst, + opcodes: opcodes + }; + }); + + return instructions; + } +} diff --git a/src/backend/gdb_expansion.ts b/src/backend/gdb_expansion.ts index 143e6d5..a88f37f 100644 --- a/src/backend/gdb_expansion.ts +++ b/src/backend/gdb_expansion.ts @@ -76,7 +76,11 @@ export function expandValue(variableCreate: Function, value: string, root: strin } namespace = namespace + pointerCombineChar + name; } else - namespace = name; + if (root && root.startsWith("*")) + namespace = name.substr(1); + else { + namespace = name; + } } } }); @@ -211,27 +215,36 @@ export function expandValue(variableCreate: Function, value: string, root: strin createValue = (name, val) => { let ref = 0; + let evaluateName; if (typeof val == "object") { ref = variableCreate(val); val = "Object"; } else if (typeof val == "string" && val.startsWith("*0x")) { if (extra && MINode.valueOf(extra, "arg") == "1") { - ref = variableCreate(getNamespace("*(" + name), { arg: true }); + const isVoid = MINode.valueOf(extra, "type") === "void *"; + evaluateName = getNamespace( ( isVoid ? "":"*") + name); + ref = variableCreate(getNamespace( ( isVoid ? "":"*") + "(" + name), { arg: true, isVoid }); val = ""; } else { - ref = variableCreate(getNamespace("*" + name)); + evaluateName = getNamespace("*" + name); + ref = variableCreate(evaluateName); val = "Object@" + val; } } else if (typeof val == "string" && val.startsWith("@0x")) { - ref = variableCreate(getNamespace("*&" + name.substr)); + evaluateName = getNamespace("*&" + name); + ref = variableCreate(evaluateName); val = "Ref" + val; } else if (typeof val == "string" && val.startsWith("<...>")) { - ref = variableCreate(getNamespace(name)); + evaluateName =getNamespace(name); + ref = variableCreate(evaluateName); val = "..."; + } else { + evaluateName = getNamespace(name); } return { name: name, value: val, + evaluateName, variablesReference: ref }; }; diff --git a/src/backend/mi2/mi2.ts b/src/backend/mi2/mi2.ts index 81cce1f..5a0fa21 100644 --- a/src/backend/mi2/mi2.ts +++ b/src/backend/mi2/mi2.ts @@ -1,12 +1,14 @@ -import { Breakpoint, IBackend, Thread, Stack, SSHArguments, Variable, VariableObject, MIError } from "../backend"; +import { Breakpoint, OurInstructionBreakpoint, IBackend, Thread, Stack, SSHArguments, Variable, VariableObject, MIError } from "../backend"; import * as ChildProcess from "child_process"; import { EventEmitter } from "events"; import { parseMI, MINode } from '../mi_parse'; import * as linuxTerm from '../linux/console'; +import { hexFormat } from '../common' import * as net from "net"; import * as fs from "fs"; import * as path from "path"; import { Client } from "ssh2"; +import * as os from 'os'; export function escape(str: string) { return str.replace(/\\/g, "\\\\").replace(/"/g, "\\\""); @@ -16,6 +18,24 @@ const nonOutput = /^(?:\d*|undefined)[\*\+\=]|[\~\@\&\^]/; const gdbMatch = /(?:\d*|undefined)\(gdb\)/; const numRegex = /\d+/; +export interface ReadMemResults { + startAddress: string; + endAddress: string; + data: string; +} + +export function parseReadMemResults(node: MINode): ReadMemResults { + const startAddress = node.resultRecords.results[0][1][0][0][1]; + const endAddress = node.resultRecords.results[0][1][0][2][1]; + const data = node.resultRecords.results[0][1][0][3][1]; + const ret: ReadMemResults = { + startAddress: startAddress, + endAddress: endAddress, + data: data + }; + return ret; +} + function couldBeOutput(line: string) { if (nonOutput.exec(line)) return false; @@ -25,6 +45,8 @@ function couldBeOutput(line: string) { const trace = false; export class MI2 extends EventEmitter implements IBackend { + protected nextTokenComing = 1; // This will be the next token output from gdb + protected needOutput: { [index: number]: '' } = {}; constructor(public application: string, public preargs: string[], public extraargs: string[], procEnv: any, public extraCommands: string[] = []) { super(); @@ -46,6 +68,7 @@ export class MI2 extends EventEmitter implements IBackend { } this.procEnv = env; } + this.cpuArch = os.arch(); } load(cwd: string, target: string, procArgs: string, separateConsole: string): Thenable { @@ -333,8 +356,14 @@ export class MI2 extends EventEmitter implements IBackend { const parsed = parseMI(line); if (this.debugOutput) this.log("log", "GDB -> App: " + JSON.stringify(parsed)); - let handled = false; if (parsed.token !== undefined) { + if (this.needOutput[parsed.token] !== undefined) { + parsed.output = this.needOutput[parsed.token]; + } + this.nextTokenComing = parsed.token + 1; + } + let handled = false; + if (parsed.token !== undefined && parsed.resultRecords) { if (this.handlers[parsed.token]) { this.handlers[parsed.token](parsed); delete this.handlers[parsed.token]; @@ -347,7 +376,11 @@ export class MI2 extends EventEmitter implements IBackend { if (parsed.outOfBandRecord) { parsed.outOfBandRecord.forEach(record => { if (record.isStream) { - this.log(record.type, record.content); + if ((record.type === 'console') && (this.needOutput[this.nextTokenComing] !== undefined)) { + this.needOutput[this.nextTokenComing] += record.content; + } else { + this.log(record.type, record.content); + } } else { if (record.type == "exec") { this.emit("exec-async-output", parsed); @@ -508,21 +541,21 @@ export class MI2 extends EventEmitter implements IBackend { }); } - next(reverse: boolean = false): Thenable { + next(reverse: boolean = false, instruction?: boolean): Thenable { if (trace) this.log("stderr", "next"); return new Promise((resolve, reject) => { - this.sendCommand("exec-next" + (reverse ? " --reverse" : "")).then((info) => { + this.sendCommand((instruction ? 'exec-next-instruction' :'exec-next') + (reverse ? " --reverse" : "")).then((info) => { resolve(info.resultRecords.resultClass == "running"); }, reject); }); } - step(reverse: boolean = false): Thenable { + step(reverse: boolean = false, instruction?: boolean): Thenable { if (trace) this.log("stderr", "step"); return new Promise((resolve, reject) => { - this.sendCommand("exec-step" + (reverse ? " --reverse" : "")).then((info) => { + this.sendCommand((instruction ? 'exec-step-instruction' : 'exec-step') + (reverse ? " --reverse" : "")).then((info) => { resolve(info.resultRecords.resultClass == "running"); }, reject); }); @@ -629,6 +662,31 @@ export class MI2 extends EventEmitter implements IBackend { }); } + addInstrBreakPoint(breakpoint: OurInstructionBreakpoint): Promise { + if (trace) { + this.log('stderr', 'addInstrBreakPoint'); + } + return new Promise((resolve, reject) => { + let bkptArgs = ''; + if (breakpoint.condition) { + bkptArgs += `-c "${breakpoint.condition}" `; + } + + bkptArgs += '*' + hexFormat(breakpoint.address); + + this.sendCommand(`break-insert ${bkptArgs}`).then((result) => { + if (result.resultRecords.resultClass === 'done') { + const bkptNum = parseInt(result.result('bkpt.number')); + breakpoint.number = bkptNum; + resolve(breakpoint); + } + else { + reject(new MIError(result.result('msg') || 'Internal error', `Setting breakpoint at ${bkptArgs}`)); + } + }, reject); + }); + } + removeBreakPoint(breakpoint: Breakpoint): Thenable { if (trace) this.log("stderr", "removeBreakPoint"); @@ -690,8 +748,12 @@ export class MI2 extends EventEmitter implements IBackend { const options: string[] = []; - if (thread != 0) + if (thread != 0){ options.push("--thread " + thread); + if (this.status == 'stopped'){ + this.currentThreadId = thread; + } + } const depth: number = (await this.sendCommand(["stack-info-depth"].concat(options).join(" "))).result("depth").valueOf(); const lowFrame: number = startFrame ? startFrame : 0; @@ -734,10 +796,54 @@ export class MI2 extends EventEmitter implements IBackend { }); } + async getRegisters(): Promise { + + if (!this.regNames || this.regNames.length == 0){ + const regNameResult = await this.sendCommand(`data-list-register-names`); + this.regNames = regNameResult.result("register-names"); + if (!this.regNames|| this.regNames.length == 0) return []; + } + + if (this.status == 'stopped' && this.currentThreadId){ + await this.sendCommand(`thread-select ${this.currentThreadId}`); + } + + const result = await this.sendCommand(`data-list-register-values x`); + const regValues = result.result("register-values"); + + if (trace) + this.log("stderr", `getRegisters: ${this.regNames.length} ${regValues.length}`); + + var ret = []; + let reg_index = 0; + for (let name_index = 0; name_index < this.regNames.length; name_index++) { + const name = this.regNames[name_index]; + if (!name || name == '') continue; + const reg = regValues[reg_index]; + const value = MINode.valueOf(reg, "value"); + const type = MINode.valueOf(reg, "type"); + ret.push({ + name: name, + evaluateName: '$'+name, //修复寄存器无法watch + value: value, + type: type, + variablesReference: 0 + }); + reg_index++; + } + + return ret; + } + async getStackVariables(thread: number, frame: number): Promise { if (trace) this.log("stderr", "getStackVariables"); + if (!thread){ + if (this.status == 'stopped'){ + this.currentThreadId = thread; + } + } const result = await this.sendCommand(`stack-list-variables --thread ${thread} --frame ${frame} --simple-values`); const variables = result.result("variables"); const ret: Variable[] = []; @@ -759,7 +865,7 @@ export class MI2 extends EventEmitter implements IBackend { if (trace) this.log("stderr", "examineMemory"); return new Promise((resolve, reject) => { - this.sendCommand("data-read-memory-bytes 0x" + from.toString(16) + " " + length).then((result) => { + this.sendCommand("data-read-memory-bytes 0x" + from.toString(16) + " " + length, true).then((result) => { resolve(result.result("memory[0].contents")); }, reject); }); @@ -789,17 +895,20 @@ export class MI2 extends EventEmitter implements IBackend { }, reject); }); } - async evalExpression(name: string, thread: number, frame: number): Promise { + async evalExpression(name: string, thread: number, frame: number, suppressFailure: boolean = false): Promise { if (trace) this.log("stderr", "evalExpression"); let command = "data-evaluate-expression "; if (thread != 0) { command += `--thread ${thread} --frame ${frame} `; + if (this.status == 'stopped'){ + this.currentThreadId = thread; + } } command += name; - return await this.sendCommand(command); + return await this.sendCommand(command, suppressFailure); } async varCreate(expression: string, name: string = "-", frame: string = "@"): Promise { @@ -873,8 +982,10 @@ export class MI2 extends EventEmitter implements IBackend { sendCommand(command: string, suppressFailure: boolean = false): Thenable { const sel = this.currentToken++; + this.needOutput[sel] = ''; return new Promise((resolve, reject) => { this.handlers[sel] = (node: MINode) => { + delete this.needOutput[sel]; if (node && node.resultRecords && node.resultRecords.resultClass === "error") { if (suppressFailure) { this.log("stderr", `WARNING: Error executing command '${command}'`); @@ -898,11 +1009,14 @@ export class MI2 extends EventEmitter implements IBackend { } isRecord:boolean = false; + currentThreadId:number = 0; status: 'running' | 'stopped' | 'none' = 'none'; + cpuArch:string = ''; prettyPrint: boolean = true; printCalls: boolean; debugOutput: boolean; features: string[]; + regNames:any[] = []; public procEnv: any; protected isSSH: boolean; protected sshReady: boolean; diff --git a/src/backend/mi_parse.ts b/src/backend/mi_parse.ts index 1bdd9be..3df6538 100644 --- a/src/backend/mi_parse.ts +++ b/src/backend/mi_parse.ts @@ -58,6 +58,7 @@ export class MINode implements MIInfo { token: number; outOfBandRecord: { isStream: boolean, type: string, asyncClass: string, output: [string, any][], content: string }[]; resultRecords: { resultClass: string, results: [string, any][] }; + public output: string = ''; constructor(token: number, info: { isStream: boolean, type: string, asyncClass: string, output: [string, any][], content: string }[], result: { resultClass: string, results: [string, any][] }) { this.token = token; diff --git a/src/backend/symbols.ts b/src/backend/symbols.ts new file mode 100644 index 0000000..4022453 --- /dev/null +++ b/src/backend/symbols.ts @@ -0,0 +1,986 @@ +import * as os from 'os'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as crypto from 'crypto'; +import { DisassemblyInstruction, SpawnLineReader, SymbolFile, validateELFHeader } from './common'; +import { IntervalTree, Interval } from 'node-interval-tree'; +import JsonStreamStringify from 'json-stream-stringify'; +const StreamArray = require('stream-json/streamers/StreamArray'); +import * as zlib from 'zlib'; + +//import { SymbolInformation as SymbolInformation } from '../symbols'; +import { GDBDebugSession } from '../gdb'; +import { hexFormat } from './common' +import { MINode } from './mi_parse'; + +export enum SymbolType { + Function, + File, + Object, + Normal +} + +export enum SymbolScope { + Local, + Global, + Neither, + Both +} + +export interface SymbolInformation { + addressOrig: number; + address: number; + length: number; + name: string; + file: number | string; // The actual file name parsed (more reliable with nm) + section?: string; // Not available with nm + type: SymbolType; + scope: SymbolScope; + isStatic: boolean; + // line?: number; // Only available when using nm + instructions: DisassemblyInstruction[]; + hidden: boolean; +} + + +const OBJDUMP_SYMBOL_RE = RegExp(/^([0-9a-f]{8})\s([lg\ !])([w\ ])([C\ ])([W\ ])([I\ ])([dD\ ])([FfO\ ])\s(.*?)\t([0-9a-f]+)\s(.*)$/); +const NM_SYMBOL_RE = RegExp(/^([0-9a-f]+).*\t(.+):[0-9]+/); // For now, we only need two things +const debugConsoleLogging = false; +const TYPE_MAP: { [id: string]: SymbolType } = { + 'F': SymbolType.Function, + 'f': SymbolType.File, + 'O': SymbolType.Object, + ' ': SymbolType.Normal +}; + +const SCOPE_MAP: { [id: string]: SymbolScope } = { + 'l': SymbolScope.Local, + 'g': SymbolScope.Global, + ' ': SymbolScope.Neither, + '!': SymbolScope.Both +}; + +export class SymbolNode implements Interval { + constructor( + public readonly symbol: SymbolInformation, // Only functions and objects + public readonly low: number, // Inclusive near as I can tell + public readonly high: number // Inclusive near as I can tell + ) {} +} + +interface IMemoryRegion { + name: string; + size: number; + vmaStart: number; // Virtual memory address + vmaStartOrig: number; + lmaStart: number; // Load memory address + attrs: string[]; +} +export class MemoryRegion1 { + public name: string; + constructor(obj: string) { + this.name = obj; + } +} +export class MemoryRegion implements IMemoryRegion { + public vmaEnd: number; // Inclusive + public lmaEnd: number; // Exclusive + public name: string; + public size: number; + public vmaStart: number; + public lmaStart: number; + public vmaStartOrig: number; + public attrs: string[]; + constructor(obj: IMemoryRegion) { + Object.assign(this, obj); + this.vmaEnd = this.vmaStart + this.size + 1; + this.lmaEnd = this.lmaStart + this.size + 1; + } + + public inVmaRegion(addr: number) { + return (addr >= this.vmaStart) && (addr < this.vmaEnd); + } + + public inLmaRegion(addr: number) { + return (addr >= this.lmaStart) && (addr < this.lmaEnd); + } + + public inRegion(addr: number) { + return this.inVmaRegion(addr) || this.inLmaRegion(addr); + } +} + +interface ISymbolTableSerData { + version: number; + memoryRegions: MemoryRegion[]; + fileTable: string[]; + symbolKeys: string[]; + allSymbols: any[][]; +} + +// Replace last part of the path containing a program to another. If search string not found +// or replacement the same as search string, then null is returned +function replaceProgInPath(filepath: string, search: string | RegExp, replace: string): string { + if (os.platform() === 'win32') { + filepath = filepath.toLowerCase().replace(/\\/g, '/'); + } + const ix = filepath.lastIndexOf('/'); + const prefix = (ix >= 0) ? filepath.substring(0, ix + 1) : '' ; + const suffix = filepath.substring(ix + 1); + const replaced = suffix.replace(search, replace); + if (replaced === suffix) { + return null; + } + const ret = prefix + replaced; + return ret; +} + +const trace = true; + +interface ExecPromise { + args: string[]; + promise: Promise; +} +export class SymbolTable { + private allSymbols: SymbolInformation[] = []; + private fileTable: string[] = []; + public memoryRegions: MemoryRegion[] = []; + + // The following are caches that are either created on demand or on symbol load. Helps performance + // on large executables since most of our searches are linear. Or, to avoid a search entirely if possible + // Case sensitivity for path names is an issue: We follow just what gcc records so inherently case-sensitive + // or case-preserving. We don't try to re-interpret/massage those path-names (but we do Normalize). + private staticsByFile: {[file: string]: SymbolInformation[]} = {}; + private globalVars: SymbolInformation[] = []; + private globalFuncsMap: {[key: string]: SymbolInformation} = {}; // Key is function name + private staticVars: SymbolInformation[] = []; + private staticFuncsMap: {[key: string]: SymbolInformation[]} = {}; // Key is function name + private fileMap: {[key: string]: string[]} = {}; // Potential list of file aliases we found + public symbolsAsIntervalTree: IntervalTree = new IntervalTree(); + public symbolsByAddress: Map = new Map(); + public symbolsByAddressOrig: Map = new Map(); + private varsByFile: {[path: string]: VariablesInFile} = null; + private nmPromises: ExecPromise[] = []; + + private objdumpPath: string; + + constructor(private gdbSession: GDBDebugSession, private executables: SymbolFile[]) { + const args = this.gdbSession.args; + //this.objdumpPath = args.objdumpPath; + if (!this.objdumpPath) { + this.objdumpPath ='objdump'; + const tmp = replaceProgInPath(args.gdbpath ? args.gdbpath : 'gdb', /gdb/i, 'objdump'); + this.objdumpPath = tmp || this.objdumpPath; + } + } + + private createSymtableSerializedFName(exeName: string) { + return this.createFileMapCacheFileName(exeName, '-syms') + '.gz'; + } + + private static CurrentVersion = 1; + private serializeSymbolTable(exeName: string) { + const fMap: {[key: string]: number} = {}; + const keys = this.allSymbols.length > 0 ? Object.keys(this.allSymbols[0]) : []; + this.fileTable = []; + const syms = []; + for (const sym of this.allSymbols) { + const fName: string = sym.file as string; + let id: number = fMap[fName]; + if (id === undefined) { + id = this.fileTable.length; + this.fileTable.push(fName); + fMap[fName] = id; + } + const tmp = sym.file; + sym.file = id; + syms.push(Object.values(sym)); + sym.file = tmp; + } + const serObj: ISymbolTableSerData = { + version: SymbolTable.CurrentVersion, + memoryRegions: this.memoryRegions, + fileTable: this.fileTable, + symbolKeys: keys, + allSymbols: syms + }; + + const fName = this.createSymtableSerializedFName(exeName); + const fStream = fs.createWriteStream(fName, { flags: 'w' }); + fStream.on('error', () => { + console.error('Saving symbol table failed!!!'); + }); + fStream.on('close', () => { + console.log('Saved symbol table'); + }); + const jsonStream = new JsonStreamStringify([serObj]); + jsonStream.on('error', () => { + console.error('Saving symbol table JsonStreamStringify() failed!!!'); + }); + jsonStream + .pipe(zlib.createGzip()) + .pipe(fStream) + .on('finish', () => { + console.log('Pipe ended'); + }); + } + + private deSerializeSymbolTable(exeName: string): Promise { + return new Promise((resolve) => { + const fName = this.createSymtableSerializedFName(exeName); + if (!fs.existsSync(fName)) { + resolve(false); + return; + } + const fStream = fs.createReadStream(fName); + fStream.on('error', () => { + resolve(false); + }); + + console.time('abc'); + const jsonStream = StreamArray.withParser(); + jsonStream.on('data', ({key, value}) => { + console.timeLog('abc', 'Parsed data:'); + fStream.close(); + reconstruct(value); + }); + fStream + .pipe(zlib.createGunzip()) + .pipe(jsonStream.input); + + const reconstruct = (data: any) => { + try { + const serObj: ISymbolTableSerData = data as ISymbolTableSerData; + if (!serObj || (serObj.version !== SymbolTable.CurrentVersion)) { + resolve(false); + return; + } + this.fileMap = {}; + for (const f of serObj.fileTable) { + if (f !== null) { // Yes, there one null in there + this.addPathVariations(f); + } + } + + this.allSymbols = []; + const keys = serObj.symbolKeys; + const n = keys.length; + for (const values of serObj.allSymbols) { + const sym: any = {}; + values.forEach((v, i) => sym[keys[i]] = v); + sym.file = serObj.fileTable[sym.file as number]; + this.addSymbol(sym/* as SymbolInformation*/); + } + this.memoryRegions = []; + for (const m of serObj.memoryRegions) { + this.memoryRegions.push(new MemoryRegion(m)); + } + console.timeEnd('abc'); + resolve(true); + } catch (e) { + resolve(false); + } + }; + }); + } + + /** + * Problem statement: + * We need a read the symbol table for multiple types of information and none of the tools so far + * give all all we need + * + * 1. List of static variables by file + * 2. List og globals + * 3. Functions (global and static) with their addresses and lengths + * + * Things we tried: + * 1.-Wi option objdump -- produces super large output (100MB+) and take minutes to produce and parse + * 2. Using gdb: We can get variable/function to file information but no addresses -- not super fast but + * inconvenient. We have a couple of do it a couple of different ways and it is still ugly + * 3. Use nm: This looked super promising until we found out it is super inacurate in telling the type of + * symbol. It classifies variables as functions and vice-versa. But for figuring out which variable + * belongs to which file that is pretty accurate + * 4. Use readelf. This went nowhere because you can't get even basic file to symbol mapping from this + * and it is not as universal for handling file formats as objdump. + * + * So, we are not using option 3 and fall back to option 2. We will never go back to option 1 + * + * Another problem is that we may have to query for symbols using different ways -- partial file names, + * full path names, etc. So, we keep a map of file to statics. + * + * Other uses for objdump is to get a section headers for memory regions that can be used for disassembly + * + * We avoid splitting the output(s) into lines and then parse line at a time. + */ + public loadSymbols(): Promise { + return new Promise(async (resolve) => { + const total = 'Total running objdump & nm'; + console.time(total); + try { + await this.loadFromObjdumpAndNm(); + + const nxtLabel = 'Postprocessing symbols'; + console.time(nxtLabel); + this.categorizeSymbols(); + this.sortGlobalVars(); + resolve(); + console.timeEnd(nxtLabel); + } + catch (e) { + // We treat this is non-fatal, but why did it fail? + this.gdbSession.handleMsg('log', `Error: objdump failed! statics/globals/functions may not be properly classified: ${e.toString()}`); + this.gdbSession.handleMsg('log', ' ENOENT means program not found. If that is not the issue, please report this problem.'); + resolve(); + } + finally { + console.timeEnd(total); + } + }); + } + + private rttSymbol; + public readonly rttSymbolName = '_SEGGER_RTT'; + private addSymbol(sym: SymbolInformation) { + const oldSym = this.symbolsByAddress.get(sym.address); + if (oldSym) { + // Probably should take the new one. Dups can come from multiple symbol (elf) files + // Not sure `symbolsAsIntervalTree` can handle duplicates and we have to do a linear search + // in allSymbols. This shouldn't really happen unless user loads duplicate symbol files + return; + } + + if (!this.rttSymbol && (sym.name === this.rttSymbolName) && (sym.type === SymbolType.Object) && (sym.length > 0)) { + this.rttSymbol = sym; + } + + this.allSymbols.push(sym); + if ((sym.type === SymbolType.Function) || (sym.length > 0)) { + const treeSym = new SymbolNode(sym, sym.address, sym.address + Math.max(1, sym.length) - 1); + this.symbolsAsIntervalTree.insert(treeSym); + } + this.symbolsByAddress.set(sym.address, sym); + this.symbolsByAddressOrig.set(sym.addressOrig, sym); + } + + private objdumpReader: SpawnLineReader; + private currentObjDumpFile: string = null; + + private readObjdumpHeaderLine(symF: SymbolFile, line: string, err: any): boolean { + if (!line) { + return line === '' ? true : false; + } + const entry = RegExp(/^\s*[0-9]+\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+(.*)$/); + // Header: + // Idx Name Size VMA LMA File off Algn + // Sample entry: + // 0 .cy_m0p_image 000025d4 10000000 10000000 00010000 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA + // 1 2 3 4 5 6 7 + // const entry = RegExp(/^\s*[0-9]+\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)[^\n]+\n\s*([^\r\n]*)\r?\n/gm); + const match = line.match(entry); + if (match) { + const attrs = match[7].trim().toLowerCase().split(/[,\s]+/g); + if (!attrs.find((s) => s === 'alloc')) { + // Technically we only need regions marked for code but lets get all non-debug, non-comment stuff + return true; + } + const name = match[1]; + const offset = symF.offset || 0; + const vmaOrig = parseInt(match[3], 16); + let vmaStart = vmaOrig + offset; + const section = symF.sectionMap[name]; + if ((name === '.text') && (typeof symF.textaddress === 'number')) { + vmaStart = symF.textaddress; + if (!section) { + symF.sections.push({ + address: vmaStart, + addressOrig: vmaOrig, + name: name + }); + symF.sectionMap[name] = symF.sections[symF.sections.length - 1]; + } + } + if (section) { + section.addressOrig = vmaStart; + vmaStart = section.address; + } + const region = new MemoryRegion({ + name: name, + size: parseInt(match[2], 16), // size + vmaStart: vmaStart, // vma + vmaStartOrig: vmaOrig, + lmaStart: parseInt(match[4], 16), // lma + attrs: attrs + }); + this.memoryRegions.push(region); + } else { + const memRegionsEnd = RegExp(/^SYMBOL TABLE:/); + if (memRegionsEnd.test(line)) { + this.objdumpReader.callback = this.readObjdumpSymbolLine.bind(this, symF); + } + } + return true; + } + + private readObjdumpSymbolLine(symF: SymbolFile, line: string, err: any): boolean { + if (!line) { + return line === '' ? true : false; + } + const match = line.match(OBJDUMP_SYMBOL_RE); + if (match) { + if (match[7] === 'd' && match[8] === 'f') { + if (match[11]) { + this.currentObjDumpFile = SymbolTable.NormalizePath(match[11].trim()); + } else { + // This can happen with C++. Inline and template methods/variables/functions/etc. are listed with + // an empty file association. So, symbols after this line can come from multiple compilation + // units with no clear owner. These can be locals, globals or other. + this.currentObjDumpFile = null; + } + } + const type = TYPE_MAP[match[8]]; + const scope = SCOPE_MAP[match[2]]; + let name = match[11].trim(); + let hidden = false; + + if (name.startsWith('.hidden')) { + name = name.substring(7).trim(); + hidden = true; + } + + const secName = match[9].trim(); + const offset = symF.offset || 0; + const addr = parseInt(match[1], 16); + const section = symF.sectionMap[secName]; + const newaddr = addr + (section ? addr - section.addressOrig : offset); + const sym: SymbolInformation = { + addressOrig: addr, + address: newaddr, + name: name, + file: this.currentObjDumpFile, + type: type, + scope: scope, + section: match[9].trim(), + length: parseInt(match[10], 16), + isStatic: (scope === SymbolScope.Local) && this.currentObjDumpFile ? true : false, + instructions: null, + hidden: hidden + }; + this.addSymbol(sym); + } + return true; + } + + private loadFromObjdumpAndNm() { + return new Promise(async (resolves, reject) => { + let rejected = false; + const objdumpPromises: ExecPromise[] = []; + for (const symbolFile of this.executables) { + const executable = symbolFile.file; + if (!validateELFHeader(executable)) { + this.gdbSession.handleMsg('log', + `Warn: ${executable} is not an ELF file format. Some features won't work -- Globals, Locals, disassembly, etc.`); + continue; + } + try { + const spawnOpts = {cwd: this.gdbSession.args.cwd}; + const objdumpStart = Date.now(); + const objDumpArgs = [ + '--syms', // Of course, we want symbols + '-C', // Demangle + '-h', // Want section headers + '-w', // Don't wrap lines (wide format) + executable]; + this.currentObjDumpFile = null; + this.objdumpReader = new SpawnLineReader(); + this.objdumpReader.on('error', (e) => { + rejected = true; + reject(e); + }); + this.objdumpReader.on('exit', (code, signal) => { + console.log('objdump exited', code, signal); + }); + this.objdumpReader.on('close', (code, signal) => { + this.objdumpReader = undefined; + this.currentObjDumpFile = null; + if (trace || this.gdbSession.args.showDevDebugOutput) { + const ms = Date.now() - objdumpStart; + this.gdbSession.handleMsg('log', `Finished reading symbols from objdump: Time: ${ms} ms\n`); + } + }); + + if (trace || this.gdbSession.args.showDevDebugOutput) { + this.gdbSession.handleMsg('log', `Reading symbols from ${this.objdumpPath} ${objDumpArgs.join(' ')}\n`); + } + objdumpPromises.push({ + args: [this.objdumpPath, ...objDumpArgs], + // tslint:disable-next-line: max-line-length + promise: this.objdumpReader.startWithProgram(this.objdumpPath, objDumpArgs, spawnOpts, this.readObjdumpHeaderLine.bind(this, symbolFile)) + }); + + const nmStart = Date.now(); + const nmProg = replaceProgInPath(this.objdumpPath, /objdump/i, 'nm'); + const nmArgs = [ + '--defined-only', + '-S', // Want size as well + '-l', // File/line info + '-C', // Demangle + '-p', // do bother sorting + // Do not use posix format. It is inaccurate + executable + ]; + const nmReader = new SpawnLineReader(); + nmReader.on('error', (e) => { + this.gdbSession.handleMsg('log', `Error: ${nmProg} failed! statics/global/functions may not be properly classified: ${e.toString()}\n`); + this.gdbSession.handleMsg('log', ' Expecting `nm` next to `objdump`. If that is not the problem please report this.\n'); + this.nmPromises = []; + }); + nmReader.on('exit', (code, signal) => { + // console.log('nm exited', code, signal); + }); + nmReader.on('close', () => { + if (trace || this.gdbSession.args.showDevDebugOutput) { + const ms = Date.now() - nmStart; + this.gdbSession.handleMsg('log', `Finished reading symbols from nm: Time: ${ms} ms\n`); + } + }); + + if (trace || this.gdbSession.args.showDevDebugOutput) { + this.gdbSession.handleMsg('log', `Reading symbols from ${nmProg} ${nmArgs.join(' ')}\n`); + } + this.nmPromises.push({ + args: [nmProg, ...nmArgs], + promise: nmReader.startWithProgram(nmProg, nmArgs, spawnOpts, this.readNmSymbolLine.bind(this, symbolFile)) + }); + } + catch (e) { + if (!rejected) { + rejected = true; + reject(e); + return; + } + } + } + // Yes, we launch both programs and wait for both to finish. Running them back to back + // takes almost twice as much time. Neither should technically fail. + await this.waitOnProgs(objdumpPromises); + // Yes, we don't wait for this. We have enough to move on + this.finishNmSymbols(); + resolves(); + }); + } + + private async waitOnProgs(promises: ExecPromise[]): Promise { + for (const p of promises) { + try { + await p.promise; + } + catch (e) { + this.gdbSession.handleMsg('log', `Failed running: ${[p.args.join(' ')]}.\n ${e}`); + } + } + return Promise.resolve(); + } + + private finishNmSymbolsPromise: Promise; + private finishNmSymbols(): Promise { + if (!this.nmPromises.length) { + return Promise.resolve(); + } + if (this.finishNmSymbolsPromise) { + return this.finishNmSymbolsPromise; + } + + this.finishNmSymbolsPromise = new Promise(async (resolve) => { + try { + await this.waitOnProgs(this.nmPromises); + // This part needs to run after both of the above finished + for (const item of this.addressToFileOrig) { + const sym = this.symbolsByAddressOrig.get(item[0]); + if (sym) { + sym.file = item[1]; + } else { + console.error('Unknown symbol address. Need to investigate', hexFormat(item[0]), item); + } + } + } + catch (e) { + // console.log('???'); + } + finally { + this.addressToFileOrig.clear(); + this.nmPromises = []; + } + }); + return this.finishNmSymbolsPromise; + } + + private addressToFileOrig: Map = new Map(); // These are addresses used before re-mapped via symbol-files + private readNmSymbolLine(symF: SymbolFile, line: string, err: any): boolean { + const match = line && line.match(NM_SYMBOL_RE); + if (match) { + const offset = symF.offset || 0; + const address = parseInt(match[1], 16) + offset; + const file = SymbolTable.NormalizePath(match[2]); + this.addressToFileOrig.set(address, file); + this.addPathVariations(file); + } + return true; + } + + public updateSymbolSize(node: SymbolNode, len: number) { + this.symbolsAsIntervalTree.remove(node); + node.symbol.length = len; + node = new SymbolNode(node.symbol, node.low, node.low + len - 1); + this.symbolsAsIntervalTree.insert(node); + } + + private sortGlobalVars() { + // We only sort globalVars. Want to preserve statics original order though. + this.globalVars.sort((a, b) => a.name.localeCompare(b.name, undefined, {sensitivity: 'base'})); + + // double underscore variables are less interesting. Push it down to the bottom + const doubleUScores: SymbolInformation[] = []; + while (this.globalVars.length > 0) { + if (this.globalVars[0].name.startsWith('__')) { + doubleUScores.push(this.globalVars.shift()); + } else { + break; + } + } + this.globalVars = this.globalVars.concat(doubleUScores); + } + + private categorizeSymbols() { + for (const sym of this.allSymbols) { + const scope = sym.scope; + const type = sym.type; + if (scope !== SymbolScope.Local) { + if (type === SymbolType.Function) { + sym.scope = SymbolScope.Global; + this.globalFuncsMap[sym.name] = sym; + } else if (type === SymbolType.Object) { + if (scope === SymbolScope.Global) { + this.globalVars.push(sym); + } else { + // These fail gdb create-vars. So ignoring them. C++ generates them. + if (debugConsoleLogging) { + console.log('SymbolTable: ignoring non local object: ' + sym.name); + } + } + } + } else if (sym.file) { + // Yes, you can have statics with no file association in C++. They are neither + // truly global or local. Some can be considered global but not sure how to filter. + if (type === SymbolType.Object) { + this.staticVars.push(sym); + } else if (type === SymbolType.Function) { + const tmp = this.staticFuncsMap[sym.name]; + if (tmp) { + tmp.push(sym); + } else { + this.staticFuncsMap[sym.name] = [sym]; + } + } + } else if (type === SymbolType.Function) { + sym.scope = SymbolScope.Global; + this.globalFuncsMap[sym.name] = sym; + } else if (type === SymbolType.Object) { + // We are currently ignoring Local objects with no file association for objects. + // Revisit later with care and decide how to classify them + if (debugConsoleLogging) { + console.log('SymbolTable: ignoring local object: ' + sym.name); + } + } + } + } + + public printSyms(cb?: (str: string) => any) { + cb = cb || console.log; + for (const sym of this.allSymbols) { + let str = sym.name ; + if (sym.type === SymbolType.Function) { + str += ' (f)'; + } else if (sym.type === SymbolType.Object) { + str += ' (o)'; + } + if (sym.file) { + str += ' (s)'; + } + cb(str); + if (sym.file) { + const maps = this.fileMap[sym.file]; + if (maps) { + for (const f of maps) { + cb('\t' + f); + } + } else { + cb('\tNoMap for? ' + sym.file); + } + } + } + } + + public printToFile(fName: string): void { + try { + const outFd = fs.openSync(fName, 'w'); + this.printSyms((str) => { + fs.writeSync(outFd, str); + fs.writeSync(outFd, '\n'); + }); + fs.closeSync(outFd); + } + catch (e) { + console.log('printSymsToFile: failed' + e); + } + } + + private addToFileMap(key: string, newMap: string): string[] { + newMap = SymbolTable.NormalizePath(newMap); + const value = this.fileMap[key] || []; + if (value.indexOf(newMap) === -1) { + value.push(newMap); + } + this.fileMap[key] = value; + return value; + } + + private addPathVariations(fileString: string) { + const curName = SymbolTable.NormalizePath(fileString); + const curSimpleName = path.basename(curName); + this.addToFileMap(curSimpleName, curSimpleName); + this.addToFileMap(curSimpleName, curName); + this.addToFileMap(curName, curSimpleName); + return { curSimpleName, curName }; + } + + protected getFileNameHashed(fileName: string): string { + try { + fileName = SymbolTable.NormalizePath(fileName); + const schemaVer = SymbolTable.CurrentVersion; // Please increment if schema changes or how objdump is invoked changes + const stats = fs.statSync(fileName); // Can fail + const cwd = process.cwd; + const str = `${fileName}-${stats.mtimeMs}-${cwd}`; + const hasher = crypto.createHash('sha256'); + hasher.update(str); + const ret = hasher.digest('hex'); + return ret; + } + catch (e) { + throw(e); + } + } + + private createFileMapCacheFileName(fileName: string, suffix = '') { + const hash = this.getFileNameHashed(fileName) + suffix + '.json'; + const fName = path.join(os.tmpdir(), 'Cortex-Debug-' + hash); + return fName; + } + + public getFunctionAtAddress(address: number): SymbolInformation { + const symNodes = this.symbolsAsIntervalTree.search(address, address); + for (const symNode of symNodes) { + if (symNode && (symNode.symbol.type === SymbolType.Function)) { + return symNode.symbol; + } + } + return null; + // return this.allSymbols.find((s) => s.type === SymbolType.Function && s.address <= address && (s.address + s.length) > address); + } + + public getFunctionSymbols(): SymbolInformation[] { + return this.allSymbols.filter((s) => s.type === SymbolType.Function); + } + + public getGlobalVariables(): SymbolInformation[] { + return this.globalVars; + } + + public async getStaticVariableNames(file: string): Promise { + if (this.varsByFile) { + const nfile = SymbolTable.NormalizePath(file); + const obj = this.varsByFile[nfile] || this.varsByFile[file]; + if (obj) { + return obj.staticNames; // Could be empty array + } + return null; + } + await this.finishNmSymbols(); + const syms = this.getStaticVariables(file); + const ret = syms.map((s) => s.name); + return ret; + } + + public getStaticVariables(file: string): SymbolInformation[] { + if (!file) { + return []; + } + const nfile = SymbolTable.NormalizePath(file); + let ret = this.staticsByFile[file]; + if (!ret) { + ret = []; + for (const s of this.staticVars) { + if ((s.file === nfile) || (s.file === file)) { + ret.push(s); + } else { + const maps = this.fileMap[s.file]; + if (maps && (maps.indexOf(nfile) !== -1)) { + ret.push(s); + } else if (maps && (maps.indexOf(file) !== -1)) { + ret.push(s); + } + } + } + this.staticsByFile[file] = ret; + } + return ret; + } + + public getFunctionByName(name: string, file?: string): SymbolInformation { + if (file) { // Try to find static function first + const nfile = SymbolTable.NormalizePath(file); + const syms = this.staticFuncsMap[name]; + if (syms) { + for (const s of syms) { // Try exact matches first (maybe not needed) + if ((s.file === file) || (s.file === nfile)) { return s; } + } + for (const s of syms) { // Try any match + const maps = this.fileMap[s.file]; // Bunch of files/aliases that may have the same symbol name + if (maps && (maps.indexOf(nfile) !== -1)) { + return s; + } else if (maps && (maps.indexOf(file) !== -1)) { + return s; + } + } + } + } + + // Fall back to global scope + const ret = this.globalFuncsMap[name]; + return ret; + } + + public getGlobalOrStaticVarByName(name: string, file?: string): SymbolInformation { + if (!file && this.rttSymbol && (name === this.rttSymbolName) ) { + return this.rttSymbol; + } + + if (file) { // If a file is given only search for static variables by file + const nfile = SymbolTable.NormalizePath(file); + for (const s of this.staticVars) { + if ((s.name === name) && ((s.file === file) || (s.file === nfile))) { + return s; + } + } + return null; + } + + // Try globals first and then statics + for (const s of this.globalVars.concat(this.staticVars)) { + if (s.name === name) { + return s; + } + } + + return null; + } + + public loadSymbolsFromGdb(waitOn: Promise): Promise { + return new Promise((resolve, reject) => { + waitOn.then(async () => { + if (true) { + // gdb is un-reliable for getting symbol information. Most of the time it works but we have + // reports of it taking 30+ seconds to dump symbols from small executable (C++) and we have + // also seen it run out of memory and crash on a well decked out Mac. Also seen asserts. + resolve(true); + return; + } + /* + if (!this.gdbSession.miDebugger.gdbVarsPromise) { + resolve(false); + return; + } + try { + const result = await this.gdbSession.miDebugger.gdbVarsPromise; + nextTick(() => { + this.varsByFile = {}; + const dbgInfo = result.result('symbols.debug'); + for (const item of dbgInfo || []) { + const fullname = getProp(item, 'fullname'); + const filename = getProp(item, 'fullname'); + const symbols = getProp(item, 'symbols'); + if (symbols && (symbols.length > 0) && (fullname || filename)) { + const fInfo = new VariablesInFile(fullname || filename); + for (const sym of symbols) { + fInfo.add(sym); + } + this.varsByFile[fInfo.filename] = fInfo; + if (fullname && (fullname !== fInfo.filename)) { + this.varsByFile[fullname] = fInfo; + } + if (filename && (filename !== fInfo.filename)) { + this.varsByFile[filename] = fInfo; + } + } + } + }); + resolve(true); + } + catch (e) { + reject(e); + } + */ + }, (e) => { + reject(e); + }); + }); + } + + public static NormalizePath(pathName: string): string { + if (!pathName) { return pathName; } + if (os.platform() === 'win32') { + // Do this so path.normalize works properly + pathName = pathName.replace(/\//g, '\\'); + } else { + pathName = pathName.replace(/\\/g, '/'); + } + pathName = path.normalize(pathName); + if (os.platform() === 'win32') { + pathName = pathName.toLowerCase(); + } + return pathName; + } +} + +function getProp(ary: any, name: string): any { + if (ary) { + for (const item of ary) { + if (item[0] === name) { + return item[1]; + } + } + } + return undefined; +} +interface SymInfoFromGdb { + name: string; + line: number; +} +class VariablesInFile { + public statics: SymInfoFromGdb[] = []; + public globals: SymInfoFromGdb[] = []; + public staticNames: string[] = []; + constructor(public filename: string) {} + public add(item: MINode) { + const isStatic = (getProp(item, 'description') || '').startsWith('static'); + const tmp: SymInfoFromGdb = { + name: getProp(item, 'name'), + line: parseInt(getProp(item, 'line') || '1') + }; + if (isStatic) { + this.statics.push(tmp); + this.staticNames.push(tmp.name); + } else { + this.globals.push(tmp); + } + } +} diff --git a/src/gdb.ts b/src/gdb.ts index 6934e43..d9e6752 100644 --- a/src/gdb.ts +++ b/src/gdb.ts @@ -3,6 +3,8 @@ import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, OutputEv import { DebugProtocol } from 'vscode-debugprotocol'; import { MI2, escape } from "./backend/mi2/mi2"; import { SSHArguments, ValuesFormattingMode } from './backend/backend'; +import { GdbDisassembler } from './backend/disasm'; +import * as child_process from 'child_process'; export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { cwd: string; @@ -39,7 +41,10 @@ export interface AttachRequestArguments extends DebugProtocol.AttachRequestArgum showDevDebugOutput: boolean; } -class GDBDebugSession extends MI2DebugSession { +export class GDBDebugSession extends MI2DebugSession { + public args:LaunchRequestArguments | AttachRequestArguments; + protected disassember: GdbDisassembler; + protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { response.body.supportsGotoTargetsRequest = true; response.body.supportsHitConditionalBreakpoints = true; @@ -49,11 +54,32 @@ class GDBDebugSession extends MI2DebugSession { response.body.supportsEvaluateForHovers = true; response.body.supportsSetVariable = true; response.body.supportsStepBack = true; + + response.body.supportsDisassembleRequest = true; + response.body.supportsReadMemoryRequest = true; + response.body.supportsInstructionBreakpoints = true; + this.sendResponse(response); } + // tslint:disable-next-line: max-line-length + public sendErrorResponsePub(response: DebugProtocol.Response, codeOrMessage: number | DebugProtocol.Message, format?: string, variables?: any, dest?: any): void { + this.sendErrorResponse(response, codeOrMessage, format, variables, dest); + } + + protected disassembleRequest( response: DebugProtocol.DisassembleResponse, args: DebugProtocol.DisassembleArguments, request?: DebugProtocol.Request): void { + this.disassember.disassembleProtocolRequest(response,args,request); + } protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { + const check_gdb = child_process.exec(`which ${args.gdbpath || "gdb"}`, (e,x,stderr)=>{ + if (e || !x || x.length == 0) { + this.sendErrorResponse(response, 104, `No ${args.gdbpath || "gdb"} found, please install it.`); + return + } + }) this.miDebugger = new MI2(args.gdbpath || "gdb", ["-q", "--interpreter=mi2"], args.debugger_args, args.env); + this.args = args; + this.disassember = new GdbDisassembler(this, args.showDevDebugOutput); this.setPathSubstitutions(args.pathSubstitutions); this.initDebugger(); this.quit = false; @@ -102,6 +128,12 @@ class GDBDebugSession extends MI2DebugSession { } protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { + const check_gdb = child_process.exec(`which ${args.gdbpath || "gdb"}`, (e,x,stderr)=>{ + if (e || !x || x.length == 0) { + this.sendErrorResponse(response, 104, `No ${args.gdbpath || "gdb"} found, please install it.`); + return + } + }) this.miDebugger = new MI2(args.gdbpath || "gdb", ["-q", "--interpreter=mi2"], args.debugger_args, args.env); this.setPathSubstitutions(args.pathSubstitutions); this.initDebugger(); @@ -113,6 +145,8 @@ class GDBDebugSession extends MI2DebugSession { this.miDebugger.printCalls = !!args.printCalls; this.miDebugger.debugOutput = !!args.showDevDebugOutput; this.stopAtEntry = args.stopAtEntry; + this.args = args; + this.disassember = new GdbDisassembler(this, args.showDevDebugOutput); if (args.ssh !== undefined) { if (args.ssh.forwardX11 === undefined) args.ssh.forwardX11 = true; diff --git a/src/mibase.ts b/src/mibase.ts index 5ca012e..d1c43a9 100644 --- a/src/mibase.ts +++ b/src/mibase.ts @@ -1,7 +1,7 @@ import * as DebugAdapter from 'vscode-debugadapter'; import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, ThreadEvent, OutputEvent, ContinuedEvent, Thread, StackFrame, Scope, Source, Handles } from 'vscode-debugadapter'; import { DebugProtocol } from 'vscode-debugprotocol'; -import { Breakpoint, IBackend, Variable, VariableObject, ValuesFormattingMode, MIError } from './backend/backend'; +import { Breakpoint, IBackend, Variable, VariableObject, ValuesFormattingMode, MIError , OurInstructionBreakpoint} from './backend/backend'; import { MINode } from './backend/mi_parse'; import { expandValue, isExpandable } from './backend/gdb_expansion'; import { MI2 } from './backend/mi2/mi2'; @@ -10,6 +10,7 @@ import * as systemPath from "path"; import * as net from "net"; import * as os from "os"; import * as fs from "fs"; +import { hexFormat } from './backend/common'; class ExtendedVariable { constructor(public name, public options) { @@ -20,7 +21,7 @@ export enum RunCommand { CONTINUE, RUN, NONE } const STACK_HANDLES_START = 1000; const VAR_HANDLES_START = 512 * 256 + 1000; - +const VARIABLES_TAG_REGISTER = 0xab; export class MI2DebugSession extends DebugSession { protected variableHandles = new Handles(VAR_HANDLES_START); protected variableHandlesReverse: { [id: string]: number } = {}; @@ -89,6 +90,9 @@ export class MI2DebugSession extends DebugSession { } } + public get_miDebugger():MI2{ + return this.miDebugger; + } protected setValuesFormattingMode(mode: ValuesFormattingMode) { switch (mode) { case "disabled": @@ -106,7 +110,7 @@ export class MI2DebugSession extends DebugSession { } } - protected handleMsg(type: string, msg: string) { + public handleMsg(type: string, msg: string) { if (type == "target") type = "stdout"; if (type == "log") @@ -190,7 +194,26 @@ export class MI2DebugSession extends DebugSession { value: res.result("value") }; } else { - await this.miDebugger.changeVariable(args.name, args.value); + /* 字符串不修改 */ + if (args.value[0] == "\"") { + this.sendErrorResponse(response, 20, "Could not modify string variable"); + return; + } + let name; + let id; + if (args.variablesReference >= VAR_HANDLES_START) { + const pointerCombineChar = "."; + id = this.variableHandles.get(args.variablesReference); + if (typeof id == "string") { + name = id[0] == '*' ? id.substr(1):id + pointerCombineChar + args.name; + } + } + /* 修复寄存器无法修改 */ + else if (args.variablesReference == VARIABLES_TAG_REGISTER) { + name = `$${args.name}`; + } + + await this.miDebugger.changeVariable(name, args.value); response.body = { value: args.value }; @@ -259,6 +282,40 @@ export class MI2DebugSession extends DebugSession { this.sendErrorResponse(response, 9, msg.toString()); }); } + protected setInstructionBreakpointsRequest( + response: DebugProtocol.SetInstructionBreakpointsResponse, + args: DebugProtocol.SetInstructionBreakpointsArguments, request?: DebugProtocol.Request): void { + this.miDebugger.clearBreakPoints().then(() => { + const all = args.breakpoints.map(brk => { + const addr = parseInt(brk.instructionReference) + brk.offset || 0; + const bpt: OurInstructionBreakpoint = { ...brk, number: -1, address: addr }; + return this.miDebugger.addInstrBreakPoint(bpt).catch((err: MIError) => err); + }); + Promise.all(all).then(brkpoints => { + response.body = { + breakpoints: brkpoints.map((bp) => { + if (bp instanceof MIError) { + return { + verified: false, + message: bp.message + } as DebugProtocol.Breakpoint; + } + + return { + id: bp.number, + verified: true + }; + }) + }; + this.sendResponse(response); + + }, msg => { + this.sendErrorResponse(response, 9, msg.toString()); + }); + }, msg => { + this.sendErrorResponse(response, 9, msg.toString()); + }); + } protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { if (!this.miDebugger) { @@ -313,13 +370,14 @@ export class MI2DebugSession extends DebugSession { } source = new Source(element.fileName, path); } - - ret.push(new StackFrame( + let tmp_stackFrame = new StackFrame( this.threadAndLevelToFrameId(args.threadId, element.level), element.function + "@" + element.address, source, element.line, - 0)); + 0); + tmp_stackFrame.instructionPointerReference = element.address + ret.push(tmp_stackFrame); }); response.body = { stackFrames: ret @@ -401,7 +459,7 @@ export class MI2DebugSession extends DebugSession { protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { const scopes = new Array(); scopes.push(new Scope("Local", STACK_HANDLES_START + (parseInt(args.frameId as any) || 0), false)); - + scopes.push(new Scope("Registers", VARIABLES_TAG_REGISTER, false)); response.body = { scopes: scopes }; @@ -411,6 +469,16 @@ export class MI2DebugSession extends DebugSession { protected async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): Promise { const variables: DebugProtocol.Variable[] = []; let id: number | string | VariableObject | ExtendedVariable; + + if (args.variablesReference == VARIABLES_TAG_REGISTER) { + var results = await this.miDebugger.getRegisters(); + response.body = { + variables: results + }; + this.sendResponse(response); + return; + } + if (args.variablesReference < VAR_HANDLES_START) { id = args.variablesReference - STACK_HANDLES_START; } else { @@ -491,8 +559,9 @@ export class MI2DebugSession extends DebugSession { } else variables.push({ name: variable.name, + evaluateName: variable.name, type: variable.type, - value: "", + value: variable.type, variablesReference: createVariable(variable.name) }); } @@ -569,7 +638,13 @@ export class MI2DebugSession extends DebugSession { // TODO: this evals on an (effectively) unknown thread for multithreaded programs. const variable = await this.miDebugger.evalExpression(JSON.stringify(`${varReq.name}+${arrIndex})`), 0, 0); try { - const expanded = expandValue(createVariable, variable.result("value"), varReq.name, variable); + let root = varReq.name; + const t = varReq.name.indexOf('(');//如果变量名称带 ( 说明它是一个参数 + let name:string = varReq.name; + if (t) { + root = name.substring(0,t) + name.substring(t+1); + } + const expanded = expandValue(createVariable, variable.result("value"), root, variable); if (!expanded) { this.sendErrorResponse(response, 15, `Could not expand variable`); } else { @@ -581,7 +656,7 @@ export class MI2DebugSession extends DebugSession { return submit(); } else if (expanded[0] != '"') { strArr.push({ - name: "[err]", + name: "value", value: expanded, variablesReference: 0 }); @@ -594,10 +669,17 @@ export class MI2DebugSession extends DebugSession { }); addOne(); } else { - strArr.push({ - name: "[err]", - value: expanded, - variablesReference: 0 + expanded.forEach(element => { + let evaluateName; + if (this.variableHandlesReverse.hasOwnProperty(element.name)) { + evaluateName = this.variableHandlesReverse[element.name]; + } + strArr.push({ + name: element.name, + evaluateName: element.evaluateName, + value: element.value, + variablesReference: element.variablesReference + }); }); submit(); } @@ -648,7 +730,7 @@ export class MI2DebugSession extends DebugSession { } protected stepBackRequest(response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments): void { - this.miDebugger.step(true).then(done => { + this.miDebugger.step(true, args.granularity === 'instruction').then(done => { this.sendResponse(response); }, msg => { this.sendErrorResponse(response, 4, `Could not step back: ${msg} - Try running 'target record-full' before stepping back`); @@ -656,7 +738,7 @@ export class MI2DebugSession extends DebugSession { } protected stepInRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { - this.miDebugger.step().then(done => { + this.miDebugger.step(false, args.granularity === 'instruction').then(done => { this.sendResponse(response); }, msg => { this.sendErrorResponse(response, 4, `Could not step in: ${msg}`); @@ -682,10 +764,11 @@ export class MI2DebugSession extends DebugSession { protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { const [threadId, level] = this.frameIdToThreadAndLevel(args.frameId); if (args.context == "watch" || args.context == "hover") { - this.miDebugger.evalExpression(args.expression, threadId, level).then((res) => { + /* 悬停查看变量时不报错 */ + this.miDebugger.evalExpression(args.expression, threadId, level, args.context == "hover").then((res) => { response.body = { variablesReference: 0, - result: res.result("value") + result: args.expression[0] == '$' ? hexFormat(parseInt(res.result("value"))) : res.result("value") }; this.sendResponse(response); }, msg => {