Files
Mulberry/tools/python/reldmp.py
T
2026-07-12 20:40:56 -05:00

389 lines
12 KiB
Python

# /// script
# requires-python = ">=3.12"
# dependencies = [
# "pyelftools>=0.32",
# ]
# ///
from dataclasses import dataclass
from pathlib import Path
import struct
import sys
from elftools.elf.elffile import ELFFile
from elftools.elf.relocation import RelocationSection
from elftools.elf.sections import Section, Symbol, SymbolTableSection
from elftools.elf.constants import E_FLAGS, E_FLAGS_MASKS
from elftools.elf.enums import ENUM_RELOC_TYPE_MIPS
from elftools.elf.descriptions import _DESCR_RELOC_TYPE_MIPS
from enum import IntEnum
# Elftools enums are just dicts...
class MipsReloc(IntEnum):
R_MIPS_NONE = 0
R_MIPS_32 = 2
R_MIPS_26 = 4
R_MIPS_HI16 = 5
R_MIPS_LO16 = 6
R_MIPS_GPREL16 = 7
R_MIPS_LITERAL = 8
R_MIPS15_S3 = 119
R_MIPS_DVP_11_PCREL = 120
R_MIPS_DVP_27_S4 = 121
R_MIPS_DVP_11_S4 = 122
R_MIPS_DVP_U15_S3 = 123
_U32 = struct.Struct("<I")
_S16 = struct.Struct("<h")
_U16 = struct.Struct("<H")
def read_generic(f, _s: struct.Struct, pos: int = -1) -> int:
if pos != -1:
old = f.tell()
f.seek(pos)
value = _s.unpack(f.read(_s.size))[0]
if pos != -1:
f.seek(old)
return value
def read_u32(f, pos: int = -1) -> int:
return read_generic(f, _U32, pos)
def read_u16(f, pos: int = -1) -> int:
return read_generic(f, _U16, pos)
def read_s16(f, pos: int = -1) -> int:
return read_generic(f, _S16, pos)
@dataclass
class RelInfo:
rom_off: int
addr: int
type: MipsReloc
sym_name: str
sym_addr: int
addend: int = 0
comment_out: bool = False
EF_MIPS_MACH = 0x00FF0000
E_MIPS_MACH_R5900 = 0x00920000
ELF_PATH = Path(r"config/SLUS_210.07")
# VADDR for the start of the .lit4/LITERAL section
LIT4_SECTION_START = 0x003C7C80
def main():
_monkey_patch()
with ELFFile(ELF_PATH.open("rb")) as elf:
if not check_elf(elf):
sys.exit(-1)
make_rels_file(elf)
make_syms_file(elf)
def make_syms_file(elf: ELFFile) -> None:
nono_chars = [
"<",
">",
":",
"\"",
"/",
"\\",
"|",
"?",
"*"
]
known_ldsyms = {
"__bss_start",
"__bss_end",
"__bss_size",
"_fbss",
"__data_start",
"__data_end",
"_heap_size",
"_stack_size",
"_end",
"_gp",
# Should these be here? Technically they are used by mwInit
# "__exception_table_start__",
# "__exception_table_end__",
# "__static_init",
# "__static_init_end",
# "_overlay_group_addresses",
}
# There should only be one symtab anyway
symtab = get_symtab_section(elf)
if symtab is None:
return
with open("symbols.txt", "w+") as rf:
all_syms = list(symtab.iter_symbols())
seen_symbols = set()
seen_addr = set()
duped_symbols = set()
duped_addr = set()
for sym in all_syms:
name = sym.name
addr = sym.entry["st_value"]
if name in seen_symbols:
duped_symbols.add(name)
elif addr in seen_addr:
duped_addr.add(addr)
else:
seen_symbols.add(name)
seen_addr.add(addr)
for sym in all_syms:
addr = sym.entry["st_value"]
size = sym.entry["st_size"]
# vis seems to always be STV_DEFAULT
vis = sym.entry["st_other"]["visibility"]
# local seems to always be 0
local = sym.entry["st_other"]["local"]
bind = sym.entry["st_info"]["bind"]
type = sym.entry["st_info"]["type"]
# assert (vis == "STV_DEFAULT" and local == 0)
# Skip common autogenerated symbols
if sym.name in ("__gnu_compiled_c", "gcc2_compiled."):
continue
# Anything below 0x100000 shouldn't exist
if addr < 0x100000:
continue
# Skip section symbols
if type == "STT_SECTION":
continue
comment = ""
# Skip local ASM labels, VU microcode labels and known linker symbols
if (sym.name.startswith("$")
or sym.name.startswith("_$")
or sym.name.startswith(".vif")
or sym.name.startswith(".dma")
or sym.name.startswith(".gif")
or sym.name.startswith(".vu")
or sym.name in known_ldsyms
):
remove = "// "
else:
remove = ""
if size != 0:
comment += f" size:0x{size:X}"
match (bind):
case "STB_GLOBAL":
# The default
comment += " visibility:global"
pass
case "STB_LOCAL":
comment += " visibility:local"
case "STB_WEAK":
comment += " visibility:weak"
case "STB_LOPROC":
# Custom MWCC thing, it's called "MULTIDEF"
# appears exactly once in Code Veronica
# due to `inline TYPE function();` declaration
# maps roughly to `weak`, but it's distinct
comment += " visibility:weak"
case _:
raise ValueError(f"Unknown symbol binding {bind} ({sym.name})")
if sym.name in duped_symbols or addr in duped_addr:
comment += " allow_duplicated:True"
if len(sym.name) > 128:
comment += f" filename:long_symbol_{addr:08X}"
elif any(char in sym.name for char in nono_chars):
comment += f" filename:bad_symbol_{addr:08X}"
rf.write(f"{remove}{sym.name:<50} = 0x{addr:08X}; //{comment}\n")
def make_rels_file(elf: ELFFile) -> None:
all_relocs = parse_rels(elf)
with open("relocs.txt", "w+") as rf:
for reloc in all_relocs:
addend = reloc.addend
rom_off = reloc.rom_off
if addend == 0:
addend_str = ""
else:
sign = "+" if addend > 0 else "-"
addend_str = f"addend:{sign}0x{abs(addend):X}"
if reloc.comment_out:
cmm = "// "
else:
cmm = ""
rf.write(f"{cmm}rom:0x{rom_off:06X} reloc:{reloc.type.name[2:]:<19} {addend_str:<15} symbol:{reloc.sym_name}\n")
def get_reloc_section(elf: ELFFile) -> tuple[RelocationSection | None, SymbolTableSection | None]:
relmain: RelocationSection | None = elf.get_section_by_name(".relmain")
if relmain is None:
print("No .relmain section!")
return None, None
if relmain['sh_link'] == 0:
print("Reloc section has no associated symbol table!")
sys.exit(-1)
symtable: SymbolTableSection = elf.get_section(relmain['sh_link'])
return relmain, symtable
def get_symtab_section(elf: ELFFile) -> tuple[SymbolTableSection | None]:
symtable: SymbolTableSection | None = elf.get_section_by_name(".symtab")
if symtable is None:
print("No .symtab section!")
return symtable
def parse_rels(elf: ELFFile) -> list[RelInfo]:
# Factor this out?
main_section: Section | None = elf.get_section_by_name("main")
if main_section is None:
print("No main section!")
sys.exit(-1)
vaddr_diff = main_section.header["sh_addr"] - main_section.header["sh_offset"]
relmain, symtab = get_reloc_section(elf)
if relmain is None or symtab is None:
return list()
gp_value = _get_gp_value(elf)
pending_hi: list[RelInfo] = []
seen_hi: dict[str, int] = {}
all_relocs: list[RelInfo] = []
for r in relmain.iter_relocations():
s: Symbol = symtab.get_symbol(r['r_info_sym'])
reloc = RelInfo(
r["r_offset"] - vaddr_diff,
0,
MipsReloc(r["r_info_type"]),
s.name,
s.entry["st_value"]
)
instr = read_u32(elf.stream, reloc.rom_off)
simm = read_s16(elf.stream, reloc.rom_off)
uimm = instr & 0xFFFF
match reloc.type:
case MipsReloc.R_MIPS_NONE:
# NONE-thing to do
pass
case MipsReloc.R_MIPS_32:
reloc.addr = instr
reloc.addend = instr - reloc.sym_addr
case MipsReloc.R_MIPS_26:
reloc.addr = (instr & 0x03FFFFFF) * 4
reloc.addend = reloc.addr - reloc.sym_addr
case MipsReloc.R_MIPS_HI16:
reloc.addr = uimm << 0x10
pending_hi.append(reloc)
case MipsReloc.R_MIPS_LO16:
# This logic roughly matches GNU AS (_bfd_mips_elf_hi16_reloc)
# and produces corrected addends for all the "orphaned" LO16s
# in Code Veronica save for one ambiguous entry at ROM:14AA8
# so I guess it's good enough.
for hi in pending_hi:
hi.addr += simm
hi.addend = hi.addr - hi.sym_addr
if simm < 0:
seen_hi[hi.sym_name] = (hi.addr + 0x10000) & 0xFFFF0000
else:
seen_hi[hi.sym_name] = hi.addr & 0xFFFF0000
reloc.addr = seen_hi[reloc.sym_name] + simm
reloc.addend = reloc.addr - reloc.sym_addr
pending_hi.clear()
case MipsReloc.R_MIPS_GPREL16:
reloc.addr = gp_value + simm
reloc.addend = reloc.addr - reloc.sym_addr
case MipsReloc.R_MIPS_LITERAL:
# Literals are also not supported, as GNU AS
# doesn't have a reloc operator for them...
# We can lie and say it's a gprel reloc instead but meh
reloc.addend = gp_value + simm - LIT4_SECTION_START
# reloc.type = MipsReloc.R_MIPS_GPREL16
# reloc.sym_name = f"D_{gp_value + simm:08X}"
reloc.comment_out = True
case MipsReloc.R_MIPS15_S3:
# Needs upstream support...
reloc.comment_out = True
case (MipsReloc.R_MIPS_DVP_U15_S3 | MipsReloc.R_MIPS_DVP_27_S4):
# Nothing to do, it's only for VU1 microcode
reloc.comment_out = True
case _:
raise ValueError(f"Unimplemented reloc {reloc.type}")
all_relocs.append(reloc)
return all_relocs
def _get_gp_value(elf: ELFFile):
reginfo = elf.get_section_by_name(".reginfo")
# It better not use GP
if reginfo is None:
return 0
addr = reginfo.header["sh_offset"]
return read_u32(elf.stream, addr + 0x14)
def check_elf(elf: ELFFile) -> bool:
flags = elf.header["e_flags"]
type = elf.header["e_type"]
if type != "ET_EXEC":
print("Not a static file!")
return False
if ((flags & E_FLAGS_MASKS.EFM_MIPS_ABI) == E_FLAGS_MASKS.EFM_MIPS_ABI_EABI64
and (flags & E_FLAGS.EF_MIPS_ARCH) != E_FLAGS.EF_MIPS_ARCH_3
and(flags & EF_MIPS_MACH) != E_MIPS_MACH_R5900):
print("Not an R5900 elf file!")
return False
if elf.header["e_shnum"] == 0:
print("No section headers!")
return False
return True
def _monkey_patch():
# Add DVP reloc types, though splat doesn't support them as of yet
# TODO: ask for R_MIPS15_S3 support at some point, the others
# are only for the VU microcode, which is out of scope
ENUM_RELOC_TYPE_MIPS["R_MIPS15_S3"] = 119
ENUM_RELOC_TYPE_MIPS["R_MIPS_DVP_11_PCREL"] = 120
ENUM_RELOC_TYPE_MIPS["R_MIPS_DVP_27_S4"] = 121
ENUM_RELOC_TYPE_MIPS["R_MIPS_DVP_11_S4"] = 122
ENUM_RELOC_TYPE_MIPS["R_MIPS_DVP_U15_S3"] = 123
_DESCR_RELOC_TYPE_MIPS[119] = "R_MIPS15_S3"
_DESCR_RELOC_TYPE_MIPS[120] = "R_MIPS_DVP_11_PCREL"
_DESCR_RELOC_TYPE_MIPS[121] = "R_MIPS_DVP_27_S4"
_DESCR_RELOC_TYPE_MIPS[122] = "R_MIPS_DVP_11_S4"
_DESCR_RELOC_TYPE_MIPS[123] = "R_MIPS_DVP_U15_S3"
if __name__ == "__main__":
main()