diff options
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/greeneye/elf.py')
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/elf.py | 322 |
1 files changed, 322 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/greeneye/elf.py b/debug/risc-v-gsp-rm-vm/greeneye/elf.py new file mode 100644 index 0000000..abfccd9 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/greeneye/elf.py @@ -0,0 +1,322 @@ +"""Minimal ELF64/ELF32 reader (stdlib only). + +Needed for three things: + * symbol resolution for the demo uproc firmware (breakpoints by name), + * program-header extraction so the real GSP ``.fwimage`` can be placed into + QEMU RAM (and relocated when a probe needs it), + * entry-point detection for the first-fault firmware probe. + +This is not a full ELF library; it parses headers, PT_LOAD segments and +``.symtab`` symbols, which is all green-eye needs. +""" +from __future__ import annotations + +import struct +from dataclasses import dataclass, field as _field + +from .errors import GreenEyeError + +EM_RISCV = 243 +PT_LOAD = 1 +SHT_SYMTAB = 2 +SHT_STRTAB = 3 +STT_FUNC = 2 +STT_OBJECT = 1 +STT_NOTYPE = 0 + + +class ElfError(GreenEyeError): + """Malformed or unsupported ELF file.""" + + +@dataclass +class Segment: + p_type: int + p_flags: int + p_offset: int + p_vaddr: int + p_paddr: int + p_filesz: int + p_memsz: int + p_align: int + + @property + def is_load(self) -> bool: + return self.p_type == PT_LOAD + + @property + def writable(self) -> bool: + return bool(self.p_flags & 0x2) + + @property + def executable(self) -> bool: + return bool(self.p_flags & 0x1) + + +@dataclass +class ElfSymbol: + name: str + value: int + size: int + bind: int + type: int + + @property + def is_function(self) -> bool: + return self.type == STT_FUNC + + @property + def is_object(self) -> bool: + return self.type == STT_OBJECT + + +@dataclass +class FirmwareImage: + path: str + is64: bool + e_machine: int + entry: int + segments: list[Segment] = _field(default_factory=list) + symbols: list[ElfSymbol] = _field(default_factory=list) + _blob: bytes | None = None + + @property + def is_riscv(self) -> bool: + return self.e_machine == EM_RISCV + + @property + def load_segments(self) -> list[Segment]: + return [s for s in self.segments if s.is_load] + + @property + def min_vaddr(self) -> int: + loads = self.load_segments + return min((s.p_vaddr for s in loads), default=0) + + @property + def max_vaddr(self) -> int: + loads = self.load_segments + return max((s.p_vaddr + s.p_memsz for s in loads), default=0) + + def read(self) -> bytes: + if self._blob is None: + with open(self.path, "rb") as fh: + self._blob = fh.read() + return self._blob + + def segment_bytes(self, seg: Segment) -> bytes: + return self.read()[seg.p_offset : seg.p_offset + seg.p_filesz] + + def flat_load_plan(self, bias: int = 0) -> list[tuple[int, bytes]]: + """Return ``(load_addr, data)`` pairs for every PT_LOAD segment. + + ``bias`` is added to each segment's virtual address (use it to place a + firmware linked for GPU physical addresses into QEMU RAM). + """ + plan: list[tuple[int, bytes]] = [] + for seg in self.load_segments: + data = bytearray(self.segment_bytes(seg)) + if len(data) < seg.p_memsz: + data += b"\x00" * (seg.p_memsz - len(data)) + plan.append((seg.p_vaddr + bias, bytes(data))) + return plan + + def relocated_entry(self, bias: int = 0) -> int: + return self.entry + bias + + def summary(self) -> str: + return ( + f"{self.path}: ELF{'64' if self.is64 else '32'} " + f"machine={self.e_machine} entry={self.entry:#x} " + f"segments={len(self.segments)} loads={len(self.load_segments)} " + f"symbols={len(self.symbols)} " + f"vaddr=[{self.min_vaddr:#x},{self.max_vaddr:#x})" + ) + + +def load_elf(path: str) -> FirmwareImage: + with open(path, "rb") as fh: + data = fh.read() + return parse_elf(data, path=path) + + +def parse_elf(data: bytes, path: str = "<memory>") -> FirmwareImage: + if len(data) < 52 or data[:4] != b"\x7fELF": + raise ElfError(f"{path}: not an ELF file") + ei_class = data[4] + if ei_class == 2: + is64 = True + hdr_fmt = "<HHIQQQIHHHHHH" + elif ei_class == 1: + is64 = False + hdr_fmt = "<HHIIIIIHHHHHH" + else: + raise ElfError(f"{path}: unknown EI_CLASS {ei_class}") + hdr_size = struct.calcsize(hdr_fmt) + fields = struct.unpack_from(hdr_fmt, data, 16) + ( + _e_type, + e_machine, + _e_version, + e_entry, + e_phoff, + e_shoff, + _e_flags, + _e_ehsize, + e_phentsize, + e_phnum, + e_shentsize, + e_shnum, + e_shstrndx, + ) = fields + + image = FirmwareImage(path=path, is64=is64, e_machine=e_machine, entry=e_entry) + + if is64: + ph_fmt = "<IIQQQQQQ" + sh_fmt = "<IIQQQQIIQQ" + sym_fmt = "<IBBHQQ" + else: + ph_fmt = "<IIIIIIII" + sh_fmt = "<IIIIIIIIII" + sym_fmt = "<IIIBBH" + + ph_size = struct.calcsize(ph_fmt) + for i in range(e_phnum): + base = e_phoff + i * e_phentsize + if base + ph_size > len(data): + break + p = struct.unpack_from(ph_fmt, data, base) + image.segments.append(Segment(*p[:8])) + + # Sections -> symbols + sh_size = struct.calcsize(sh_fmt) + sections: list[tuple[int, int, int, int]] = [] # type, offset, size, link + for i in range(e_shnum): + base = e_shoff + i * e_shentsize + if base + sh_size > len(data): + break + sh = struct.unpack_from(sh_fmt, data, base) + sh_type, sh_offset, sh_size_val, sh_link = sh[1], sh[4], sh[5], sh[6] + sections.append((sh_type, sh_offset, sh_size_val, sh_link)) + + for sh_type, sh_offset, sh_size_val, sh_link in sections: + if sh_type != SHT_SYMTAB: + continue + strtab_off, strtab_size = 0, 0 + if 0 <= sh_link < len(sections): + _, strtab_off, strtab_size, _ = sections[sh_link] + strtab = data[strtab_off : strtab_off + strtab_size] + count = sh_size_val // struct.calcsize(sym_fmt) + for i in range(count): + base = sh_offset + i * struct.calcsize(sym_fmt) + if base + struct.calcsize(sym_fmt) > len(data): + break + st = struct.unpack_from(sym_fmt, data, base) + if is64: + st_name, st_info, _st_other, _st_shndx, st_value, st_size_val = st + else: + st_name, st_value, st_size_val, st_info, _st_other, _st_shndx = st + if st_name == 0 or st_value == 0: + continue + name = _read_cstr(strtab, st_name) + if not name: + continue + image.symbols.append( + ElfSymbol( + name=name, + value=st_value, + size=st_size_val, + bind=(st_info >> 4) & 0xF, + type=st_info & 0xF, + ) + ) + return image + + +def _read_cstr(blob: bytes, offset: int) -> str: + if offset >= len(blob): + return "" + end = blob.find(b"\x00", offset) + if end == -1: + end = len(blob) + return blob[offset:end].decode("utf-8", "replace") + + +def write_relocated_elf(path: str, out_path: str, bias: int) -> str: + """Write a copy of an ELF64 whose PT_LOADs and entry are shifted by bias. + + ``p_paddr`` is forced to ``p_vaddr + bias`` so the layout matches the + virtual addresses. Useful for formats/tools that honour e_entry. + """ + with open(path, "rb") as fh: + data = bytearray(fh.read()) + if len(data) < 64 or data[:4] != b"\x7fELF" or data[4] != 2: + raise ElfError(f"{path}: relocation requires a 64-bit ELF") + e_entry = struct.unpack_from("<Q", data, 24)[0] + e_phoff = struct.unpack_from("<Q", data, 32)[0] + e_phentsize = struct.unpack_from("<H", data, 54)[0] + e_phnum = struct.unpack_from("<H", data, 56)[0] + struct.pack_into("<Q", data, 24, e_entry + bias) + for i in range(e_phnum): + base = e_phoff + i * e_phentsize + if base + 56 > len(data): + break + p_type = struct.unpack_from("<I", data, base)[0] + if p_type != PT_LOAD: + continue + p_vaddr = struct.unpack_from("<Q", data, base + 16)[0] + struct.pack_into("<Q", data, base + 16, p_vaddr + bias) + struct.pack_into("<Q", data, base + 24, p_vaddr + bias) + with open(out_path, "wb") as fh: + fh.write(data) + return out_path + + +def _trampoline(entry_offset: int) -> bytes: + """RV64 code at offset 0 that jumps to ``PC + entry_offset``. + + QEMU's RISC-V ``-kernel`` raw/ELF path starts the CPU at the RAM base for + images whose e_entry it does not honour; this 8-byte stub (auipc t0, hi; + jalr x0, t0, lo) makes the firmware entry reachable from the base without + patching the image itself. + """ + if not -0x80000000 <= entry_offset < 0x80000000: + raise ElfError("entry offset out of trampoline range") + hi20 = ((entry_offset + 0x800) >> 12) & 0xFFFFF + lo12 = entry_offset & 0xFFF + auipc = (hi20 << 12) | (5 << 7) | 0x17 # auipc t0, hi20 + jalr = (lo12 << 20) | (5 << 15) | 0x67 # jalr x0, t0, lo12 + return struct.pack("<II", auipc, jalr) + + +def write_flat_kernel( + path: str, + out_path: str, + *, + load_addr: int = 0x80000000, + trampoline: bool = True, +) -> tuple[str, int]: + """Flatten an ELF's PT_LOADs into a raw image QEMU can boot as `-kernel`. + + Returns ``(out_path, entry_addr)`` where ``entry_addr = load_addr + + (e_entry - min_vaddr)``. When the entry is not at offset 0 a tiny + PC-relative trampoline is written at offset 0 (QEMU starts at the load + base for raw kernels). ``load_addr`` is not encoded anywhere except in + the returned entry address: all code is position-independent relative to + the image base, exactly as it executes on the device. + """ + image = load_elf(path) + span = image.max_vaddr - image.min_vaddr + flat = bytearray(span) + for seg in image.load_segments: + start = seg.p_vaddr - image.min_vaddr + end = start + seg.p_filesz + flat[start:end] = image.segment_bytes(seg) + entry_offset = image.entry - image.min_vaddr + if trampoline and entry_offset != 0: + stub = _trampoline(entry_offset) + flat[0 : len(stub)] = stub + with open(out_path, "wb") as fh: + fh.write(flat) + return out_path, load_addr + entry_offset |
