#!/usr/bin/env python3 """ Convert a PS2 EE ELF into a tb_ee_core_elf_runner-compatible image manifest pair (no external deps; pure stdlib). Emits two files at the requested output prefix, identical in format to generate_synthetic_image.py: .image.hex iverilog $readmemh, @ directives for populated 128-bit qwords only. Each line is 32 hex chars (MSB-first, byte 15 leftmost). .manifest.hex line 0 = ELF entry point (32-bit hex) line 1 = stack-top hint (32-bit hex) Supports ELF32 little-endian, ELFCLASS32, EM_MIPS, e_type ET_EXEC or ET_DYN. PT_LOAD segments are placed at their physical address (low 29 bits of p_vaddr — strips the kuseg/kseg0/kseg1 alias bits so the data lands at the correct phys offset in ee_ram_stub). Stack-top is approximated as (ee_ram_bytes - 0x10) since real PS2 ELFs don't carry a stack pointer in their headers; the TB will set $sp to this if the manifest is read. Usage: elf_to_eeram.py --in path/to/game.elf --out-prefix /tmp/game Verdict-aware notes: * Segments overflowing the EE RAM image cause a fatal error. * Segments overlapping each other are flagged but not fatal — the later one wins (matches how a real loader would behave). """ import sys import struct import argparse ELFMAG = b"\x7fELF" ELFCLASS32 = 1 ELFDATA2LSB = 1 EM_MIPS = 8 ET_EXEC = 2 ET_DYN = 3 PT_LOAD = 1 def parse_elf32_le(data: bytes): """Return (entry, [(p_vaddr, p_offset, p_filesz, p_memsz), ...]) for PT_LOAD segments of a 32-bit little-endian MIPS ELF. Raises ValueError on bad magic / wrong class / wrong arch. """ if len(data) < 52 or data[:4] != ELFMAG: raise ValueError("not an ELF file (bad magic)") if data[4] != ELFCLASS32: raise ValueError(f"only ELFCLASS32 supported (got class={data[4]})") if data[5] != ELFDATA2LSB: raise ValueError(f"only little-endian supported (got data={data[5]})") (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) = \ struct.unpack_from(" ee_ram_bytes: raise ValueError( f"PT_LOAD at vaddr=0x{p_vaddr:08x} phys=0x{phys:08x} " f"size=0x{p_memsz:x} overflows EE RAM (0x{ee_ram_bytes:x})") # Detect overlap (informational only). for (lo, hi) in placed: if not (phys + p_memsz <= lo or phys >= hi): print(f"[elf_to_eeram] WARNING: PT_LOAD at phys=0x{phys:08x} " f"size=0x{p_memsz:x} overlaps prior placement", file=sys.stderr) placed.append((phys, phys + p_memsz)) # Copy p_filesz bytes from file at p_offset → phys. p_memsz can # be larger than p_filesz (.bss tail); image is already zero- # initialised so the tail is naturally zero. chunk = elf_bytes[p_offset:p_offset + p_filesz] image[phys:phys + p_filesz] = chunk print(f"[elf_to_eeram] placed PT_LOAD vaddr=0x{p_vaddr:08x} " f"phys=0x{phys:08x} filesz=0x{p_filesz:x} memsz=0x{p_memsz:x}") return entry, image def qword_to_hex(image: bytearray, qw_phys: int) -> str: """MSB-first hex string for the qword at byte offset qw_phys.""" bytes16 = image[qw_phys:qw_phys + 16] return bytes16[::-1].hex() def emit_image_hex(image: bytearray, path: str) -> None: qw_size = 16 with open(path, "w") as f: f.write("// Ch270 ELF-derived EE-RAM image\n") f.write(f"// {len(image)} bytes / {len(image)//qw_size} qwords\n") f.write("// Populated qwords only; TB zero-inits before $readmemh.\n\n") any_emitted = False for qw_idx in range(0, len(image) // qw_size): qw_byte = qw_idx * qw_size qw_bytes = image[qw_byte:qw_byte + qw_size] if any(b != 0 for b in qw_bytes): f.write(f"@{qw_idx:08x}\n") f.write(qword_to_hex(image, qw_byte) + "\n") any_emitted = True if not any_emitted: f.write("@00000000\n00000000000000000000000000000000\n") def emit_manifest_hex(path: str, entry: int, stack_top: int) -> None: with open(path, "w") as f: f.write("// Ch270 manifest from ELF\n") f.write(f"// line 0 = entry, line 1 = stack_top hint\n") f.write(f"{entry:08x}\n") f.write(f"{stack_top:08x}\n") def main() -> int: p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) p.add_argument("--in", dest="elf_in", required=True, help="input ELF path") p.add_argument("--out-prefix", required=True, help="output file prefix") p.add_argument("--ee-ram-bytes", type=lambda s: int(s, 0), default=2 * 1024 * 1024, help="EE RAM size in bytes (default 2 MiB)") args = p.parse_args() with open(args.elf_in, "rb") as f: elf_bytes = f.read() entry, image = build_image(elf_bytes, args.ee_ram_bytes) stack_top = args.ee_ram_bytes - 0x10 emit_image_hex(image, f"{args.out_prefix}.image.hex") emit_manifest_hex(f"{args.out_prefix}.manifest.hex", entry, stack_top) print(f"[elf_to_eeram] wrote {args.out_prefix}.image.hex + " f"{args.out_prefix}.manifest.hex (entry=0x{entry:08x}, " f"stack_top=0x{stack_top:08x}, ee_ram={args.ee_ram_bytes} bytes)") return 0 if __name__ == "__main__": sys.exit(main())