UNPKG

@bcoders.gr/evm-disassembler

Version:

A comprehensive EVM bytecode disassembler and analyzer with support for multiple EVM versions

334 lines (276 loc) 10.1 kB
/** * Output formatting for disassembled bytecode * @module formatter */ const { DANGEROUS_OPCODES } = require('./constants'); /** * Format disassembled instructions as text * @param {Array} instructions - Decoded instructions * @param {Object} options - Formatting options * @returns {string} Formatted output */ function formatAsText(instructions, options = {}) { const { showHex = true, showPushData = true, showStackDepth = false, highlightDangerous = true, lineNumbers = true } = options; const lines = []; const maxPcWidth = Math.max(...instructions.map(i => i.pc.toString().length), 4); // Header if (lineNumbers) { lines.push(`${'PC'.padEnd(maxPcWidth)} | OPCODE | ${showHex ? 'HEX | ' : ''}${showPushData ? 'DATA' : ''}`); lines.push('-'.repeat(60)); } // Instructions for (const inst of instructions) { let line = ''; // PC if (lineNumbers) { line += inst.pc.toString().padEnd(maxPcWidth) + ' | '; } // Opcode (with danger highlighting) let opcodeStr = inst.opcode.padEnd(11); if (highlightDangerous && DANGEROUS_OPCODES.includes(inst.opcode)) { opcodeStr = `[!] ${opcodeStr}`; } line += opcodeStr + ' | '; // Hex if (showHex) { line += inst.opcodeHex + ' | '; } // Push data if (showPushData && inst.pushData) { line += '0x' + inst.pushData; if (inst.truncated) { line += ' (truncated)'; } } // Stack depth if (showStackDepth && inst.stackDepthBefore !== undefined) { line += ` [stack: ${inst.stackDepthBefore} -> ${inst.stackDepthAfter}]`; } lines.push(line); } return lines.join('\n'); } /** * Format disassembled instructions as JSON * @param {Array} instructions - Decoded instructions * @param {Object} analysis - Analysis results * @param {Object} options - Formatting options * @returns {Object} JSON output */ function formatAsJSON(instructions, analysis = {}, options = {}) { const { includeRaw = false, pretty = true } = options; const output = { summary: { totalInstructions: instructions.length, bytecodeSize: instructions.length > 0 ? instructions[instructions.length - 1].nextPc / 2 : 0 }, instructions: instructions.map(inst => { const formatted = { pc: inst.pc, opcode: inst.opcode }; if (inst.pushData) { formatted.pushData = '0x' + inst.pushData; formatted.pushValue = inst.pushValue; } if (includeRaw) { formatted.raw = inst.raw; } if (inst.stackDepthBefore !== undefined) { formatted.stackDepth = { before: inst.stackDepthBefore, after: inst.stackDepthAfter }; } return formatted; }) }; // Add analysis results if provided if (analysis.metadata) { output.metadata = analysis.metadata; } if (analysis.functions) { output.functions = analysis.functions; } if (analysis.stack) { output.stackAnalysis = analysis.stack; } if (analysis.patterns) { output.patterns = analysis.patterns; } if (analysis.complexity) { output.complexity = analysis.complexity; } return pretty ? JSON.stringify(output, null, 2) : JSON.stringify(output); } /** * Format disassembled instructions as assembly-like syntax * @param {Array} instructions - Decoded instructions * @param {Object} options - Formatting options * @returns {string} Assembly-formatted output */ function formatAsAssembly(instructions, options = {}) { const { includeComments = true, includeLabels = true, indentJumps = true } = options; const lines = []; const jumpDests = new Set( instructions .filter(i => i.opcode === 'JUMPDEST') .map(i => i.pc) ); let labelCounter = 0; const pcToLabel = new Map(); // Assign labels to jump destinations if (includeLabels) { for (const pc of jumpDests) { pcToLabel.set(pc, `label_${labelCounter++}`); } } // Format instructions for (let i = 0; i < instructions.length; i++) { const inst = instructions[i]; const nextInst = instructions[i + 1]; let line = ''; // Add label if this is a jump destination if (includeLabels && jumpDests.has(inst.pc)) { lines.push(''); lines.push(`${pcToLabel.get(inst.pc)}:`); } // Indent based on control flow if (indentJumps && inst.opcode !== 'JUMPDEST') { line += ' '; } // Add opcode line += inst.opcode; // Add operands if (inst.pushData) { line += ' 0x' + inst.pushData; // Add comment with decimal value for small numbers if (includeComments && inst.pushData.length <= 8) { const decimal = BigInt('0x' + inst.pushData).toString(); line += ` ; ${decimal}`; } } // Add comments for specific opcodes if (includeComments) { if (inst.opcode === 'JUMP' || inst.opcode === 'JUMPI') { // Try to determine jump target if (i > 0 && instructions[i - 1].opcode.startsWith('PUSH')) { const target = parseInt(instructions[i - 1].pushData, 16); const label = pcToLabel.get(target); if (label) { line += ` ; -> ${label}`; } else { line += ` ; -> 0x${target.toString(16)}`; } } } else if (inst.opcode === 'PUSH4' && inst.pushData) { // Check if it looks like a function selector const selector = inst.pushData.substring(0, 8); const { KNOWN_SIGNATURES } = require('./constants'); const sig = KNOWN_SIGNATURES.get(selector); if (sig) { line += ` ; ${sig}`; } } } lines.push(line); } return lines.join('\n'); } /** * Format disassembled instructions as Markdown * @param {Array} instructions - Decoded instructions * @param {Object} analysis - Analysis results * @param {Object} options - Formatting options * @returns {string} Markdown output */ function formatAsMarkdown(instructions, analysis = {}, options = {}) { const lines = []; lines.push('# EVM Bytecode Disassembly\n'); // Summary section lines.push('## Summary\n'); lines.push(`- **Total Instructions**: ${instructions.length}`); lines.push(`- **Bytecode Size**: ${instructions.length > 0 ? instructions[instructions.length - 1].nextPc / 2 : 0} bytes`); if (analysis.metadata) { lines.push(`- **Compiler**: ${analysis.metadata.compiler || 'Unknown'}`); lines.push(`- **Version**: ${analysis.metadata.version || 'Unknown'}`); } lines.push(''); // Functions section if (analysis.functions && analysis.functions.functions.length > 0) { lines.push('## Detected Functions\n'); lines.push('| Selector | Signature | Known |'); lines.push('|----------|-----------|-------|'); for (const func of analysis.functions.functions) { lines.push(`| \`${func.selector}\` | ${func.signature} | ${func.isKnown ? '✓' : '✗'} |`); } lines.push(''); } // Complexity metrics if (analysis.complexity) { lines.push('## Complexity Metrics\n'); lines.push(`- **Cyclomatic Complexity**: ${analysis.complexity.cyclomaticComplexity}`); lines.push(`- **Max Stack Depth**: ${analysis.complexity.maxStackDepth}`); lines.push(`- **Unique Opcodes**: ${analysis.complexity.uniqueOpcodes}`); if (analysis.complexity.mostFrequentOpcodes.length > 0) { lines.push('\n### Most Frequent Opcodes\n'); lines.push('| Opcode | Count | Percentage |'); lines.push('|--------|-------|------------|'); for (const op of analysis.complexity.mostFrequentOpcodes.slice(0, 5)) { lines.push(`| ${op.opcode} | ${op.count} | ${op.percentage}% |`); } } lines.push(''); } // Disassembly section lines.push('## Disassembly\n'); lines.push('```assembly'); lines.push(formatAsAssembly(instructions, { includeComments: true, includeLabels: true })); lines.push('```'); return lines.join('\n'); } /** * Format disassembled instructions as CSV * @param {Array} instructions - Decoded instructions * @returns {string} CSV output */ function formatAsCSV(instructions) { const lines = []; // Header lines.push('PC,Opcode,Hex,PushData,StackBefore,StackAfter'); // Data rows for (const inst of instructions) { const row = [ inst.pc, inst.opcode, inst.opcodeHex, inst.pushData || '', inst.stackDepthBefore !== undefined ? inst.stackDepthBefore : '', inst.stackDepthAfter !== undefined ? inst.stackDepthAfter : '' ]; lines.push(row.map(v => `"${v}"`).join(',')); } return lines.join('\n'); } module.exports = { formatAsText, formatAsJSON, formatAsAssembly, formatAsMarkdown, formatAsCSV };