#!/usr/bin/env python3 """Give every unit's objdiff target the data the unit defines. objdiff_data.py [--build-dir ps2/build/ntsc] [--lcf ] objdiff counts a unit's data from its target object alone, and splat's reference for a unit is its code plus the constants splat migrates into it -- no .data, .sdata, .sbss or .bss, nothing a unit places under a section name of its own (.vtables, .ctor, an overlay's .tdata), and no unit that has no code. So the data the source defines was mostly never counted, and the constants that were could not match: splat sizes a label up to the next one, padding included, spells a duplicated name with a `__N` suffix, and cuts the image into units by its own reckoning rather than the linker script's. This writes, after the link, one target per unit under /objdiff/: * the bytes are retail's; * they are cut and named the way the source cuts and names them: every datum the unit's object defines, at the address the link map says it landed on, under the name the object gives it, in a section named as the object's; * each datum is as long as retail's own symbol table says. Where retail gives no size, the datum runs to retail's next label; the source's size is taken only when it fits inside that and what is left is zero fill; * a pointer is relocated against whatever retail's word points at, named the way the source names that address -- a base relocation that disagrees with retail's word becomes a mismatch, not a copy; * the unit's code is splat's reference, as before. The constants splat migrated into it are kept but no longer allocated, so objdiff neither counts them twice nor sees a second copy to pair. Nothing here is linked: the images and every object the link takes are as they were. """ import argparse import bisect import os import re import struct import sys from pathlib import Path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import disassemble # noqa: E402 import layout # noqa: E402 import region # noqa: E402 import retail # noqa: E402 SHT_PROGBITS, SHT_SYMTAB, SHT_STRTAB, SHT_NOBITS, SHT_REL = 1, 2, 3, 8, 9 SHF_ALLOC, SHF_EXECINSTR = 2, 4 STB_LOCAL, STB_GLOBAL = 0, 1 STT_NOTYPE, STT_SECTION = 0, 3 R_MIPS_32 = 2 SHN_ABS = 0xFFF1 STV_HIDDEN = 2 MAP_LINE = re.compile(r"^ ([0-9A-F]{8}) ([0-9A-F]{8}) (\S+)\s+(.*)\t\((.+)\)$") IMAGE_HEADER = re.compile(r"^# \.(\w+)$") class Section: def __init__(self, name, sh_type, flags, addralign, data=b"", size=None, link=0, info=0, entsize=0, addr=0): self.name = name self.type = sh_type self.flags = flags self.addralign = addralign self.data = data self.size = len(data) if size is None else size self.link = link self.info = info self.entsize = entsize self.addr = addr class Symbol: def __init__(self, name, value, size, bind, sym_type, other, shndx): self.name = name self.value = value self.size = size self.bind = bind self.type = sym_type self.other = other self.shndx = shndx class Object: """A relocatable ELF32 little-endian object, read whole.""" def __init__(self, path): raw = Path(path).read_bytes() self.header = raw[:0x34] shoff, = struct.unpack_from("> 4, info & 0xF, other, shndx)) # {section index: [(offset, type, symbol index)]} self.relocations = {} for s in self.sections: if s.type == SHT_REL: self.relocations[s.info] = [ (off, info & 0xFF, info >> 8) for off, info in struct.iter_unpack("= 0 else None def build_data(base, linkmap_entries, retail_labels, globals_, by_address, stats): """The data sections a unit's target holds, cut and named as the source does. Returns [(section, [(symbol, offset, size)], [(offset, name, addend)])]. """ placed = place(base, linkmap_entries) where = {i for i, _address, _image in placed} for i, sym in enumerate(base.symbols): if sym.size and i not in where and (base.is_data(sym.shndx) or base.is_microcode(sym.shndx)): stats["unplaced"] += sym.size # The unit's own names first: `@N` and `x$N` are local, and every unit # has its own. names = dict(globals_) names.update((sym.name, sym.value) for sym in base.symbols if sym.shndx == SHN_ABS) names.update((base.symbols[i].name, address) for i, address, _image in placed) groups = {} for i, address, image in placed: sym = base.symbols[i] if sym.size == 0 or not (base.is_data(sym.shndx) or base.is_microcode(sym.shndx)): continue groups.setdefault(base.sections[sym.shndx].name, []).append((address, image, i)) out = [] for name, members in groups.items(): members.sort() template = next(s for s in base.sections if s.name == name) nobits = template.type == SHT_NOBITS align = max(s.addralign for s in base.sections if s.name == name) or 1 start, image = members[0][0], members[0][1] rows = [] end = start for address, _image, i in members: sym = base.symbols[i] size = retail_labels.size(image, address, sym.name, sym.size, nobits) if size != sym.size: stats["sized apart from the source"] += sym.size rows.append((address, size, i)) end = max(end, address + size) data = bytearray(b"" if nobits else retail.read(image, start, end - start)) relocs = [] for address, size, i in ([] if nobits else rows): sym = base.symbols[i] own = base.sections[sym.shndx].data for offset, r_type, target in base.relocations.get(sym.shndx, ()): if not sym.value <= offset < sym.value + sym.size: continue if r_type != R_MIPS_32: stats["relocations of another type"] += 1 continue at = address + offset - sym.value - start if at + 4 > address + size - start: continue word, = struct.unpack_from(" {build}/objdiff/") remarks = ", ".join(f"{key} {value}" for key, value in stats.items() if value) if remarks: print(f"objdiff_data: {remarks}") if args.stamp: Path(args.stamp).parent.mkdir(parents=True, exist_ok=True) Path(args.stamp).write_text(f"{written}\n", encoding="utf-8") return 0 if __name__ == "__main__": sys.exit(main())