use alloy_primitives::{Bytes, U256};
use revm::bytecode::opcode::OpCode;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DisassemblyInstruction {
pub pc: usize,
pub opcode: OpCode,
pub push_data: Vec<u8>,
}
impl DisassemblyInstruction {
pub fn new(pc: usize, opcode: OpCode) -> Self {
Self { pc, opcode, push_data: Vec::new() }
}
pub fn with_push_data(pc: usize, opcode: OpCode, push_data: Vec<u8>) -> Self {
Self { pc, opcode, push_data }
}
pub fn is_push(&self) -> bool {
let opcode_byte = self.opcode.get();
(0x60..=0x7F).contains(&opcode_byte)
}
pub fn push_size(&self) -> usize {
if self.is_push() {
(self.opcode.get() - 0x60 + 1) as usize
} else {
0
}
}
pub fn instruction_size(&self) -> usize {
1 + self.push_size()
}
}
#[derive(Debug, Clone)]
pub struct DisassemblyResult {
pub bytecode: Bytes,
pub instructions: Vec<DisassemblyInstruction>,
}
impl DisassemblyResult {
pub fn new(bytecode: Bytes, instructions: Vec<DisassemblyInstruction>) -> Self {
Self { bytecode, instructions }
}
pub fn instruction_count(&self) -> usize {
self.instructions.len()
}
pub fn get_instruction_at_pc(&self, pc: usize) -> Option<&DisassemblyInstruction> {
self.instructions.iter().find(|inst| inst.pc == pc)
}
pub fn get_push_instructions(&self) -> Vec<&DisassemblyInstruction> {
self.instructions.iter().filter(|inst| inst.is_push()).collect()
}
pub fn find_instruction_containing_pc(&self, pc: usize) -> Option<&DisassemblyInstruction> {
self.instructions.iter().find(|inst| {
let start = inst.pc;
let end = start + inst.instruction_size();
pc >= start && pc < end
})
}
}
pub fn disassemble(bytecode: &Bytes) -> DisassemblyResult {
let mut instructions = Vec::new();
let mut pc = 0;
while pc < bytecode.len() {
let opcode_byte = bytecode[pc];
let opcode = unsafe { OpCode::new_unchecked(opcode_byte) };
if (0x60..=0x7F).contains(&opcode_byte) {
let push_size = (opcode_byte - 0x60 + 1) as usize;
let data_start = pc + 1;
let data_end = data_start + push_size;
let mut push_data = Vec::new();
for i in data_start..data_end {
if i < bytecode.len() {
push_data.push(bytecode[i]);
} else {
push_data.push(0); }
}
instructions.push(DisassemblyInstruction::with_push_data(pc, opcode, push_data));
pc = data_end;
} else {
instructions.push(DisassemblyInstruction::new(pc, opcode));
pc += 1;
}
}
DisassemblyResult::new(bytecode.clone(), instructions)
}
pub fn extract_push_value(instruction: &DisassemblyInstruction) -> Option<U256> {
if !instruction.is_push() || instruction.push_data.is_empty() {
return None;
}
let mut value = U256::ZERO;
for &byte in &instruction.push_data {
value = value.wrapping_shl(8).wrapping_add(U256::from(byte));
}
Some(value)
}
pub fn format_instruction(instruction: &DisassemblyInstruction, show_pc: bool) -> String {
let pc_part = if show_pc { format!("{:04x}: ", instruction.pc) } else { String::new() };
let opcode_name = if instruction.opcode.is_valid() {
instruction.opcode.as_str().to_string()
} else {
format!("'{:x}'(Unknown Opcode)", instruction.opcode.get())
};
if instruction.is_push() && !instruction.push_data.is_empty() {
let hex_data = instruction.push_data.iter().map(|b| format!("{b:02x}")).collect::<String>();
format!("{pc_part}{opcode_name} 0x{hex_data}")
} else {
format!("{pc_part}{opcode_name}")
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::Bytes;
#[test]
fn test_disassemble_simple() {
let bytecode = Bytes::from(vec![0x80, 0x81, 0x82]); let result = disassemble(&bytecode);
assert_eq!(result.instructions.len(), 3);
assert_eq!(result.instructions[0].pc, 0);
assert_eq!(result.instructions[1].pc, 1);
assert_eq!(result.instructions[2].pc, 2);
for inst in &result.instructions {
assert!(!inst.is_push());
assert!(inst.push_data.is_empty());
}
}
#[test]
fn test_disassemble_push_instructions() {
let bytecode = Bytes::from(vec![
0x60, 0x42, 0x61, 0x12, 0x34, 0x80, ]);
let result = disassemble(&bytecode);
assert_eq!(result.instructions.len(), 3);
assert_eq!(result.instructions[0].pc, 0);
assert!(result.instructions[0].is_push());
assert_eq!(result.instructions[0].push_data, vec![0x42]);
assert_eq!(result.instructions[0].instruction_size(), 2);
assert_eq!(result.instructions[1].pc, 2);
assert!(result.instructions[1].is_push());
assert_eq!(result.instructions[1].push_data, vec![0x12, 0x34]);
assert_eq!(result.instructions[1].instruction_size(), 3);
assert_eq!(result.instructions[2].pc, 5);
assert!(!result.instructions[2].is_push());
assert!(result.instructions[2].push_data.is_empty());
assert_eq!(result.instructions[2].instruction_size(), 1);
}
#[test]
fn test_extract_push_value() {
let push1 = DisassemblyInstruction::with_push_data(
0,
unsafe { OpCode::new_unchecked(0x60) },
vec![0x42],
);
assert_eq!(extract_push_value(&push1), Some(U256::from(0x42)));
let push2 = DisassemblyInstruction::with_push_data(
0,
unsafe { OpCode::new_unchecked(0x61) },
vec![0x12, 0x34],
);
assert_eq!(extract_push_value(&push2), Some(U256::from(0x1234)));
let push4 = DisassemblyInstruction::with_push_data(
0,
unsafe { OpCode::new_unchecked(0x63) },
vec![0x12, 0x34, 0x56, 0x78],
);
assert_eq!(extract_push_value(&push4), Some(U256::from(0x12345678)));
}
#[test]
fn test_find_instruction_containing_pc() {
let bytecode = Bytes::from(vec![
0x60, 0x42, 0x61, 0x12, 0x34, 0x80, ]);
let result = disassemble(&bytecode);
assert_eq!(result.find_instruction_containing_pc(0).unwrap().pc, 0);
assert_eq!(result.find_instruction_containing_pc(1).unwrap().pc, 0);
assert_eq!(result.find_instruction_containing_pc(2).unwrap().pc, 2);
assert_eq!(result.find_instruction_containing_pc(3).unwrap().pc, 2);
assert_eq!(result.find_instruction_containing_pc(4).unwrap().pc, 2);
assert_eq!(result.find_instruction_containing_pc(5).unwrap().pc, 5);
assert!(result.find_instruction_containing_pc(6).is_none());
}
#[test]
fn test_truncated_push_instruction() {
let bytecode = Bytes::from(vec![0x61, 0x12]); let result = disassemble(&bytecode);
assert_eq!(result.instructions.len(), 1);
assert!(result.instructions[0].is_push());
assert_eq!(result.instructions[0].push_data, vec![0x12, 0x00]); }
#[test]
fn test_format_instruction() {
let push_inst = DisassemblyInstruction::with_push_data(
10,
unsafe { OpCode::new_unchecked(0x61) },
vec![0x12, 0x34],
);
let with_pc = format_instruction(&push_inst, true);
assert_eq!(with_pc, "000a: PUSH2 0x1234");
let without_pc = format_instruction(&push_inst, false);
assert_eq!(without_pc, "PUSH2 0x1234");
let regular_inst = DisassemblyInstruction::new(5, unsafe { OpCode::new_unchecked(0x80) });
let regular_formatted = format_instruction(®ular_inst, true);
assert_eq!(regular_formatted, "0005: DUP1");
}
}