增加汇编调试功能

Signed-off-by: Haoyang Chen <chenhaoyang@kylinos.cn>
This commit is contained in:
Haoyang Chen 2022-10-09 10:32:35 +08:00
parent fd7d8ee164
commit fc6a3de435
9 changed files with 2622 additions and 22 deletions

View File

@ -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",

View File

@ -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 {

369
src/backend/common.ts Normal file
View File

@ -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);
}
}
}

1050
src/backend/disasm.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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;

View File

@ -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;

986
src/backend/symbols.ts Normal file
View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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}`);