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..f5114b76 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,26 @@ 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())); + 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(); @@ -231,12 +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); } + onDataDispose = term.onData(onDataHandler).dispose; lblConsoleFor.style.display = "block"; lblConsoleBaudrate.style.display = "none"; consoleBaudrates.style.display = "none"; @@ -248,6 +285,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 +296,23 @@ 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 () => { + onDataDispose(); isConsoleClosed = true; if (transport) { await transport.disconnect(); @@ -277,6 +329,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..aa288cf3 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.5", "pako": "^2.1.0", "tslib": "^2.4.1" }, @@ -2524,6 +2525,12 @@ "node": ">=12.0.0" } }, + "node_modules/jselftools": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/jselftools/-/jselftools-0.2.5.tgz", + "integrity": "sha512-OV1ef2ocEVa2BCYL3FO9kEaGsPNqbWb20ssnwnWOjxlZTSpGsrgIYJK083B4hJl+5zc7bLed3Aw3Y4zfAGLeZg==", + "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.5", + "resolved": "https://registry.npmjs.org/jselftools/-/jselftools-0.2.5.tgz", + "integrity": "sha512-OV1ef2ocEVa2BCYL3FO9kEaGsPNqbWb20ssnwnWOjxlZTSpGsrgIYJK083B4hJl+5zc7bLed3Aw3Y4zfAGLeZg==" + }, "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..c0daaafc 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ }, "dependencies": { "atob-lite": "^2.0.0", + "jselftools": "^0.2.5", "pako": "^2.1.0", "tslib": "^2.4.1" }, diff --git a/src/address_decoder.ts b/src/address_decoder.ts new file mode 100644 index 00000000..7d172a55 --- /dev/null +++ b/src/address_decoder.ts @@ -0,0 +1,221 @@ +import ELFFile, { CompileUnit, DWARFInfo } from "jselftools"; +import { AddressLocation } from "./types/decoder"; +import { getSHA256 } from "./util"; + +const ADDRESS_RE = /0x[0-9a-f]{8}/gi; + +type SubprogramInfo = [start: number, end: number, fnName: string, dwarfinfo?: DWARFInfo, CU?: CompileUnit]; + +/** + * Class to check and decode an address + */ +export class AddressDecoder { + private static subprograms: SubprogramInfo[][] = []; + private static sha: string[] = []; + private static intervals: number[][] = []; + + /** + * load elf files and filter for faster address decoding + * @param {ArrayBufferLike[]} elfFileBuffers elf file buffers + */ + static async update(elfFileBuffers: ArrayBufferLike[]): Promise { + this.subprograms = []; + this.sha = []; + for (const elfFileBuffer of elfFileBuffers) { + const { subprograms, isRom } = await this.loadElfFile(elfFileBuffer); + if (!isRom) { + this.sha.push(await getSHA256(elfFileBuffer)); + } + const start = subprograms[0][0]; + const end = subprograms[subprograms.length - 1][1]; + this.intervals.push([start, end]); + this.subprograms.push(subprograms); + } + } + + /** + * load elf file and parse it + * @param {ArrayBufferLike} elfFileBuffer elf file buffer + * @returns {Promise} sorted subprograms + */ + static async loadElfFile(elfFileBuffer: ArrayBufferLike): Promise<{ + subprograms: SubprogramInfo[]; + isRom: boolean; + }> { + const elffile = new ELFFile(elfFileBuffer); + 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]); + } + } + } + } + } + } + } 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]); + } + } + } + } + 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 { + 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, + }; + } + } + i++; + } + return undefined; + } + + /** + * decode the address and call the output function with the decoded address + * @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) { + return false; + } + const hexAddress = address.toString(16); + 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})`; + } + } else { + decodedLine += " in ROM"; + } + outputFn(decodedLine); + return true; + } + + static parser(line: string, outputFn: (message: string) => void) { + const parserOutput = (line: string) => { + outputFn("\x1b[33m-- " + line + "\x1b[0m"); + }; + const match = line.match(ADDRESS_RE); + if (match) { + outputFn(line); + 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 && this.sha) { + outputFn(line); + 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; + } + } + 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, dwarfinfo: DWARFInfo): AddressLocation | undefined { + 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/index.ts b/src/index.ts index 438f978f..84a5b1a1 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 "./address_decoder"; 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; +}