@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
JavaScript
/**
* 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
};