增加汇编调试功能
Signed-off-by: Haoyang Chen <chenhaoyang@kylinos.cn>
This commit is contained in:
parent
fd7d8ee164
commit
fc6a3de435
|
@ -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",
|
||||
|
|
|
@ -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,15 +69,18 @@ export interface IBackend {
|
|||
stepOut(): Thenable<boolean>;
|
||||
loadBreakPoints(breakpoints: Breakpoint[]): Thenable<[boolean, Breakpoint][]>;
|
||||
addBreakPoint(breakpoint: Breakpoint): Thenable<[boolean, Breakpoint]>;
|
||||
addInstrBreakPoint(breakpoint: OurInstructionBreakpoint): Promise<OurInstructionBreakpoint>;
|
||||
removeBreakPoint(breakpoint: Breakpoint): Thenable<boolean>;
|
||||
clearBreakPoints(source?: string): Thenable<any>;
|
||||
getThreads(): Thenable<Thread[]>;
|
||||
getStack(startFrame: number, maxLevels: number, thread: number): Thenable<Stack[]>;
|
||||
getRegisters(): Promise<any[]>;
|
||||
getStackVariables(thread: number, frame: number): Thenable<Variable[]>;
|
||||
evalExpression(name: string, thread: number, frame: number): Thenable<any>;
|
||||
isReady(): boolean;
|
||||
changeVariable(name: string, rawValue: string): Thenable<any>;
|
||||
examineMemory(from: number, to: number): Thenable<any>;
|
||||
record(): Thenable<any>;
|
||||
}
|
||||
|
||||
export class VariableObject {
|
||||
|
|
|
@ -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<boolean>;
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public startWithProgram(
|
||||
prog: string, args: readonly string[] = [],
|
||||
spawnOpts: childProcess.SpawnOptions = {}, cb: (line: string) => boolean = null): Promise<boolean> {
|
||||
if (this.promise) { throw new Error('SpawnLineReader: can\'t reuse this object'); }
|
||||
this.callback = cb;
|
||||
this.promise = new Promise<boolean>((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<boolean> {
|
||||
if (this.promise) { throw new Error('SpawnLineReader: can\'t reuse this object'); }
|
||||
this.callback = cb;
|
||||
this.promise = new Promise<boolean>((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<boolean> {
|
||||
if (this.promise) { throw new Error('SpawnLineReader: can\'t reuse this object'); }
|
||||
this.callback = cb;
|
||||
this.promise = new Promise<boolean>((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);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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<any> {
|
||||
|
@ -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) {
|
||||
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<boolean> {
|
||||
next(reverse: boolean = false, instruction?: boolean): Thenable<boolean> {
|
||||
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<boolean> {
|
||||
step(reverse: boolean = false, instruction?: boolean): Thenable<boolean> {
|
||||
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<OurInstructionBreakpoint> {
|
||||
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<boolean> {
|
||||
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<any[]> {
|
||||
|
||||
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,
|
||||
value: value,
|
||||
type: type,
|
||||
variablesReference: 0
|
||||
});
|
||||
reg_index++;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
async getStackVariables(thread: number, frame: number): Promise<Variable[]> {
|
||||
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);
|
||||
});
|
||||
|
@ -796,6 +902,9 @@ export class MI2 extends EventEmitter implements IBackend {
|
|||
let command = "data-evaluate-expression ";
|
||||
if (thread != 0) {
|
||||
command += `--thread ${thread} --frame ${frame} `;
|
||||
if (this.status == 'stopped'){
|
||||
this.currentThreadId = thread;
|
||||
}
|
||||
}
|
||||
command += name;
|
||||
|
||||
|
@ -873,8 +982,10 @@ export class MI2 extends EventEmitter implements IBackend {
|
|||
|
||||
sendCommand(command: string, suppressFailure: boolean = false): Thenable<MINode> {
|
||||
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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<any>;
|
||||
}
|
||||
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<SymbolNode> = new IntervalTree<SymbolNode>();
|
||||
public symbolsByAddress: Map<number, SymbolInformation> = new Map<number, SymbolInformation>();
|
||||
public symbolsByAddressOrig: Map<number, SymbolInformation> = new Map<number, SymbolInformation>();
|
||||
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<boolean> {
|
||||
return new Promise<boolean>((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<void> {
|
||||
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<void>(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<void> {
|
||||
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<void>;
|
||||
private finishNmSymbols(): Promise<void> {
|
||||
if (!this.nmPromises.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (this.finishNmSymbolsPromise) {
|
||||
return this.finishNmSymbolsPromise;
|
||||
}
|
||||
|
||||
this.finishNmSymbolsPromise = new Promise<void>(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<number, string> = new Map<number, string>(); // 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<string[]> {
|
||||
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<void>): Promise<boolean> {
|
||||
return new Promise<boolean>((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);
|
||||
}
|
||||
}
|
||||
}
|
23
src/gdb.ts
23
src/gdb.ts
|
@ -3,6 +3,7 @@ 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';
|
||||
|
||||
export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
|
||||
cwd: string;
|
||||
|
@ -39,7 +40,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 +53,26 @@ 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 {
|
||||
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;
|
||||
|
@ -113,6 +132,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;
|
||||
|
|
|
@ -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';
|
||||
|
@ -20,7 +20,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<string | VariableObject | ExtendedVariable>(VAR_HANDLES_START);
|
||||
protected variableHandlesReverse: { [id: string]: number } = {};
|
||||
|
@ -89,6 +89,9 @@ export class MI2DebugSession extends DebugSession {
|
|||
}
|
||||
}
|
||||
|
||||
public get_miDebugger():MI2{
|
||||
return this.miDebugger;
|
||||
}
|
||||
protected setValuesFormattingMode(mode: ValuesFormattingMode) {
|
||||
switch (mode) {
|
||||
case "disabled":
|
||||
|
@ -106,7 +109,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 +193,8 @@ export class MI2DebugSession extends DebugSession {
|
|||
value: res.result("value")
|
||||
};
|
||||
} else {
|
||||
await this.miDebugger.changeVariable(args.name, args.value);
|
||||
let name = args.variablesReference == VARIABLES_TAG_REGISTER ? `$${args.name}` : args.name;
|
||||
await this.miDebugger.changeVariable(name, args.value);
|
||||
response.body = {
|
||||
value: args.value
|
||||
};
|
||||
|
@ -259,6 +263,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 +351,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 +440,7 @@ export class MI2DebugSession extends DebugSession {
|
|||
protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void {
|
||||
const scopes = new Array<Scope>();
|
||||
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 +450,15 @@ export class MI2DebugSession extends DebugSession {
|
|||
protected async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): Promise<void> {
|
||||
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 {
|
||||
|
@ -648,7 +696,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 +704,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}`);
|
||||
|
|
Loading…
Reference in New Issue