Source code for m2isar.backends.disass.disass

# SPDX-License-Identifier: Apache-2.0
#
# This file is part of the M2-ISA-R project: https://github.com/tum-ei-eda/M2-ISA-R
#
# Copyright (C) 2022
# Chair of Electrical Design Automation
# Technical University of Munich

"""Simple disassembler backend for M2-ISA-R ISA metamodels. Not very
actively maintained, might break or otherwise not work as expected.
"""

import argparse
import logging
import pathlib
import pickle
from collections import defaultdict
from io import SEEK_CUR, BytesIO
from elftools.elf.elffile import ELFFile
from elftools.elf.sections import SymbolTableSection
from elftools.elf.enums import ENUM_ST_INFO_TYPE
from elftools.elf.constants import SH_FLAGS

from ...metamodel import M2_METAMODEL_VERSION, M2Model, arch
from .asm_formatter import AsmFormatter

[docs] logger = logging.getLogger("viewer")
[docs] def get_function_symbols(elf): symbols = {} # Find executable section indices. executable_sections = set() for idx, section in enumerate(elf.iter_sections()): if section["sh_flags"] & SH_FLAGS.SHF_EXECINSTR: executable_sections.add(idx) for section in elf.iter_sections(): if not isinstance(section, SymbolTableSection): continue for symbol in section.iter_symbols(): name = symbol.name addr = symbol["st_value"] shndx = symbol["st_shndx"] if not name: continue # Ignore undefined/special symbols. if not isinstance(shndx, int): continue # Only symbols belonging to executable sections. if shndx not in executable_sections: continue if name.startswith("$"): continue symbols.setdefault(addr, []).append(name) # if symbol["st_info"]["type"] != "STT_FUNC": # continue # addr = symbol["st_value"] # name = symbol.name # if addr and name: # symbols[addr] = name return symbols
[docs] def get_disassembly_regions(filename): """ Yield tuples: (name, virtual_address, bytes) """ with open(filename, "rb") as f: magic = f.read(4) f.seek(0) if magic == b"\x7fELF": elf = ELFFile(f) symbols = get_function_symbols(elf) for section in elf.iter_sections(): # SHF_EXECINSTR = 0x4 if section["sh_flags"] & 0x4: data = section.data() if data: yield ( section.name, section["sh_addr"], data, symbols, ) else: # Raw binary data = f.read() yield ( "<binary>", 0, data, )
[docs] def disassemble_region(data, symbols, base_addr, core, instrs_by_size, readlen, steplen, args): f = BytesIO(data) prev_count = 0 while iw_read := f.read(readlen): # read() advances, so rewind to make this equivalent to your old peek() f.seek(-len(iw_read), SEEK_CUR) iw = iw_read[:readlen] found_ins = None for cls in sorted(core.instr_classes): ii = int.from_bytes(iw[:cls // 8], "little") i = find_instr(ii, instrs_by_size[cls]) if i is not None: found_ins = i if found_ins is None: ins_str = "unknown" step = steplen if prev_count > 2: print(f"\trepeated {prev_count-2} times.") prev_count = 0 else: if found_ins.name == "DII": prev_count += 1 if prev_count > 1: f.seek(found_ins.size // 8, SEEK_CUR) continue else: if prev_count > 2: print(f"\trepeated {prev_count-2} times.") prev_count = 0 operands = decode(ii, found_ins) if args.format: asm_name = found_ins.mnemonic assembly = found_ins.assembly or "" fmt = AsmFormatter() asm_args = fmt.format(assembly, **operands) else: asm_name = found_ins.name op_str = " | ".join( [f"{k}={v}" for k, v in operands.items()] ) asm_args = f"[{op_str}]" ins_str = f"{asm_name}\t{asm_args}" step = found_ins.size // 8 offset = f.tell() address = base_addr + offset if address in symbols: print() for name in symbols[address]: print(f"{address:016x} <{name}>:") iword = int.from_bytes(iw[:step], "little") iword = f"{iword:0{step * 2}x}" print(f"{address:08x}: {iword:<16} {ins_str}") f.seek(step, SEEK_CUR)
[docs] def sort_instruction(entry): """Key function for sorting instructions: Sorts by most restrictive mask first, to accurately distinguish overlapping opcodes """ (code, mask), _ = entry return bin(mask).count("1"), code
[docs] def find_instr(iw: int, instructions: "dict[tuple[int, int], arch.Instruction]"): """Linear search for an instruction by its codeword.""" for (code, mask), instr_def in instructions.items(): if (iw & mask) == code: return instr_def return None
[docs] def slice_int(v: int, upper: int, lower: int): return (v & ((1 << upper + 1) - 1)) >> lower
[docs] def decode(iw: int, instr: arch.Instruction): """Separate out operands of an instruction from its codeword.""" enc_idx = 0 operands = defaultdict(int) for enc in reversed(instr.encoding): if isinstance(enc, arch.BitField): lower = enc.range.lower length = enc.range.length operands[enc.name] += slice_int(iw, enc_idx+length-1, enc_idx) << lower enc_idx += length else: enc_idx += enc.length return operands
[docs] def main(): parser = argparse.ArgumentParser() parser.add_argument('top_level', help="A .m2isarmodel file containing the models to generate.") parser.add_argument("core_name") parser.add_argument('bin') parser.add_argument("--format", action="store_true", help="Use assembly formatting string and mnemonic") parser.add_argument("--raw", action="store_true", help="Read read bytes (no segments, no virtual addresses) from binary") parser.add_argument("--log", default="info", choices=["critical", "error", "warning", "info", "debug"]) args = parser.parse_args() logging.basicConfig(level=getattr(logging, args.log.upper())) top_level = pathlib.Path(args.top_level) abs_top_level = top_level.resolve() search_path = abs_top_level.parent.parent model_fname = abs_top_level if abs_top_level.suffix == ".core_desc": logger.warning(".core_desc file passed as input. This is deprecated behavior, please change your scripts!") search_path = abs_top_level.parent model_path = search_path.joinpath('gen_model') if not model_path.exists(): raise FileNotFoundError('Models not generated!') model_fname = model_path / (abs_top_level.stem + '.m2isarmodel') output_base_path = search_path.joinpath('gen_output') output_base_path.mkdir(exist_ok=True) logger.info("loading models") with open(model_fname, 'rb') as f: model_obj: "M2Model" = pickle.load(f) if model_obj.model_version != M2_METAMODEL_VERSION: logger.warning("Loaded model version mismatch") cores = model_obj.cores core = cores[args.core_name] readlen = max(core.instr_classes) // 8 steplen = min(core.instr_classes) // 8 instrs_by_size = defaultdict(dict) # group instructions by their codeword width for k, v in core.instructions.items(): instrs_by_size[v.size][k] = v # sort instructions by opcode for k, v in instrs_by_size.items(): instrs_by_size[k] = dict(sorted(v.items(), key=sort_instruction, reverse=True)) instrs_by_size = dict(sorted(instrs_by_size.items())) prev_count = 0 if not args.raw: for region_name, base_addr, data, symbols in get_disassembly_regions(args.bin): logger.info( "Disassembling %s at 0x%x (%d bytes)", region_name, base_addr, len(data), ) disassemble_region( data, symbols, base_addr, core, instrs_by_size, readlen, steplen, args, ) return logger.info("Reading raw bytes from binary file") with open(args.bin, "rb") as f: # read at most XLEN bytes at a time while iw_read := f.peek(readlen): # truncate read data as peek is not guaranteed to return exactly XLEN bytes iw = iw_read[:readlen] # look for instruction found_ins = None for cls in sorted(core.instr_classes): ii = int.from_bytes(iw[:cls // 8], "little") i = find_instr(ii, instrs_by_size[cls]) if i is not None: found_ins = i if found_ins is None: ins_str = "unknown" step = steplen if prev_count > 2: print(f"\trepeated {prev_count-2} times.") prev_count = 0 # decode instruction operands else: if found_ins and found_ins.name == "DII": prev_count += 1 if prev_count > 1: bla = f.tell() f.seek(step, SEEK_CUR) continue else: if prev_count > 2: print(f"\trepeated {prev_count-2} times.") prev_count = 0 operands = decode(ii, found_ins) if args.format: asm_name = found_ins.mnemonic assembly = found_ins.assembly fmt = AsmFormatter() if assembly is None: assembly = "" asm_args = fmt.format(assembly, **operands) else: asm_name = found_ins.name op_str = " | ".join([f"{k}={v}" for k, v in operands.items()]) asm_args = f"[{op_str}]" ins_str = f"{asm_name}\t{asm_args}" step = found_ins.size // 8 # print decoded instruction mnemonic iword = int.from_bytes(iw[:step], "little") iword = "{iword:0{step}x}".format(iword=iword, step=step*2) print(f"{f.tell():08x}: {iword:<16} {ins_str}") f.seek(step, SEEK_CUR)
if __name__ == "__main__": try: main() except BrokenPipeError: pass