fix(psyq_identify): read .text bytes, not objdump's rendering — 10 more objects located

obj_text_pattern parsed ONE WORD PER DISASSEMBLY LINE, and objdump collapses a
run of identical words into a single `...` line. Every collapsed word was
silently missing from the pattern, so from the first run onward the pattern was
MISALIGNED against the image and find() returned None -- printed as the
confident, wrong sentence "not linked by EXE".

Measured on 2D_BG0.o (libgs): 3 `...` lines, 520 words parsed for a 526-word
object. It was listed as absent while 507 of its 507 non-relocated words match
the EXE exactly at 0x8005080C. Any object whose .text holds a run of >=3
identical words was invisible -- to the map the entire library-linking pipeline
consumes for placement.

That is why 2D_BG0.o was never linked: not excluded by a reason, just invisible.
It sits in config/splat.us.exe.yaml under a scattered-.bss exclusion that cannot
apply to it, since the object has no .bss section at all.

Reading the section bytes and taking relocation offsets from `objdump -r`
removes the pretty-printer from the loop (R33).

MEASURED: libgs goes from 36/201 to 46/201 objects located.
This commit is contained in:
Drew T
2026-09-03 22:35:12 -06:00
parent b31e499c9b
commit f030992c67
+32 -24
View File
@@ -34,33 +34,41 @@ text = b[TLO - VRAM_BASE: THI - VRAM_BASE]
twords = [struct.unpack_from("<I", text, i)[0] for i in range(0, len(text), 4)]
def obj_text_pattern(o):
"""Return (words, mask) for the object's .text; mask[i]=0 on relocated/jump words.
"""Return (words, mask) for the object's `.text`, READ FROM THE SECTION BYTES.
A data-only object (no `.text` section — e.g. libgs GLOBAL.o, which defines only
globals) makes `objdump -j .text` exit non-zero; treat that as an empty .text so the
caller's `no-.text` path handles it instead of crashing."""
p = subprocess.run(["mipsel-linux-gnu-objdump", "-dr", "-j", ".text", o],
capture_output=True, text=True)
if p.returncode != 0:
return [], []
d = p.stdout
words, mask = [], []
pending_reloc = False
for line in d.splitlines():
mi = re.match(r"\s+([0-9a-f]+):\s+([0-9a-f]{8})\s", line)
if mi:
w = int(mi.group(2), 16)
words.append(w)
# mask jal/j (opcode 2/3) always (R_MIPS_26 target is link-resolved)
mask.append(0 if (w >> 26) in (2, 3) else 0xFFFFFFFF)
elif "R_MIPS" in line and words:
# relocation annotation follows its instruction line -> mask low 16 (hi/lo/pc16)
if "_26" in line:
mask[-1] = 0
else:
mask[-1] = 0xFFFF0000
WHY NOT `objdump -dr` (P31 S77). The first version parsed one word per DISASSEMBLY LINE, and
objdump COLLAPSES a run of identical words into a single `...` line. Every collapsed word was
silently missing from the pattern, so from the first run onward the pattern was MISALIGNED
against the image and `find` returned None -- reported as the confident, wrong sentence
"not linked by EXE".
Measured on `2D_BG0.o` (libgs): 3 `...` lines, **520 words parsed for a 526-word object**, so
it was listed as absent while 507 of its 507 non-relocated words match the EXE exactly at
0x8005080C. It had been excluded from the LINKED build under a `.bss` reason that cannot even
apply to it -- the object has no `.bss` section at all. Any object whose `.text` holds a run of
>=3 identical words was invisible to this map, which is the map the whole library-linking
pipeline consumes.
Reading the section bytes and taking relocation offsets from `objdump -r` removes the rendering
from the loop entirely (R33: derive from the bytes, do not re-parse a pretty-printer)."""
h = subprocess.run(["mipsel-linux-gnu-objdump", "-h", o], capture_output=True, text=True).stdout
m = re.search(r"^\s*\d+\s+\.text\s+([0-9a-f]+)\s+\S+\s+\S+\s+([0-9a-f]+)", h, re.M)
if not m:
return [], [] # data-only object (e.g. libgs GLOBAL.o)
size, off = int(m.group(1), 16), int(m.group(2), 16)
blob = open(o, "rb").read()[off:off + size]
words = list(struct.unpack("<%dI" % (len(blob) // 4), blob[:len(blob) // 4 * 4]))
mask = [0 if (w >> 26) in (2, 3) else 0xFFFFFFFF for w in words] # jal/j always link-resolved
rr = subprocess.run(["mipsel-linux-gnu-objdump", "-r", o], capture_output=True, text=True).stdout
body = re.search(r"RELOCATION RECORDS FOR \[\.text\]:(.*?)(?=\nRELOCATION RECORDS|\Z)", rr, re.S)
if body:
for rm in re.finditer(r"^([0-9a-f]+)\s+(R_MIPS_\w+)", body.group(1), re.M):
i = int(rm.group(1), 16) // 4
if 0 <= i < len(mask):
mask[i] = 0 if "_26" in rm.group(2) else 0xFFFF0000
return words, mask
def find(words, mask):
n = len(words)
if n < 2: