UNPKG

microvium

Version:

A compact, embeddable scripting engine for microcontrollers for executing small scripts written in a subset of JavaScript.

1,135 lines 102 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.decodeSnapshot = void 0; const IL = __importStar(require("./il")); const snapshot_il_1 = require("./snapshot-il"); const utils_1 = require("./utils"); const smart_buffer_1 = require("smart-buffer"); const crc_1 = require("crc"); const runtime_types_1 = require("./runtime-types"); const _ = __importStar(require("lodash")); const stringify_il_1 = require("./stringify-il"); const bytecode_opcodes_1 = require("./bytecode-opcodes"); const snapshot_1 = require("./snapshot"); const il_opcodes_1 = require("./il-opcodes"); /** Decode a snapshot (bytecode) to IL */ function decodeSnapshot(snapshot) { const buffer = smart_buffer_1.SmartBuffer.fromBuffer(snapshot.data); let region = []; let regionStack = []; let regionName; let regionStart = 0; const gcAllocationsRegion = []; const romAllocationsRegion = []; const processedAllocationsByOffset = new Map(); const resumePointByPhysicalAddress = new Map(); const reconstructionInfo = (snapshot instanceof snapshot_1.SnapshotClass ? snapshot.reconstructionInfo : undefined); let handlesBeginOffset = Infinity; beginRegion('Header'); const bytecodeVersion = readHeaderField8('bytecodeVersion'); const headerSize = readHeaderField8('headerSize'); const requiredEngineVersion = readHeaderField8('requiredEngineVersion'); readHeaderField8('reserved'); const bytecodeSize = readHeaderField16('bytecodeSize', false); const expectedCRC = readHeaderField16('expectedCRC', true); const requiredFeatureFlags = readHeaderField32('requiredFeatureFlags', false); if (bytecodeSize !== buffer.length) { return (0, utils_1.invalidOperation)(`Invalid bytecode file (bytecode size mismatch)`); } if (headerSize !== snapshot_il_1.HEADER_SIZE) { return (0, utils_1.invalidOperation)(`Invalid bytecode file (header size unexpected)`); } const actualCRC = (0, crc_1.crc16ccitt)(snapshot.data.slice(8)); if (actualCRC !== expectedCRC) { return (0, utils_1.invalidOperation)(`Invalid bytecode file (CRC mismatch)`); } if (bytecodeVersion !== snapshot_il_1.ENGINE_MAJOR_VERSION) { return (0, utils_1.invalidOperation)(`Bytecode version ${bytecodeVersion} is not supported`); } const snapshotInfo = { globalSlots: new Map(), functions: new Map(), exports: new Map(), allocations: new Map(), flags: new Set(), builtins: { promisePrototype: IL.undefinedValue, arrayPrototype: IL.undefinedValue, asyncCatchBlock: IL.undefinedValue, asyncContinue: IL.undefinedValue, asyncHostCallback: IL.undefinedValue, } }; // Section offsets const sectionOffsets = {}; for (let i = 0; i < runtime_types_1.mvm_TeBytecodeSection.BCS_SECTION_COUNT; i++) { sectionOffsets[i] = readHeaderField16(runtime_types_1.mvm_TeBytecodeSection[i], true); } endRegion('Header'); if (requiredEngineVersion !== snapshot_il_1.ENGINE_MINOR_VERSION) { return (0, utils_1.invalidOperation)(`Engine version ${requiredEngineVersion} is not supported (expected ${snapshot_il_1.ENGINE_MINOR_VERSION})`); } // Note: we could in future decode the ROM and HEAP sections explicitly, since // the heap is now parsable. But for the moment, just the reachable // allocations in these are decoded, and they're done so as part of // interpreting the corresponding pointers that reference each allocation. decodeFlags(); decodeImportTable(); decodeExportTable(); decodeShortCallTable(); decodeBuiltins(); decodeStringTable(); decodeGlobals(); region.push({ offset: undefined, size: undefined, content: { type: 'Region', regionName: 'ROM allocations', value: romAllocationsRegion } }); region.push({ offset: undefined, size: undefined, content: { type: 'Region', regionName: 'GC allocations', value: gcAllocationsRegion } }); (0, utils_1.hardAssert)(regionStack.length === 0); // Make sure all regions have ended finalizeRegions(region, 0, buffer.length); const disassembly = { bytecodeSize: snapshot.data.length, components: region }; return { snapshotInfo, disassembly: stringifyDisassembly(disassembly) }; function decodeBuiltins() { const { offset: builtinsOffset, size } = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_BUILTINS); (0, utils_1.hardAssert)(size === runtime_types_1.mvm_TeBuiltins.BIN_BUILTIN_COUNT * 2); const builtins = {}; beginRegion('Builtins'); for (let i = 0; i < runtime_types_1.mvm_TeBuiltins.BIN_BUILTIN_COUNT; i++) { const builtinOffset = builtinsOffset + i * 2; const builtinValue = buffer.readUInt16LE(builtinOffset); const value = decodeValue(builtinValue); region.push({ offset: builtinOffset, size: 2, content: { type: 'LabeledValue', label: `[${runtime_types_1.mvm_TeBuiltins[i]}]`, value } }); const logicalValue = getLogicalValue(value); if (logicalValue.type === 'DeletedValue') { return (0, utils_1.unexpected)(); } builtins[i] = logicalValue; } endRegion('Builtins'); snapshotInfo.builtins.promisePrototype = builtins[runtime_types_1.mvm_TeBuiltins.BIN_PROMISE_PROTOTYPE]; snapshotInfo.builtins.arrayPrototype = builtins[runtime_types_1.mvm_TeBuiltins.BIN_ARRAY_PROTO]; snapshotInfo.builtins.asyncCatchBlock = builtins[runtime_types_1.mvm_TeBuiltins.BIN_ASYNC_CATCH_BLOCK]; snapshotInfo.builtins.asyncHostCallback = builtins[runtime_types_1.mvm_TeBuiltins.BIN_ASYNC_HOST_CALLBACK]; snapshotInfo.builtins.asyncContinue = builtins[runtime_types_1.mvm_TeBuiltins.BIN_ASYNC_CONTINUE]; } function decodeFlags() { for (let i = 0; i < 32; i++) { if (requiredFeatureFlags & (1 << i)) { snapshotInfo.flags.add(i); } } } function decodeExportTable() { const { offset, size } = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_EXPORT_TABLE); buffer.readOffset = offset; const exportCount = size / 4; beginRegion('Export Table'); for (let i = 0; i < exportCount; i++) { const offset = buffer.readOffset; const exportID = buffer.readUInt16LE(); const exportValue = buffer.readUInt16LE(); const value = decodeValue(exportValue); region.push({ offset, size: 4, content: { type: 'LabeledValue', label: `[${exportID}]`, value } }); const logicalValue = getLogicalValue(value); if (logicalValue.type !== 'DeletedValue') { snapshotInfo.exports.set(exportID, logicalValue); } } endRegion('Export Table'); } function decodeShortCallTable() { const { size } = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_SHORT_CALL_TABLE); if (size > 0) { return (0, utils_1.notImplemented)(); // TODO } } function decodeStringTable() { const { size, offset } = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_STRING_TABLE); buffer.readOffset = offset; const stringTableCount = size / 2; beginRegion('String Table'); for (let i = 0; i < stringTableCount; i++) { let value = readValue(`[${i}]`); } endRegion('String Table'); } function getName(offset, type) { if (reconstructionInfo) { const name = reconstructionInfo.names[type]?.[offset]; if (!name) { // Note: Names should either come consistently from the reconstruction // info or not at all. We can't mix unless we do extra work for // namespace clashes. return (0, utils_1.invalidOperation)(`Name not found for bytecode offset ${stringifyOffset(offset)}`); } else { return name; } } else { return undefined; } } function hasName(offset, type) { if (reconstructionInfo) { return offset in reconstructionInfo.names[type]; } else { return undefined; } } function decodeGlobals() { const { offset, size } = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_GLOBALS); const globalVariableCount = size / 2; buffer.readOffset = offset; beginRegion('Globals'); for (let i = 0; i < globalVariableCount; i++) { const slotOffset = offset + i * 2; // Handles are at the end of the globals const isHandle = slotOffset >= handlesBeginOffset; if (isHandle) { readValue(`Handle`); } else { const value = readValue(`[${i}]`); if (value.type === 'DeletedValue') continue; snapshotInfo.globalSlots.set(getNameOfGlobal(i), { value, indexHint: i }); } } endRegion('Globals'); } function decodeImportTable() { const { offset, size } = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_IMPORT_TABLE); buffer.readOffset = offset; const importCount = size / 2; beginRegion('Import Table'); for (let i = 0; i < importCount; i++) { const offset = buffer.readOffset; const u16 = buffer.readUInt16LE(); region.push({ offset, size: 2, content: { type: 'Attribute', label: `[${i}]`, value: u16 } }); } endRegion('Import Table'); } function getSectionInfo(section) { const offset = getSectionOffset(section); const size = getSectionSize(section); const end = offset + size; return { offset, size, end }; function getSectionOffset(section) { return sectionOffsets[section] ?? (0, utils_1.unexpected)(); } function getSectionSize(section) { if (section < runtime_types_1.mvm_TeBytecodeSection.BCS_SECTION_COUNT - 1) { return getSectionOffset(section + 1) - getSectionOffset(section); } else { (0, utils_1.hardAssert)(section === runtime_types_1.mvm_TeBytecodeSection.BCS_SECTION_COUNT - 1); return bytecodeSize - getSectionOffset(section); } } } function beginRegion(name) { regionStack.push({ region, regionName, regionStart }); const newRegion = []; region.push({ offset: buffer.readOffset, size: undefined, content: { type: 'Region', regionName: name, value: newRegion } }); region = newRegion; regionStart = buffer.readOffset; regionName = name; } function endRegion(name) { (0, utils_1.hardAssert)(regionName === name); (0, utils_1.hardAssert)(regionStack.length > 0); ({ region, regionName, regionStart } = regionStack.pop()); } function finalizeRegions(region, start, end) { if (region.length === 0) return undefined; // Calculate offset for nested regions for (const component of region) { // Nested region if (component.content.type === 'Region') { const finalizeResult = finalizeRegions(component.content.value); // Delete empty region if (!finalizeResult) { component.size = 0; } else { if (component.offset === undefined) { component.offset = finalizeResult.offset; } else if (finalizeResult.offset < component.offset) { component.content.value.push({ offset: component.offset, size: finalizeResult.offset - component.offset, content: { type: 'RegionOverflow' } }); console.error(`!! WARNING: Region overflow at 0x${finalizeResult.offset.toString(16).padStart(4, '0')} by ${component.offset - finalizeResult.offset} bytes`); } if (component.size === undefined) { component.size = finalizeResult.size; } else if (finalizeResult.size > component.size) { component.content.value.push({ offset: component.offset + finalizeResult.size, size: component.size - finalizeResult.size, content: { type: 'RegionOverflow' } }); console.error(`!! WARNING: Region overflow at 0x${(component.offset + component.size).toString(16).padStart(4, '0')} by ${component.offset - finalizeResult.offset} bytes`); } else if (component.size > finalizeResult.size) { component.content.value.push({ offset: component.offset + component.size, size: component.size - finalizeResult.size, content: { type: 'UnusedSpace' } }); } } } } const sortedComponents = _.sortBy(region, component => component.offset); // Clear out and rebuild region.splice(0, region.length); const regionStart = start !== undefined ? start : sortedComponents[0].offset; let cursor = regionStart; for (const component of sortedComponents) { // Skip empty regions if (component.content.type === 'Region' && component.content.value.length === 0) { continue; } if (component.offset > cursor) { region.push({ offset: cursor, size: component.offset - cursor, content: { type: 'UnusedSpace' } }); } else if (cursor > component.offset) { region.push({ offset: cursor, size: -(cursor - component.offset), content: { type: 'OverlapWarning', offsetStart: component.offset, offsetEnd: cursor } }); console.error(`!! WARNING: Entry overlap at 0x${component.offset} by ${cursor - component.offset} bytes`); } region.push(component); cursor = component.offset + component.size; } if (end !== undefined && cursor < end) { region.push({ offset: cursor, size: end - cursor, content: { type: 'UnusedSpace' } }); cursor = end; } return { size: cursor - regionStart, offset: regionStart }; } function readValue(label) { const offset = buffer.readOffset; const u16 = buffer.readUInt16LE(); const value = decodeValue(u16); region.push({ offset, size: 2, content: { type: 'LabeledValue', label, value } }); return getLogicalValue(value); } function readLogicalAt(offset, region, label, shallow = false) { const value = readValueAt(offset, region, label, shallow); const logical = getLogicalValue(value); return logical; } function readValueAt(offset, region, label, shallow = false) { const u16 = buffer.readUInt16LE(offset); const value = decodeValue(u16, shallow); region.push({ offset, size: 2, content: { type: 'LabeledValue', label, value } }); return value; } function decodeValue(u16, shallow = false) { if ((u16 & 1) === 0) { return decodeShortPtr(u16, shallow); } else if ((u16 & 3) === 1) { if (u16 < runtime_types_1.vm_TeWellKnownValues.VM_VALUE_WELLKNOWN_END) { return decodeWellKnown(u16); } else { return decodeBytecodeMappedPtr(u16 & 0xFFFC, shallow); } } else { (0, utils_1.hardAssert)((u16 & 3) === 3); return decodeVirtualInt14(u16); } } function decodeBytecodeMappedPtr(offset, shallow) { // If the pointer points to a global variable, it is treated as logically // pointing to the thing that global variable points to. const { offset: globalsOffset, end: globalsEnd } = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_GLOBALS); const isHandle = offset >= globalsOffset && offset < globalsEnd; if (isHandle) { const handle16 = buffer.readUInt16LE(offset); handlesBeginOffset = Math.min(handlesBeginOffset, offset); const handleValue = decodeValue(handle16); if (handleValue.type === 'DeletedValue') return (0, utils_1.unexpected)(); return { type: 'Pointer', offset, target: handleValue }; } const { offset: romOffset, end: romEnd } = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_ROM); if (offset >= romOffset && offset < romEnd) { return { type: 'Pointer', offset, target: decodeAllocationAtOffset(offset, 'bytecode', shallow) }; } return (0, utils_1.invalidOperation)('Pointer out of range'); } function decodeWellKnown(u16) { const value = u16; switch (value) { case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_UNDEFINED: return IL.undefinedValue; case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_NULL: return IL.nullValue; case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_TRUE: return IL.trueValue; case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_FALSE: return IL.falseValue; case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_NAN: return IL.numberValue(NaN); case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_NEG_ZERO: return IL.numberValue(-0); case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_DELETED: return IL.deletedValue; case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_STR_LENGTH: return IL.stringValue('length'); case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_STR_PROTO: return IL.stringValue('__proto__'); case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_NO_OP_FUNC: return IL.noOpFunction; case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_WELLKNOWN_END: return (0, utils_1.unexpected)(); default: return (0, utils_1.unexpected)(); } } function decodeShortPtr(u16, shallow) { (0, utils_1.hardAssert)((u16 & 0xFFFE) === u16); const offset = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_HEAP).offset + u16; return { type: 'Pointer', offset, target: decodeAllocationAtOffset(offset, 'gc', shallow) }; } function decodeVirtualInt14(u16) { u16 = u16 >> 2; const value = u16 >= 0x2000 ? u16 - 0x4000 : u16; return { type: 'NumberValue', value }; } function offsetToAllocationID(offset) { const name = getName(offset, 'allocation'); return name !== undefined ? parseInt(name) : offset; } function readHeaderField8(name) { const address = buffer.readOffset; const value = buffer.readUInt8(); region.push({ offset: address, size: 1, content: { type: 'HeaderField', name, isOffset: false, value } }); return value; } function readHeaderField16(name, isOffset) { const address = buffer.readOffset; const value = buffer.readUInt16LE(); region.push({ offset: address, size: 2, content: { type: 'HeaderField', name, isOffset, value } }); return value; } function readHeaderField32(name, isOffset) { const address = buffer.readOffset; const value = buffer.readUInt32LE(); region.push({ offset: address, size: 4, content: { type: 'HeaderField', name, isOffset, value } }); return value; } function decodeAllocationAtOffset(offset, section, shallow) { if (shallow) { return undefined; } if (processedAllocationsByOffset.has(offset)) { return processedAllocationsByOffset.get(offset); } const value = decodeAllocationContent(offset, section); // The decode is supposed to insert the value. It needs to do this itself // because it needs to happen before nested allocations are pursued. The // exception is resume points which look like allocations at first but are // actually value types that reference into a function allocation. (0, utils_1.hardAssert)(value.type === 'ResumePoint' || processedAllocationsByOffset.get(offset) === value); return value; } function readAllocationHeader(allocationOffset, region) { const headerWord = buffer.readUInt16LE(allocationOffset - 2); const size = (headerWord & 0xFFF); // Size excluding header const typeCode = headerWord >> 12; // Allocation header region.push({ offset: allocationOffset - 2, size: 2, content: { type: 'AllocationHeaderAttribute', text: `Size: ${size}, Type: ${runtime_types_1.TeTypeCode[typeCode]}` } }); return { size, typeCode }; } function getAllocationRegionForSection(section) { switch (section) { case 'bytecode': return romAllocationsRegion; case 'gc': return gcAllocationsRegion; default: return (0, utils_1.assertUnreachable)(section); } } function decodeAllocationContent(offset, section) { const region = getAllocationRegionForSection(section); // Note: in the case of functions, the size bits are repurposed and don't // represent the actual allocation size const { size, typeCode } = readAllocationHeader(offset, region); switch (typeCode) { case runtime_types_1.TeTypeCode.TC_REF_TOMBSTONE: return (0, utils_1.unexpected)(); case runtime_types_1.TeTypeCode.TC_REF_INT32: return decodeInt32(region, offset, size); case runtime_types_1.TeTypeCode.TC_REF_FLOAT64: return decodeFloat64(region, offset, size); case runtime_types_1.TeTypeCode.TC_REF_STRING: case runtime_types_1.TeTypeCode.TC_REF_INTERNED_STRING: return decodeString(region, offset, size); case runtime_types_1.TeTypeCode.TC_REF_PROPERTY_LIST: return decodePropertyList(region, offset, size, section); case runtime_types_1.TeTypeCode.TC_REF_ARRAY: return decodeArray(region, offset, size, false, section); case runtime_types_1.TeTypeCode.TC_REF_FIXED_LENGTH_ARRAY: return decodeArray(region, offset, size, true, section); case runtime_types_1.TeTypeCode.TC_REF_FUNCTION: return decodeFunction(region, offset, size); case runtime_types_1.TeTypeCode.TC_REF_HOST_FUNC: return decodeHostFunction(region, offset, size); case runtime_types_1.TeTypeCode.TC_REF_CLOSURE: return decodeClosure(region, offset, size, section); case runtime_types_1.TeTypeCode.TC_REF_SYMBOL: return (0, utils_1.reserved)(); case runtime_types_1.TeTypeCode.TC_REF_VIRTUAL: return (0, utils_1.reserved)(); case runtime_types_1.TeTypeCode.TC_REF_UINT8_ARRAY: return decodeUint8Array(region, offset, size, section); case runtime_types_1.TeTypeCode.TC_REF_CLASS: return decodeClass(region, offset, size); case runtime_types_1.TeTypeCode.TC_REF_RESERVED_1: return (0, utils_1.unexpected)(); default: return (0, utils_1.unexpected)(); } } function decodeInt32(region, offset, size) { const i = buffer.readInt32LE(offset); const value = { type: 'NumberValue', value: i }; processedAllocationsByOffset.set(offset, value); region.push({ offset: offset, size: size, content: { type: 'LabeledValue', label: 'Value', value } }); return value; } function decodeFloat64(region, offset, size) { const n = buffer.readDoubleLE(offset); const value = { type: 'NumberValue', value: n }; processedAllocationsByOffset.set(offset, value); region.push({ offset: offset, size: size, content: { type: 'LabeledValue', label: 'Value', value } }); return value; } function decodeFunction(region, offset, sizeBits) { /* `decodeFunction` is called when it finds an allocation with a TC_REF_FUNCTION header type-code. This used to mean that we've encountered the main entry point for a function, but now the introduction of async-await means that a function has multiple entry points. */ const isResumePoint = Boolean(sizeBits & 0x0800); if (isResumePoint) { return decodeResumePoint(region, offset, sizeBits); } else { return decodeFunctionFromMainEntry(region, offset, sizeBits); } } function decodeResumePoint(region, offset, sizeBits) { // Hack: a resume point is the only thing that looks like an allocation but // actually points into an existing allocation. So even though there is a // header, we don't want this shown in the disassembly because it will show // up again when we decode the main function. const allocationHeader = region.pop(); (0, utils_1.hardAssert)(allocationHeader?.content.type === 'AllocationHeaderAttribute'); const resumeAddress = offset; sizeBits = sizeBits & ~0x800; (0, utils_1.hardAssert)((resumeAddress & 0xFFFC) == resumeAddress); // Read function header that precedes the instruction (0, utils_1.hardAssert)(((runtime_types_1.TeTypeCode.TC_REF_FUNCTION << 12) | 0x0800 | sizeBits) === buffer.readUInt16LE(resumeAddress - 2)); // The back-pointer reuses the bits that are normally used for the allocation size const backPointer = sizeBits << 2; const mainFunctionAddress = resumeAddress - backPointer; // Sanity-check that we've found the main function address correctly const mainHeader = buffer.readUInt16LE(mainFunctionAddress - 2); (0, utils_1.hardAssert)(mainHeader >> 12 === runtime_types_1.TeTypeCode.TC_REF_FUNCTION); (0, utils_1.hardAssert)((mainHeader & 0x0800) === 0); // Not a continuation // Synthesize an mvm_Value representing the function pointer so we // can use the common decoding logic const mainFunctionValue = mainFunctionAddress | 1; // Note: the `decodeValue` function as a side effect will actually decode // the main function, if it's not already decoding. This is because the // resume point is reachable independently of the main entry point to the // function, and so we could have encountered this `VM_OP3_ASYNC_RESUME` // before encountering the main function (or we might only encounter the // main function through one of its resume points). const mainFunction = getLogicalValue(decodeValue(mainFunctionValue)); (0, utils_1.hardAssert)(mainFunction.type === 'FunctionValue'); // As a consequence of decoding the main function, all the resume points in // the function will also be decoded, so we can do a lookup to find this // resume point. const resumePoint = resumePointByPhysicalAddress.get(resumeAddress); (0, utils_1.hardAssert)(resumePoint?.type === 'ResumePoint'); return resumePoint; } function decodeFunctionFromMainEntry(region, offset, sizeBits) { const functionID = getName(offset, 'allocation') || stringifyOffset(offset); const functionValue = { type: 'FunctionValue', value: functionID }; processedAllocationsByOffset.set(offset, functionValue); // Note: for functions, the size bits have been repurposed const isContinuation = Boolean(sizeBits & 0x0800); (0, utils_1.hardAssert)(!isContinuation); // Not supported yet const maxStackDepth = sizeBits & 0x00FF; const ilFunc = { type: 'Function', id: functionID, blocks: {}, maxStackDepth: maxStackDepth, entryBlockID: offsetToBlockID(offset), }; snapshotInfo.functions.set(ilFunc.id, ilFunc); const functionBodyRegion = [{ offset, size: 0, content: { type: 'Attribute', label: 'maxStackDepth', value: maxStackDepth } }, { offset, size: 0, content: { type: 'Attribute', label: 'isContinuation', value: isContinuation ? 1 : 0 } }]; const { blocks, size } = decodeInstructions(functionBodyRegion, offset, functionID); ilFunc.blocks = blocks; region.push({ offset, size, content: { type: 'Region', regionName: `Function ${functionID}`, value: functionBodyRegion } }); return functionValue; } function decodeInstructions(region, offset, functionID) { const originalReadOffset = buffer.readOffset; buffer.readOffset = offset; // Because the size bits of the function header are repurposed, we need to // infer the size by the last reachable instruction in the instruction graph. let functionEndOffset = offset; const instructionsCovered = new Set(); const blockEntryOffsets = new Set(); const instructionsByOffset = new Map(); const decodingBlock = new Map(); // The entry point is at the beginning (and this will also explore the whole // reachable instruction graph) decodeBlock(offset, 0, undefined); buffer.readOffset = originalReadOffset; const blocks = divideInstructionsIntoBlocks(); const size = functionEndOffset - offset; return { blocks, size }; function divideInstructionsIntoBlocks() { const blocks = {}; const instructionsInOrder = _.sortBy([...instructionsByOffset], ([offset]) => offset); (0, utils_1.hardAssert)(instructionsInOrder.length > 0); const [firstOffset, [firstInstruction, firstInstrDisassembly]] = instructionsInOrder[0]; let block = { expectedStackDepthAtEntry: firstInstruction.stackDepthBefore, id: offsetToBlockID(firstOffset), operations: [] }; let blockRegion = []; region.push({ offset: firstOffset, size: undefined, content: { type: 'Region', regionName: `Block ${block.id}`, value: blockRegion } }); blocks[block.id] = block; const iter = instructionsInOrder[Symbol.iterator](); let iterResult = iter.next(); while (!iterResult.done) { const [instructionOffset, [instruction, disassembly, instructionSize]] = iterResult.value; const isStartOfBlock = block.operations.length === 0; // Resume points are special because they're addressable, so we need to // register them globally. Catch blocks are also special because they're // addressable. We don't know which blocks are catch blocks, so we just // map all the blocks (the start of every block). if (instruction.opcode === 'AsyncResume' || isStartOfBlock) { resumePointByPhysicalAddress.set(instructionOffset, { type: 'ResumePoint', address: { type: 'ProgramAddressValue', funcId: functionID, blockId: block.id, operationIndex: block.operations.length } }); } block.operations.push(instruction); blockRegion.push({ offset: instructionOffset, size: instructionSize, content: { type: 'Annotation', text: disassembly } }); iterResult = iter.next(); if (!iterResult.done) { const [nextOffset, [nextInstruction]] = iterResult.value; // Start a new block? if (blockEntryOffsets.has(nextOffset)) { // End off previous block if (!il_opcodes_1.blockTerminatingOpcodes.has(instruction.opcode)) { // Add implicit jump/fall-through block.operations.push({ opcode: 'Jump', stackDepthBefore: instruction.stackDepthBefore, stackDepthAfter: instruction.stackDepthAfter, operands: [{ type: 'LabelOperand', targetBlockId: offsetToBlockID(nextOffset) }] }); blockRegion.push({ offset: nextOffset, size: 0, content: { type: 'Annotation', text: '<implicit fallthrough>' } }); } // Create new block block = { expectedStackDepthAtEntry: nextInstruction.stackDepthBefore, id: offsetToBlockID(nextOffset), operations: [] }; blocks[block.id] = block; blockRegion = []; region.push({ offset: nextOffset, size: undefined, content: { type: 'Region', regionName: `Block ${block.id}`, value: blockRegion } }); } } } return blocks; } function decodeBlock(blockOffset, stackDepth, tryStack) { if (decodingBlock.has(blockOffset)) { (0, utils_1.hardAssert)(decodingBlock.get(blockOffset).stackDepth === stackDepth, `Inconsistent stack depth at block 0x${blockOffset.toString(16)}`); return; } decodingBlock.set(blockOffset, { stackDepth }); const blockLinks = []; const prevOffset = buffer.readOffset; buffer.readOffset = blockOffset; blockEntryOffsets.add(blockOffset); while (true) { if (instructionsCovered.has(buffer.readOffset)) { break; } instructionsCovered.add(buffer.readOffset); const instructionOffset = buffer.readOffset; const stackDepthBefore = stackDepth; const decodeResult = decodeInstruction(region, stackDepthBefore, tryStack); if (buffer.readOffset > functionEndOffset) { functionEndOffset = buffer.readOffset; } const opcode = decodeResult.operation.opcode; const op = { ...decodeResult.operation, opcode: opcode, stackDepthBefore: (0, utils_1.notUndefined)(stackDepthBefore), stackDepthAfter: undefined, }; // Special case for StartTry and EndTry because they jump stack depths if (opcode === 'StartTry') { tryStack = { outer: tryStack, stackDepthBeforeTry: stackDepth }; stackDepth += 2; } else if (opcode === 'EndTry') { stackDepth = (0, utils_1.notUndefined)(tryStack).stackDepthBeforeTry; tryStack = (0, utils_1.notUndefined)(tryStack).outer; } else { const calculateStackDepthChange = IL.calcStaticStackChangeOfOp(op) ?? (0, utils_1.unexpected)(); stackDepth += calculateStackDepthChange; } op.stackDepthAfter = stackDepth; const size = buffer.readOffset - instructionOffset; const disassembly = decodeResult.disassembly || (0, stringify_il_1.stringifyOperation)(op); instructionsByOffset.set(instructionOffset, [op, disassembly, size]); // Control flow operations if (decodeResult.jumpTo) { blockLinks.push(...decodeResult.jumpTo.targets); if (!decodeResult.jumpTo.alsoContinue) break; } } buffer.readOffset = prevOffset; for (const { offset, stackDepth, tryStack } of blockLinks) { decodeBlock(offset, stackDepth, tryStack); } } } function decodeHostFunction(region, offset, size) { const hostFunctionIndex = buffer.readUInt16LE(offset); const { offset: importTableOffset, size: importTableSize } = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_IMPORT_TABLE); const hostFunctionIDOffset = importTableOffset + hostFunctionIndex * 2; (0, utils_1.hardAssert)(hostFunctionIDOffset < importTableOffset + importTableSize); const hostFunctionID = buffer.readUInt16LE(hostFunctionIDOffset); const hostFunctionValue = { type: 'HostFunctionValue', value: hostFunctionID }; processedAllocationsByOffset.set(offset, hostFunctionValue); region.push({ offset: offset, size: size, content: { type: 'Attribute', label: 'Value', value: `Import Table [${hostFunctionIndex}] (&${stringifyOffset(hostFunctionIDOffset)})` } }); return hostFunctionValue; } function decodeString(region, offset, size) { const origOffset = buffer.readOffset; buffer.readOffset = offset; const str = buffer.readString(size - 1, 'utf8'); buffer.readOffset = origOffset; const value = { type: 'StringValue', value: str }; processedAllocationsByOffset.set(offset, value); region.push({ offset: offset, size: size, content: { type: 'LabeledValue', label: 'Value', value } }); return value; } function decodePropertyList(region, offset, size, section) { const allocationID = offsetToAllocationID(offset); const object = { type: 'ObjectAllocation', allocationID, prototype: undefined, properties: {}, memoryRegion: getAllocationMemoryRegion(section), keysAreFixed: false, internalSlots: [IL.deletedValue, IL.deletedValue] }; snapshotInfo.allocations.set(allocationID, object); const ref = { type: 'ReferenceValue', value: allocationID }; processedAllocationsByOffset.set(offset, ref); const objRegion = []; let groupRegion = objRegion; let groupOffset = offset; let groupSize = size; let internalSlotIndex = 2; while (true) { const dpNext = decodeValue(buffer.readUInt16LE(groupOffset), true); if (dpNext.type === 'DeletedValue') return (0, utils_1.unexpected)(); groupRegion.push({ size: 2, offset: groupOffset, content: { type: 'LabeledValue', label: 'dpNext', value: dpNext } }); const dpProto = readLogicalAt(groupOffset + 2, groupRegion, 'dpProto'); if (groupOffset === offset) { // First group object.prototype = dpProto; } else if (dpProto.type !== 'NullValue') { return (0, utils_1.invalidOperation)('Only the first TsPropertyList in the chain should have a prototype.'); } const propsOffset = groupOffset + 4; const propCount = (groupSize - 4) / 4; // Each key-value pair is 4 bytes for (let i = 0; i < propCount; i++) { const propOffset = propsOffset + i * 4; const key = readLogicalAt(propOffset, groupRegion, 'key'); const value = readValueAt(propOffset + 2, groupRegion, 'value'); const logical = getLogicalValue(value); // Internal slots if (key.type === 'NumberValue' && (0, runtime_types_1.isSInt14)(key.value) && key.value < 0) { // Both the key and value are considered to be distinct internal // slots, so we can use the key slot for storage (as long as it's // storing a negative int14) object.internalSlots[internalSlotIndex++] = key; object.internalSlots[internalSlotIndex++] = logical; continue; } if (key.type !== 'StringValue') { return (0, utils_1.invalidOperation)('Expected property key to be string'); } const keyStr = key.value; if (logical.type !== 'DeletedValue') { object.properties[keyStr] = logical; } } // Next group, if there is one if (dpNext.type === 'NullValue') { break; } if (dpNext.type !== 'Pointer') { return (0, utils_1.unexpected)(); } // TODO: Test this path groupRegion = []; groupOffset = dpNext.offset; const { size, typeCode } = readAllocationHeader(groupOffset, region); groupSize = size; (0, utils_1.hardAssert)(typeCode === runtime_types_1.TeTypeCode.TC_REF_PROPERTY_LIST); region.push({ offset: groupOffset, size: undefined, content: { type: 'Region', regionName: `Child TsPropertyList`, value: groupRegion } }); } region.push({ offset: offset, size: size, content: { type: 'Region', regionName: `TsPropertyList`, value: objRegion } }); return ref; } function decodeClass(region, offset, size) { (0, utils_1.hardAssert)(size === 4); const classRegion = []; const constructorFunc = readLogicalAt(offset, classRegion, 'constructorFunc'); const staticProps = readLogicalAt(offset + 2, classRegion, 'staticProps'); region.push({ offset, size, content: { type: 'Region', regionName: 'Class', value: classRegion } }); const value = { type: 'ClassValue', staticProps, constructorFunc }; processedAllocationsByOffset.set(offset, value); return value; } function decodeClosure(region, offset, size, section) { const memoryRegion = getAllocationMemoryRegion(section); const allocationID = offsetToAllocationID(offset); const closure = { type: 'ClosureAllocation', allocationID, slots: [], memoryRegion, }; snapshotInfo.allocations.set(allocationID, closure); const ref = { type: 'ReferenceValue', value: allocationID }; processedAllocationsByOffset.set(offset, ref); const closureRegion = []; const length = size / 2; for (let i = 0; i < length; i++) { const itemOffset = offset + i * 2; const itemRaw = buffer.readUInt16LE(itemOffset); const item = decodeValue(itemRaw); const logical = getLogicalValue(item); closure.slots[i] = logical; closureRegion.push({ offset: itemOffset, size: 2, content: { type: 'LabeledValue', label: `closure[${i}]`, value: item } }); } region.push({ offset, size, content: { type: 'Region', regionName: 'TsClosure', value: closureRegion } }); return ref; } fu