From b0a9cfb4d2e36861399a1f0953126343abdc05b6 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Thu, 3 Apr 2025 22:38:58 +0200 Subject: [PATCH 1/7] Add panic address decoder and monitor input --- examples/typescript/src/index.html | 4 + examples/typescript/src/index.ts | 64 +++++++- package-lock.json | 12 ++ package.json | 1 + src/index.ts | 1 + src/panic_decoder.ts | 237 +++++++++++++++++++++++++++++ src/types/decoder.ts | 7 + src/util.ts | 12 ++ 8 files changed, 336 insertions(+), 2 deletions(-) create mode 100644 src/panic_decoder.ts create mode 100644 src/types/decoder.ts diff --git a/examples/typescript/src/index.html b/examples/typescript/src/index.html index 91299155..db84dc18 100644 --- a/examples/typescript/src/index.html +++ b/examples/typescript/src/index.html @@ -86,6 +86,10 @@

Console

+ +

+ +
diff --git a/examples/typescript/src/index.ts b/examples/typescript/src/index.ts index 5c4b7738..e4ef0179 100644 --- a/examples/typescript/src/index.ts +++ b/examples/typescript/src/index.ts @@ -19,13 +19,14 @@ const lblConsoleFor = document.getElementById("lblConsoleFor"); const lblConnTo = document.getElementById("lblConnTo"); const table = document.getElementById("fileTable") as HTMLTableElement; const alertDiv = document.getElementById("alertDiv"); +const addElfFileButton = document.getElementById("addElfFile") as HTMLInputElement; const debugLogging = document.getElementById("debugLogging") as HTMLInputElement; // This is a frontend example of Esptool-JS using local bundle file // To optimize use a CDN hosted version like // https://unpkg.com/esptool-js@0.5.0/bundle.js -import { ESPLoader, FlashOptions, LoaderOptions, Transport } from "../../../lib"; +import { ESPLoader, FlashOptions, LoaderOptions, Transport, AddressDecoder } from "../../../lib"; import { serial } from "web-serial-polyfill"; const serialLib = !navigator.serial && navigator.usb ? serial : navigator.serial; @@ -72,6 +73,27 @@ function handleFileSelect(evt) { reader.readAsBinaryString(file); } +/** + * File reader handler to read given local files. + * @param {Event} evt File Select event + */ +async function handleElfFileSelect(evt) { + const files = evt.target.files; + + if (files.length === 0) return; + // get all files as an array of arrayBuffers + const elfFileBuffers = await Promise.all(Array.from(files).map((file: File) => file.arrayBuffer())); + console.log(files, elfFileBuffers); + await AddressDecoder.update(elfFileBuffers); +} + +addElfFileButton.onchange = handleElfFileSelect; + +const encoder = new TextEncoder(); +export const stringToUInt8Array = function (textString: string) { + return encoder.encode(textString); +}; + const espLoaderTerminal = { clean() { term.clear(); @@ -237,6 +259,15 @@ consoleStartButton.onclick = async () => { device = await serialLib.requestPort({}); transport = new Transport(device, true); } + term.onData((data: string) => { + const writer = transport.device.writable?.getWriter(); + if (writer) { + writer.write(stringToUInt8Array(data)); + writer.releaseLock(); + } else { + console.error("Unable to write to serial port"); + } + }); lblConsoleFor.style.display = "block"; lblConsoleBaudrate.style.display = "none"; consoleBaudrates.style.display = "none"; @@ -248,6 +279,10 @@ consoleStartButton.onclick = async () => { await transport.connect(parseInt(consoleBaudrates.value)); isConsoleClosed = false; + const output = (line: string) => { + term.writeln(line); + }; + let lastLine = ""; while (true && !isConsoleClosed) { const readLoop = transport.rawRead(); const { value, done } = await readLoop.next(); @@ -255,12 +290,24 @@ consoleStartButton.onclick = async () => { if (done || !value) { break; } - term.write(value); + + const valueStr = uInt8ArrayToString(value); + lastLine += valueStr; + const splitLine = lastLine.split("\r\n"); + while (splitLine.length > 1) { + const line = splitLine.shift(); + if (line !== undefined) { + AddressDecoder.parser(line, output); + } + } + lastLine = splitLine[0]; } console.log("quitting console"); }; consoleStopButton.onclick = async () => { + // eslint-disable-next-line @typescript-eslint/no-empty-function + term.onData = () => {}; isConsoleClosed = true; if (transport) { await transport.disconnect(); @@ -277,6 +324,19 @@ consoleStopButton.onclick = async () => { cleanUp(); }; +/** + * Convert a Uint8Array to a string + * @param {Uint8Array} fileBuffer Uint8Array to convert + * @returns {string} String representation of the Uint8Array + */ +export function uInt8ArrayToString(fileBuffer: Uint8Array): string { + let fileBufferString = ""; + for (let i = 0; i < fileBuffer.length; i++) { + fileBufferString += String.fromCharCode(fileBuffer[i]); + } + return fileBufferString; +} + /** * Validate the provided files images and offset to see if they're valid. * @returns {string} Program input validation result diff --git a/package-lock.json b/package-lock.json index de78d61b..26a3f399 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "atob-lite": "^2.0.0", + "jselftools": "^0.2.2", "pako": "^2.1.0", "tslib": "^2.4.1" }, @@ -2524,6 +2525,12 @@ "node": ">=12.0.0" } }, + "node_modules/jselftools": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/jselftools/-/jselftools-0.2.2.tgz", + "integrity": "sha512-H535wvUmAxOw+9FXok7BIYHO4MAKfx71Rw3bKgYlfV2Q9CYXC1aKhTQC1R7q358TfwktIYZSzjoqSecSOnnPPg==", + "license": "MIT" + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -5580,6 +5587,11 @@ "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", "dev": true }, + "jselftools": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/jselftools/-/jselftools-0.2.2.tgz", + "integrity": "sha512-H535wvUmAxOw+9FXok7BIYHO4MAKfx71Rw3bKgYlfV2Q9CYXC1aKhTQC1R7q358TfwktIYZSzjoqSecSOnnPPg==" + }, "jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", diff --git a/package.json b/package.json index 61fbcb8f..2795fd18 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ }, "dependencies": { "atob-lite": "^2.0.0", + "jselftools": "^0.2.2", "pako": "^2.1.0", "tslib": "^2.4.1" }, diff --git a/src/index.ts b/src/index.ts index 438f978f..b910e96a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,3 +14,4 @@ export { LoaderOptions } from "./types/loaderOptions.js"; export { FlashOptions } from "./types/flashOptions.js"; export { IEspLoaderTerminal } from "./types/loaderTerminal.js"; export { Before, After } from "./types/resetModes.js"; +export { AddressDecoder } from "./panic_decoder"; diff --git a/src/panic_decoder.ts b/src/panic_decoder.ts new file mode 100644 index 00000000..e462d400 --- /dev/null +++ b/src/panic_decoder.ts @@ -0,0 +1,237 @@ +import ELFFile, { CompileUnit, Die, DWARFInfo } from "jselftools"; +import { AddressLocation } from "./types/decoder"; +import { getSHA256 } from "./util"; + +const ADDRESS_RE = /0x[0-9a-f]{8}/gi; + +interface ParsedElfFile { + dwarfinfo: DWARFInfo; + subprograms: Die[]; + intervals: number[][]; + sha: string; +} + +interface Interval { + start: number; + end: number; + elfIdx: number; +} + +/** + * Class to check and decode an address + */ +export class AddressDecoder { + private static elfFiles: ParsedElfFile[] = []; + private static sha = ""; + private static intervals: Interval[] = []; + + /** + * load elf files and filter for faster address decoding + * @param {ArrayBufferLike[]} elfFileBuffers elf file buffers + * @param appIdx the index in the {@link elfFileBuffers} array to use for the sha + */ + static async update(elfFileBuffers: ArrayBufferLike[], appIdx = 0): Promise { + for (const elfFileBuffer of elfFileBuffers) { + this.elfFiles.push(await this.loadElfFile(elfFileBuffer)); + } + this.sha = this.elfFiles[appIdx].sha; + let i = 0; + for (const elfFile of this.elfFiles) { + for (const interval of elfFile.intervals) { + this.intervals.push({ + start: interval[0], + end: interval[1], + elfIdx: i++, + }); + } + } + this.intervals.sort((a, b) => a.start - b.start); + } + + /** + * load elf file and parse it + * @param {ArrayBufferLike} elfFileBuffer elf file buffer + * @returns {Promise} parsed elf file + */ + static async loadElfFile(elfFileBuffer: ArrayBufferLike): Promise { + const sha = await getSHA256(elfFileBuffer); + const elffile = new ELFFile(elfFileBuffer); + const dwarfinfo = elffile.get_dwarf_info(); + const subprograms: Die[] = []; + + for (const CU of dwarfinfo.get_CUs()) { + for (const die of CU.dies) { + if (die.has_children) { + for (const child of die.children) { + if (child.tag === "DW_TAG_subprogram") { + const highPc = child.attributes["DW_AT_high_pc"]; + const lowPc = child.attributes["DW_AT_low_pc"]; + if (lowPc && lowPc.value > 0 && highPc.value > 0) { + subprograms.push(child); + } + } + } + } + } + } + + const intervals: number[][] = []; + for (const section of elffile.body.sections) { + if (section.flags["execinstr"]) { + const addr = section.addr; + intervals.push([Number(addr), Number(addr) + Number(section.size)]); + } + } + intervals.sort((a, b) => a[0] - b[0]); + return { + intervals, + sha, + dwarfinfo, + subprograms, + }; + } + + /** + * get the index of the elf file that contains the address + * @param address resolves an address to an elf file index + * @returns the elf index of the elf file that contains the address or -1 if no elf file contains the address + */ + static getElfIdx(address: number): number | undefined { + for (const { start, end, elfIdx } of this.intervals) { + if (start > address) { + break; + } else if (start <= address && address < end) { + return elfIdx; + } + } + return undefined; + } + + static getDecodedAddress(address: number): { fnName: string; line: AddressLocation | undefined } | undefined { + const addressElfIdx = this.getElfIdx(address); + if (addressElfIdx === undefined) { + return undefined; + } + + const subprograms = this.elfFiles[addressElfIdx].subprograms; + for (const subprogram of subprograms) { + const lowPc = subprogram.attributes["DW_AT_low_pc"].value; + const highPc = subprogram.attributes["DW_AT_high_pc"].value + lowPc; + if (address >= lowPc && address < highPc) { + const line = this.checkLineprogram(subprogram.cu, address, addressElfIdx); + return { + fnName: subprogram.attributes["DW_AT_name"].value, + line, + }; + } + } + return undefined; + } + + /** + * decode the address and call the output function with the decoded address + * @param address the address to decode + * @param outputFn the function to call with the decoded address + * @returns true if the address was decoded, false otherwise + */ + static decode(address: number, outputFn: (message: string) => void): boolean { + const decodedAddress = this.getDecodedAddress(address); + if (decodedAddress === undefined || decodedAddress.line === undefined) { + return false; + } + const hexAddress = address.toString(16); + const { fnName, line } = decodedAddress; + const { directory, filename, lineNumber, column, discriminator } = line; + if (discriminator > 0) { + // eslint-disable-next-line prettier/prettier + outputFn("0x" + hexAddress + ": " + directory + "/" + fnName + " at " + filename + ":" + lineNumber + ":" + column + " (discriminator " + discriminator + ")"); + } else { + // eslint-disable-next-line prettier/prettier + outputFn("0x" + hexAddress + ": " + directory + "/" + fnName + " at " + filename + ":" + lineNumber + ":" + column); + } + return true; + } + + static parser(line: string, outputFn: (message: string) => void) { + const parserOutput = (line: string) => { + outputFn("\x1b[33m-- " + line + "\x1b[0m"); + }; + if (ADDRESS_RE.test(line)) { + outputFn(line); + const match = line.match(ADDRESS_RE) ?? []; + const addrMap = match.map((hex) => parseInt(hex, 16)); + let decoded = false; + for (const addr of addrMap) { + if (this.decode(addr, parserOutput)) { + decoded = true; + } + } + if (decoded) { + outputFn(""); + } + return; + } else if (line.includes("ELF file SHA256:")) { + const hash = this.extractHash(line); + if (hash) { + outputFn(line); + if (!this.sha.startsWith(hash)) { + parserOutput( + "Warning: Checksum mismatch between flashed and built applications. Checksum of built application is " + + this.sha, + ); + } + return; + } + } + outputFn(line); + } + + static extractHash(line: string): string { + const pattern = /(?:I \(\d+\) cpu_start: )?ELF file SHA256:\s+(\w+)/; + const match = line.match(pattern); + return match ? match[1] : ""; + } + + private static checkLineprogram( + cu: CompileUnit, + address: number, + elfIdx: number | undefined, + ): AddressLocation | undefined { + if (elfIdx === undefined) { + elfIdx = this.getElfIdx(address); + if (elfIdx === undefined) { + return undefined; + } + } + const dwarfinfo = this.elfFiles[elfIdx].dwarfinfo; + + const lineprog = dwarfinfo.line_program_for_CU(cu); + if (!lineprog) { + return undefined; + } + const delta = lineprog.header.version < 5 ? 1 : 0; + let prevstate = null; + for (const entry of lineprog.get_entries()) { + if (entry.state === null) { + continue; + } + if (prevstate && prevstate.address <= address && address < entry.state.address) { + const filename = lineprog.header.file_entry[prevstate.file - delta]; + const directory = lineprog.header.include_directory[filename.dir_index - delta]; + return { + directory, + filename: filename.name, + lineNumber: prevstate.line, + column: prevstate.column, + discriminator: prevstate.discriminator, + }; + } + if (entry.state.end_sequence) { + prevstate = null; + } else { + prevstate = entry.state; + } + } + return undefined; + } +} diff --git a/src/types/decoder.ts b/src/types/decoder.ts new file mode 100644 index 00000000..687cc917 --- /dev/null +++ b/src/types/decoder.ts @@ -0,0 +1,7 @@ +export interface AddressLocation { + directory: string; + filename: string; + lineNumber: number; + column: number; + discriminator: number; +} diff --git a/src/util.ts b/src/util.ts index 1a4d437e..9dcf1aa4 100644 --- a/src/util.ts +++ b/src/util.ts @@ -16,3 +16,15 @@ export function padTo(data: Uint8Array, alignment: number, padCharacter = 0xff): } return data; } + +/** + * get the SHA256 hash of an ArrayBuffer + * @param {ArrayBufferLike} arrayBuffer ArrayBuffer to hash + * @returns {string} SHA256 hash of the ArrayBuffer + */ +export async function getSHA256(arrayBuffer: ArrayBufferLike): Promise { + const hashBuffer = await crypto.subtle.digest("SHA-256", arrayBuffer); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); + return hashHex; +} From 20034f3844ffb3bf02bce7b1a322081a36298b2e Mon Sep 17 00:00:00 2001 From: Konstantin Date: Thu, 3 Apr 2025 23:19:54 +0200 Subject: [PATCH 2/7] fix small errors and parser output --- src/panic_decoder.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/panic_decoder.ts b/src/panic_decoder.ts index e462d400..a2cd2494 100644 --- a/src/panic_decoder.ts +++ b/src/panic_decoder.ts @@ -41,9 +41,10 @@ export class AddressDecoder { this.intervals.push({ start: interval[0], end: interval[1], - elfIdx: i++, + elfIdx: i, }); } + i++; } this.intervals.sort((a, b) => a.start - b.start); } @@ -144,10 +145,10 @@ export class AddressDecoder { const { directory, filename, lineNumber, column, discriminator } = line; if (discriminator > 0) { // eslint-disable-next-line prettier/prettier - outputFn("0x" + hexAddress + ": " + directory + "/" + fnName + " at " + filename + ":" + lineNumber + ":" + column + " (discriminator " + discriminator + ")"); + outputFn("0x" + hexAddress + ": " + fnName + " at " + directory + "/" + filename + ":" + lineNumber + ":" + column + " (discriminator " + discriminator + ")"); } else { // eslint-disable-next-line prettier/prettier - outputFn("0x" + hexAddress + ": " + directory + "/" + fnName + " at " + filename + ":" + lineNumber + ":" + column); + outputFn("0x" + hexAddress + ": " + fnName + " at " + directory + "/" + filename + ":" + lineNumber + ":" + column); } return true; } @@ -172,7 +173,7 @@ export class AddressDecoder { return; } else if (line.includes("ELF file SHA256:")) { const hash = this.extractHash(line); - if (hash) { + if (hash && this.sha) { outputFn(line); if (!this.sha.startsWith(hash)) { parserOutput( From 30531a2e67b8cac1e151bd906b51d85ce2b00c81 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Thu, 3 Apr 2025 23:55:09 +0200 Subject: [PATCH 3/7] Add missing JSDoc types and clean up decoder output --- examples/typescript/src/index.ts | 1 - src/panic_decoder.ts | 40 +++++++++++++++++++------------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/examples/typescript/src/index.ts b/examples/typescript/src/index.ts index e4ef0179..b09ed76d 100644 --- a/examples/typescript/src/index.ts +++ b/examples/typescript/src/index.ts @@ -83,7 +83,6 @@ async function handleElfFileSelect(evt) { if (files.length === 0) return; // get all files as an array of arrayBuffers const elfFileBuffers = await Promise.all(Array.from(files).map((file: File) => file.arrayBuffer())); - console.log(files, elfFileBuffers); await AddressDecoder.update(elfFileBuffers); } diff --git a/src/panic_decoder.ts b/src/panic_decoder.ts index a2cd2494..87e3872a 100644 --- a/src/panic_decoder.ts +++ b/src/panic_decoder.ts @@ -24,13 +24,15 @@ export class AddressDecoder { private static elfFiles: ParsedElfFile[] = []; private static sha = ""; private static intervals: Interval[] = []; + private static appIdx = 0; /** * load elf files and filter for faster address decoding * @param {ArrayBufferLike[]} elfFileBuffers elf file buffers - * @param appIdx the index in the {@link elfFileBuffers} array to use for the sha + * @param {number} appIdx the index in the {@link elfFileBuffers} array to use for the sha */ static async update(elfFileBuffers: ArrayBufferLike[], appIdx = 0): Promise { + this.appIdx = appIdx; for (const elfFileBuffer of elfFileBuffers) { this.elfFiles.push(await this.loadElfFile(elfFileBuffer)); } @@ -94,8 +96,8 @@ export class AddressDecoder { /** * get the index of the elf file that contains the address - * @param address resolves an address to an elf file index - * @returns the elf index of the elf file that contains the address or -1 if no elf file contains the address + * @param {number} address resolves an address to an elf file index + * @returns {number} the elf index of the elf file that contains the address or -1 if no elf file contains the address */ static getElfIdx(address: number): number | undefined { for (const { start, end, elfIdx } of this.intervals) { @@ -108,7 +110,9 @@ export class AddressDecoder { return undefined; } - static getDecodedAddress(address: number): { fnName: string; line: AddressLocation | undefined } | undefined { + static getDecodedAddress( + address: number, + ): { fnName: string; line: AddressLocation | undefined; isRom: boolean } | undefined { const addressElfIdx = this.getElfIdx(address); if (addressElfIdx === undefined) { return undefined; @@ -122,6 +126,7 @@ export class AddressDecoder { const line = this.checkLineprogram(subprogram.cu, address, addressElfIdx); return { fnName: subprogram.attributes["DW_AT_name"].value, + isRom: addressElfIdx !== this.appIdx, line, }; } @@ -131,25 +136,28 @@ export class AddressDecoder { /** * decode the address and call the output function with the decoded address - * @param address the address to decode - * @param outputFn the function to call with the decoded address - * @returns true if the address was decoded, false otherwise + * @param {number} address the address to decode + * @param {(message: string) => void} outputFn the function to call with the decoded address + * @returns {boolean} true if the address was decoded, false otherwise */ static decode(address: number, outputFn: (message: string) => void): boolean { const decodedAddress = this.getDecodedAddress(address); - if (decodedAddress === undefined || decodedAddress.line === undefined) { + if (decodedAddress === undefined) { return false; } const hexAddress = address.toString(16); - const { fnName, line } = decodedAddress; - const { directory, filename, lineNumber, column, discriminator } = line; - if (discriminator > 0) { - // eslint-disable-next-line prettier/prettier - outputFn("0x" + hexAddress + ": " + fnName + " at " + directory + "/" + filename + ":" + lineNumber + ":" + column + " (discriminator " + discriminator + ")"); - } else { - // eslint-disable-next-line prettier/prettier - outputFn("0x" + hexAddress + ": " + fnName + " at " + directory + "/" + filename + ":" + lineNumber + ":" + column); + const { fnName, line, isRom } = decodedAddress; + let decodedLine = "0x" + hexAddress + ": " + fnName; + if (line !== undefined) { + decodedLine += ` at ${line.directory}/${line.filename}:${line.lineNumber}:${line.column}`; + if (line.discriminator > 0) { + decodedLine += ` (discriminator ${line.discriminator})`; + } + } + if (isRom) { + decodedLine += " in ROM"; } + outputFn(decodedLine); return true; } From a04dee02236685d7ae11531906846c4a41075b03 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Sun, 6 Apr 2025 14:12:51 +0200 Subject: [PATCH 4/7] also parse symtab if there is no dwarfinfo --- package-lock.json | 14 ++-- package.json | 2 +- src/panic_decoder.ts | 194 +++++++++++++++++-------------------------- 3 files changed, 85 insertions(+), 125 deletions(-) diff --git a/package-lock.json b/package-lock.json index 26a3f399..aa288cf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "atob-lite": "^2.0.0", - "jselftools": "^0.2.2", + "jselftools": "^0.2.5", "pako": "^2.1.0", "tslib": "^2.4.1" }, @@ -2526,9 +2526,9 @@ } }, "node_modules/jselftools": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/jselftools/-/jselftools-0.2.2.tgz", - "integrity": "sha512-H535wvUmAxOw+9FXok7BIYHO4MAKfx71Rw3bKgYlfV2Q9CYXC1aKhTQC1R7q358TfwktIYZSzjoqSecSOnnPPg==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/jselftools/-/jselftools-0.2.5.tgz", + "integrity": "sha512-OV1ef2ocEVa2BCYL3FO9kEaGsPNqbWb20ssnwnWOjxlZTSpGsrgIYJK083B4hJl+5zc7bLed3Aw3Y4zfAGLeZg==", "license": "MIT" }, "node_modules/jsesc": { @@ -5588,9 +5588,9 @@ "dev": true }, "jselftools": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/jselftools/-/jselftools-0.2.2.tgz", - "integrity": "sha512-H535wvUmAxOw+9FXok7BIYHO4MAKfx71Rw3bKgYlfV2Q9CYXC1aKhTQC1R7q358TfwktIYZSzjoqSecSOnnPPg==" + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/jselftools/-/jselftools-0.2.5.tgz", + "integrity": "sha512-OV1ef2ocEVa2BCYL3FO9kEaGsPNqbWb20ssnwnWOjxlZTSpGsrgIYJK083B4hJl+5zc7bLed3Aw3Y4zfAGLeZg==" }, "jsesc": { "version": "3.1.0", diff --git a/package.json b/package.json index 2795fd18..c0daaafc 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ }, "dependencies": { "atob-lite": "^2.0.0", - "jselftools": "^0.2.2", + "jselftools": "^0.2.5", "pako": "^2.1.0", "tslib": "^2.4.1" }, diff --git a/src/panic_decoder.ts b/src/panic_decoder.ts index 87e3872a..b2fc32c0 100644 --- a/src/panic_decoder.ts +++ b/src/panic_decoder.ts @@ -1,135 +1,100 @@ -import ELFFile, { CompileUnit, Die, DWARFInfo } from "jselftools"; +import ELFFile, { CompileUnit, DWARFInfo } from "jselftools"; import { AddressLocation } from "./types/decoder"; import { getSHA256 } from "./util"; const ADDRESS_RE = /0x[0-9a-f]{8}/gi; -interface ParsedElfFile { - dwarfinfo: DWARFInfo; - subprograms: Die[]; - intervals: number[][]; - sha: string; -} - -interface Interval { - start: number; - end: number; - elfIdx: number; -} +type SubprogramInfo = [start: number, end: number, fnName: string, dwarfinfo?: DWARFInfo, CU?: CompileUnit]; /** * Class to check and decode an address */ export class AddressDecoder { - private static elfFiles: ParsedElfFile[] = []; - private static sha = ""; - private static intervals: Interval[] = []; - private static appIdx = 0; + private static subprograms: SubprogramInfo[] = []; + private static sha: string[] = []; /** * load elf files and filter for faster address decoding * @param {ArrayBufferLike[]} elfFileBuffers elf file buffers - * @param {number} appIdx the index in the {@link elfFileBuffers} array to use for the sha */ - static async update(elfFileBuffers: ArrayBufferLike[], appIdx = 0): Promise { - this.appIdx = appIdx; + static async update(elfFileBuffers: ArrayBufferLike[]): Promise { + this.subprograms = []; + this.sha = []; for (const elfFileBuffer of elfFileBuffers) { - this.elfFiles.push(await this.loadElfFile(elfFileBuffer)); - } - this.sha = this.elfFiles[appIdx].sha; - let i = 0; - for (const elfFile of this.elfFiles) { - for (const interval of elfFile.intervals) { - this.intervals.push({ - start: interval[0], - end: interval[1], - elfIdx: i, - }); + const { subprograms, isRom } = await this.loadElfFile(elfFileBuffer); + if (!isRom) { + this.sha.push(await getSHA256(elfFileBuffer)); } - i++; + this.subprograms.push(...subprograms); } - this.intervals.sort((a, b) => a.start - b.start); + this.subprograms.sort((a, b) => a[0] - b[0]); } /** * load elf file and parse it * @param {ArrayBufferLike} elfFileBuffer elf file buffer - * @returns {Promise} parsed elf file + * @returns {Promise} sorted subprograms */ - static async loadElfFile(elfFileBuffer: ArrayBufferLike): Promise { - const sha = await getSHA256(elfFileBuffer); + static async loadElfFile(elfFileBuffer: ArrayBufferLike): Promise<{ + subprograms: SubprogramInfo[]; + isRom: boolean; + }> { const elffile = new ELFFile(elfFileBuffer); - const dwarfinfo = elffile.get_dwarf_info(); - const subprograms: Die[] = []; - - for (const CU of dwarfinfo.get_CUs()) { - for (const die of CU.dies) { - if (die.has_children) { - for (const child of die.children) { - if (child.tag === "DW_TAG_subprogram") { - const highPc = child.attributes["DW_AT_high_pc"]; - const lowPc = child.attributes["DW_AT_low_pc"]; - if (lowPc && lowPc.value > 0 && highPc.value > 0) { - subprograms.push(child); + const subprograms: SubprogramInfo[] = []; + let isRom = false; + if (elffile.has_dwarf_info()) { + // most app elf files have dwarf info + const dwarfinfo = elffile.get_dwarf_info(); + for (const CU of dwarfinfo.get_CUs()) { + for (const die of CU.dies) { + if (die.has_children) { + for (const child of die.children) { + if (child.tag === "DW_TAG_subprogram") { + const lowPc = child.attributes["DW_AT_low_pc"]; + const highPc = child.attributes["DW_AT_high_pc"]; + if (lowPc && lowPc.value > 0 && highPc.value > 0) { + const fnName = child.attributes["DW_AT_name"]?.value; + subprograms.push([lowPc.value, highPc.value + lowPc.value, fnName, dwarfinfo, CU]); + } } } } } } - } - - const intervals: number[][] = []; - for (const section of elffile.body.sections) { - if (section.flags["execinstr"]) { - const addr = section.addr; - intervals.push([Number(addr), Number(addr) + Number(section.size)]); + } else { + // rom elf files don't have dwarf info# + isRom = true; + const symtab = elffile.get_symtab(); + if (symtab) { + for (const symbol of symtab.iter_symbols()) { + if (symbol.info.type == "STT_FUNC") { + const start = Number(symbol.value); + const end = start + Number(symbol.size); + subprograms.push([start, end, symbol.name]); + } + } } } - intervals.sort((a, b) => a[0] - b[0]); - return { - intervals, - sha, - dwarfinfo, - subprograms, - }; + return { subprograms, isRom }; } - /** - * get the index of the elf file that contains the address - * @param {number} address resolves an address to an elf file index - * @returns {number} the elf index of the elf file that contains the address or -1 if no elf file contains the address - */ - static getElfIdx(address: number): number | undefined { - for (const { start, end, elfIdx } of this.intervals) { + static getDecodedAddress(address: number): { fnName: string; line: AddressLocation | undefined } | undefined { + for (const [start, end, fnName, dwarfinfo, cu] of this.subprograms) { + if (end < address) { + continue; + } if (start > address) { + // already after the function break; - } else if (start <= address && address < end) { - return elfIdx; } - } - return undefined; - } - - static getDecodedAddress( - address: number, - ): { fnName: string; line: AddressLocation | undefined; isRom: boolean } | undefined { - const addressElfIdx = this.getElfIdx(address); - if (addressElfIdx === undefined) { - return undefined; - } - - const subprograms = this.elfFiles[addressElfIdx].subprograms; - for (const subprogram of subprograms) { - const lowPc = subprogram.attributes["DW_AT_low_pc"].value; - const highPc = subprogram.attributes["DW_AT_high_pc"].value + lowPc; - if (address >= lowPc && address < highPc) { - const line = this.checkLineprogram(subprogram.cu, address, addressElfIdx); - return { - fnName: subprogram.attributes["DW_AT_name"].value, - isRom: addressElfIdx !== this.appIdx, - line, - }; + let line = undefined; + if (cu && dwarfinfo) { + line = this.checkLineprogram(cu, address, dwarfinfo); } + return { + fnName: fnName, + line, + }; } return undefined; } @@ -146,15 +111,14 @@ export class AddressDecoder { return false; } const hexAddress = address.toString(16); - const { fnName, line, isRom } = decodedAddress; + const { fnName, line } = decodedAddress; let decodedLine = "0x" + hexAddress + ": " + fnName; if (line !== undefined) { decodedLine += ` at ${line.directory}/${line.filename}:${line.lineNumber}:${line.column}`; if (line.discriminator > 0) { decodedLine += ` (discriminator ${line.discriminator})`; } - } - if (isRom) { + } else { decodedLine += " in ROM"; } outputFn(decodedLine); @@ -165,9 +129,9 @@ export class AddressDecoder { const parserOutput = (line: string) => { outputFn("\x1b[33m-- " + line + "\x1b[0m"); }; - if (ADDRESS_RE.test(line)) { + const match = line.match(ADDRESS_RE); + if (match) { outputFn(line); - const match = line.match(ADDRESS_RE) ?? []; const addrMap = match.map((hex) => parseInt(hex, 16)); let decoded = false; for (const addr of addrMap) { @@ -183,11 +147,19 @@ export class AddressDecoder { const hash = this.extractHash(line); if (hash && this.sha) { outputFn(line); - if (!this.sha.startsWith(hash)) { - parserOutput( - "Warning: Checksum mismatch between flashed and built applications. Checksum of built application is " + - this.sha, - ); + if (this.sha.length !== 0) { + let foundHash = false; + for (const sha of this.sha) { + if (sha.startsWith(hash)) { + foundHash = true; + } + } + if (!foundHash) { + parserOutput( + "Warning: Checksum mismatch between flashed and built applications. Checksum of built application is " + + this.sha.join(", "), + ); + } } return; } @@ -201,19 +173,7 @@ export class AddressDecoder { return match ? match[1] : ""; } - private static checkLineprogram( - cu: CompileUnit, - address: number, - elfIdx: number | undefined, - ): AddressLocation | undefined { - if (elfIdx === undefined) { - elfIdx = this.getElfIdx(address); - if (elfIdx === undefined) { - return undefined; - } - } - const dwarfinfo = this.elfFiles[elfIdx].dwarfinfo; - + private static checkLineprogram(cu: CompileUnit, address: number, dwarfinfo: DWARFInfo): AddressLocation | undefined { const lineprog = dwarfinfo.line_program_for_CU(cu); if (!lineprog) { return undefined; From 9f6e15c896f0d5a21c4e817cd5a6fe04bd666a48 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Mon, 7 Apr 2025 14:49:45 +0200 Subject: [PATCH 5/7] Only check subprograms when address is in range --- src/panic_decoder.ts | 53 ++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/src/panic_decoder.ts b/src/panic_decoder.ts index b2fc32c0..7d172a55 100644 --- a/src/panic_decoder.ts +++ b/src/panic_decoder.ts @@ -10,8 +10,9 @@ type SubprogramInfo = [start: number, end: number, fnName: string, dwarfinfo?: D * Class to check and decode an address */ export class AddressDecoder { - private static subprograms: SubprogramInfo[] = []; + private static subprograms: SubprogramInfo[][] = []; private static sha: string[] = []; + private static intervals: number[][] = []; /** * load elf files and filter for faster address decoding @@ -25,9 +26,11 @@ export class AddressDecoder { if (!isRom) { this.sha.push(await getSHA256(elfFileBuffer)); } - this.subprograms.push(...subprograms); + const start = subprograms[0][0]; + const end = subprograms[subprograms.length - 1][1]; + this.intervals.push([start, end]); + this.subprograms.push(subprograms); } - this.subprograms.sort((a, b) => a[0] - b[0]); } /** @@ -62,7 +65,7 @@ export class AddressDecoder { } } } else { - // rom elf files don't have dwarf info# + // rom elf files don't have dwarf info isRom = true; const symtab = elffile.get_symtab(); if (symtab) { @@ -75,26 +78,38 @@ export class AddressDecoder { } } } + subprograms.sort((a, b) => a[0] - b[0]); return { subprograms, isRom }; } + /** + * Given an address, decode it and return the function name and line + * @param { number } address the address to decode + * @returns { { fnName: string; line: AddressLocation | undefined } | undefined } the decoded address or undefined if it wasn't decoded + */ static getDecodedAddress(address: number): { fnName: string; line: AddressLocation | undefined } | undefined { - for (const [start, end, fnName, dwarfinfo, cu] of this.subprograms) { - if (end < address) { - continue; - } - if (start > address) { - // already after the function - break; - } - let line = undefined; - if (cu && dwarfinfo) { - line = this.checkLineprogram(cu, address, dwarfinfo); + let i = 0; + for (const [start, end] of this.intervals) { + if (start <= address && address < end) { + for (const [start, end, fnName, dwarfinfo, cu] of this.subprograms[i]) { + if (end < address) { + continue; + } + if (start > address) { + // already after the function + break; + } + let line = undefined; + if (cu && dwarfinfo) { + line = this.checkLineprogram(cu, address, dwarfinfo); + } + return { + fnName: fnName, + line, + }; + } } - return { - fnName: fnName, - line, - }; + i++; } return undefined; } From 17d7dcbe788b7bb650b77f34420cd27fed0415f7 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Mon, 7 Apr 2025 15:10:57 +0200 Subject: [PATCH 6/7] fix monitor input --- examples/typescript/src/index.ts | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/examples/typescript/src/index.ts b/examples/typescript/src/index.ts index b09ed76d..f5114b76 100644 --- a/examples/typescript/src/index.ts +++ b/examples/typescript/src/index.ts @@ -252,21 +252,28 @@ disconnectButton.onclick = async () => { cleanUp(); }; +/** + * Handles incoming data from the terminal and writes it to the transport device. + * @param {string} data - The string data received from the terminal. + */ +function onDataHandler(data: string) { + const writer = transport.device.writable?.getWriter(); + if (writer) { + writer.write(stringToUInt8Array(data)); + writer.releaseLock(); + } else { + console.error("Unable to write to serial port"); + } +} +let onDataDispose: () => void; + let isConsoleClosed = false; consoleStartButton.onclick = async () => { if (device === null) { device = await serialLib.requestPort({}); transport = new Transport(device, true); } - term.onData((data: string) => { - const writer = transport.device.writable?.getWriter(); - if (writer) { - writer.write(stringToUInt8Array(data)); - writer.releaseLock(); - } else { - console.error("Unable to write to serial port"); - } - }); + onDataDispose = term.onData(onDataHandler).dispose; lblConsoleFor.style.display = "block"; lblConsoleBaudrate.style.display = "none"; consoleBaudrates.style.display = "none"; @@ -305,8 +312,7 @@ consoleStartButton.onclick = async () => { }; consoleStopButton.onclick = async () => { - // eslint-disable-next-line @typescript-eslint/no-empty-function - term.onData = () => {}; + onDataDispose(); isConsoleClosed = true; if (transport) { await transport.disconnect(); From 8cd41e130a65aa0c099399ffefa2e36bbe3e1233 Mon Sep 17 00:00:00 2001 From: archef2000 Date: Sat, 28 Jun 2025 21:12:02 +0000 Subject: [PATCH 7/7] rename panic_decoder to address_decoder --- src/{panic_decoder.ts => address_decoder.ts} | 0 src/index.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{panic_decoder.ts => address_decoder.ts} (100%) diff --git a/src/panic_decoder.ts b/src/address_decoder.ts similarity index 100% rename from src/panic_decoder.ts rename to src/address_decoder.ts diff --git a/src/index.ts b/src/index.ts index b910e96a..84a5b1a1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,4 +14,4 @@ export { LoaderOptions } from "./types/loaderOptions.js"; export { FlashOptions } from "./types/flashOptions.js"; export { IEspLoaderTerminal } from "./types/loaderTerminal.js"; export { Before, After } from "./types/resetModes.js"; -export { AddressDecoder } from "./panic_decoder"; +export { AddressDecoder } from "./address_decoder";