Ctrl K
ComunidadGuías…

tools/rotor_disassembly_compare.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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()