tools/rotor_disassembly_compare.py
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env python3
# This script is used to compare objdump outputs of Rotor and RISC-V
import re
import subprocess
from abc import abstractmethod
from typing import Callable
from argparse import ArgumentParser
# noinspection PyClassHasNoInit
class Logger:
# noinspection PyClassHasNoInit
class Colors:
RESET = "\033[0m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
PURPLE = "\033[35m"
CYAN = "\033[36m"
WHITE = "\033[37m"
GRAY = "\033[90m"
@staticmethod
def debug(message: str, after=""):
Logger.__log_colored(Logger.Colors.GRAY, message + Logger.Colors.RESET + after)
@staticmethod
def info(message: str):
Logger.__log_colored(Logger.Colors.RESET, message)
@staticmethod
def error(message: str, after=""):
Logger.__log_colored(Logger.Colors.RED, message + Logger.Colors.RESET + after)
@staticmethod
def warning(message: str, after=""):
Logger.__log_colored(Logger.Colors.YELLOW, message + Logger.Colors.RESET + after)
@staticmethod
def success(message: str, after=""):
Logger.__log_colored(Logger.Colors.GREEN, message + Logger.Colors.RESET + after)
@staticmethod
def header(message: str, after=""):
Logger.__log_colored(Logger.Colors.BLUE, message + Logger.Colors.RESET + after)
@staticmethod
def print_instruction_array(max_elements: int, array: list):
if len(array) == 0:
print("[]")
return
if len(array) <= max_elements:
for item in array:
print(item)
return
start_elements_count = max_elements // 2
end_elements_count = max_elements - start_elements_count
for i in range(start_elements_count):
print(array[i])
print("...")
for i in range(end_elements_count):
print(array[-end_elements_count + i])
@staticmethod
def __log_colored(color: str, message: str, color_end=Colors.RESET):
print(f"{color}{message}{color_end}")
class Instruction:
# noinspection PyClassHasNoInit
class Source:
RISCV = "RISC-V"
ROTOR = "Rotor"
def __init__(self, source: str, address: str, instruction: str, operands: list[str] | None, comments: str | None):
if source is None:
raise ValueError("Source must be provided")
if address is None or instruction is None:
raise ValueError("Address and instruction must be provided")
self.source = source
self.address = int(address, 16)
self.instruction = instruction
self.operands = operands if operands else []
self.comments = comments
@staticmethod
def from_riscv(instruction: str) -> "Instruction":
return Instruction.__create(instruction, Instruction.Source.RISCV, r"\s+([\da-f]+):\s+[\da-f]+\s+([\w.]*)(?:\s+([\w,()\-]*))?(.*?)(?:\r?\n|$)")
@staticmethod
def from_rotor(instruction: str) -> "Instruction":
return Instruction.__create(instruction, Instruction.Source.ROTOR, r"0x([\da-f]+):\s+([\w.]*)(?:\s+([\w,()\-]*))?(.*?)(?:\r?\n|$)")
@staticmethod
def __create(instruction: str, source: str, regex: str):
match = re.match(regex, instruction, re.IGNORECASE)
if not match:
raise ValueError("Instruction does not match regex")
address = match.group(1)
instruction = match.group(2)
operands = match.group(3).split(",") if match.group(3) else None
comments = match.group(4).strip() if match.group(4) else None
return Instruction(source, address, instruction, operands, comments)
def __eq__(self, other: "Instruction"):
return self.address == other.address and self.instruction == other.instruction and Instruction.__operands_eq(self.operands, other.operands)
def __ne__(self, other: "Instruction"):
return not self.__eq__(other)
@staticmethod
def __operands_eq(op_a: list[str], op_b: list[str]):
if op_a is None and op_b is None:
return True
if op_a is None or op_b is None:
return False
if len(op_a) != len(op_b):
return False
for i in range(len(op_a)):
if not Instruction.__operand_eq(op_a[i], op_b[i]):
return False
return True
@staticmethod
def __operand_eq(op_a: str, op_b: str):
op_a = op_a.lower()
op_b = op_b.lower()
if op_a == op_b or op_a == "0x" + op_b or op_b == "0x" + op_a:
return True
return False
def __str__(self):
return f"{hex(self.address)}: {self.instruction} {', '.join(self.operands)}"
def __repr__(self):
return self.__str__()
class CodeReader:
def __init__(self, instruction_class: Callable[[str], Instruction]):
self.instruction_class = instruction_class
self.__peek: Instruction | None = None
def peek_instruction(self) -> Instruction:
if self.__peek is None:
self.__peek = self._actual_next_instruction()
return self.__peek
def next_instruction(self) -> Instruction:
if self.__peek is not None:
next_instruction = self.__peek
self.__peek = None
return next_instruction
return self._actual_next_instruction()
@abstractmethod
def _actual_next_instruction(self) -> Instruction:
pass
class StdoutReader(CodeReader):
def __init__(self, stdout: str, instruction_class: Callable[[str], Instruction]):
super().__init__(instruction_class)
self.iterator = iter(stdout.splitlines())
def _actual_next_instruction(self) -> Instruction:
while True:
try:
line = next(self.iterator)
return self.instruction_class(str(line))
except StopIteration:
raise EOFError("End of output")
except ValueError:
continue
class FileReader(CodeReader):
def __init__(self, file_path, instruction_class: Callable[[str], Instruction]):
super().__init__(instruction_class)
self.file = open(file_path, "r")
def _actual_next_instruction(self) -> Instruction:
while True:
if (line := self.file.readline()) == "":
raise EOFError("End of file")
try:
return self.instruction_class(str(line))
except ValueError:
continue
def parse_args():
argparser = ArgumentParser()
argparser.add_argument("-s", "--rotor-path", dest="rotor_path", default="./rotor;./tools/rotor;../rotor", help="Path to the Rotor executable (multiple paths can be provided, first one that exists will be used)")
argparser.add_argument("-r", "--riscv-path", dest="riscv_path", default="riscv64-unknown-elf-objdump", help="Path to the RISC-V objdump executable")
argparser.add_argument(dest="in_file", help="Input binary")
return argparser.parse_args()
def main():
args = parse_args()
Logger.debug("Dumping assembly using RISC-V at path " + args.riscv_path)
try:
riscv_output = subprocess.run([args.riscv_path, "-d", args.in_file], capture_output=True, text=True)
except FileNotFoundError:
Logger.error("RISC-V objdump executable not found")
return
if riscv_output.returncode != 0:
Logger.error("RISC-V objdump failed")
print(riscv_output.stderr)
return
rotor_paths = args.rotor_path.split(";")
rotor_output = None
for rotor_path in rotor_paths:
Logger.debug("Trying to dump assembly using Rotor at path " + rotor_path)
try:
rotor_output = subprocess.run([rotor_path, "-l", args.in_file, "-", "0", "-s"], capture_output=True, text=True)
break
except FileNotFoundError:
Logger.debug("Rotor executable not found at path " + rotor_path)
continue
if rotor_output is None:
Logger.error("Rotor executable not found")
return
if rotor_output.returncode != 0:
Logger.error("Rotor failed")
print(rotor_output.stderr)
print(rotor_output.stdout)
return
riscv_reader = StdoutReader(riscv_output.stdout, Instruction.from_riscv)
rotor_reader = StdoutReader(rotor_output.stdout, Instruction.from_rotor)
compare_objdump(riscv_reader, rotor_reader)
def compare_objdump(riscv_reader: CodeReader, rotor_reader: CodeReader):
at_start = True
num_missing_instruction_set = 0
num_mismatched_opcode = 0
num_mismatched_operands = 0
Logger.info("Comparing instructions...")
while True:
try:
# assert: We are at the same position in both files
mismatching_instructions = []
riscv_curr = riscv_reader.next_instruction()
rotor_curr = rotor_reader.next_instruction()
while rotor_curr.address > riscv_curr.address:
mismatching_instructions.append(riscv_curr)
riscv_curr = riscv_reader.next_instruction()
if len(mismatching_instructions) > 0:
# RISC-V had some instructions which Rotor didn't have
if rotor_curr.address == riscv_curr.address: # Are we back on track?
# Yes => log warning and continue
num_missing_instruction_set += 1
Logger.warning("PROBLEM: Found RISC-V instructions without corresponding Rotor instructions:")
Logger.print_instruction_array(5, mismatching_instructions)
print()
continue
else:
# No => seems like whole instruction set is mismatched. Log error and exit
Logger.error("ERROR: Instruction sets are mismatched")
Logger.header("===== RISC-V =====")
Logger.print_instruction_array(5, [*mismatching_instructions, riscv_curr])
Logger.header("===== Rotor =====")
print(rotor_curr)
print()
Logger.error("Analysis stopped prematurely due to error")
break
while riscv_curr.address > rotor_curr.address:
mismatching_instructions.append(rotor_curr)
rotor_curr = rotor_reader.next_instruction()
if len(mismatching_instructions) > 0:
# Rotor had some instructions which RISC-V didn't have
if rotor_curr.address == riscv_curr.address: # Are we back on track?
# Yes => log warning and continue
if at_start:
Logger.debug(f"NOTICE: Found erroneously decompiled Rotor instructions potentially outside of .text segment: {hex(mismatching_instructions[0].address)} - {hex(rotor_curr.address)}")
else:
num_missing_instruction_set += 1
Logger.warning("PROBLEM: Found Rotor instructions without corresponding RISC-V instructions")
Logger.print_instruction_array(5, mismatching_instructions)
print()
continue
else:
# No => seems like whole instruction set is mismatched. Log error and exit
Logger.error("Instruction sets are mismatched")
Logger.header("===== RISC-V =====")
print(riscv_curr)
Logger.header("===== Rotor =====")
Logger.print_instruction_array(5, [*mismatching_instructions, rotor_curr])
print()
Logger.error("Analysis stopped prematurely due to error")
break
# We are ready to check for instruction equality
if riscv_curr.instruction != rotor_curr.instruction:
# Opcode is different, this might indicate an unimplemented instruction or pseudoinstruction, or an unhandled compressed instruction
num_mismatched_opcode += 1
Logger.warning("PROBLEM: Instructions are not equal")
Logger.header("RISC-V - ", repr(riscv_curr))
Logger.header("ROTOR - ", repr(rotor_curr))
print()
elif riscv_curr != rotor_curr:
# Opcode is the same but operands are different, this is 100% a bug in Rotor operand printing!
num_mismatched_operands += 1
Logger.warning("PROBLEM: Operands are not equal")
Logger.header("RISC-V - ", repr(riscv_curr))
Logger.header("ROTOR - ", repr(rotor_curr))
print()
at_start = False
except EOFError:
Logger.info("Disassembly comparison finished")
num_problems = num_missing_instruction_set + num_mismatched_opcode + num_mismatched_operands
if num_problems == 0:
Logger.success("No problems found! 🎉")
else:
Logger.error(f"Found {num_problems} problems:")
Logger.error(f" - Missing instruction sets: {num_missing_instruction_set}")
Logger.error(f" - Mismatched instructions: {num_mismatched_opcode}")
Logger.error(f" - Mismatched operands with identical instructions: {num_mismatched_operands}")
break
if __name__ == '__main__':
main()