From 3ebbec942643c85e8ffecffc80bf7b00ea1398da Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:03:50 -0600 Subject: [PATCH] =?UTF-8?q?fix(psyq=5Fintegrate):=20main=20was=20RED=20on?= =?UTF-8?q?=20every=20incremental=20relink=20=E2=80=94=20make=20the=20exte?= =?UTF-8?q?rnals=20file=20monotonic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit THE TRUE IDENTITY OF THE LONG-STANDING 'main link defect' (2026-08-15). The extra C function never broke the link; the RELINK it forced did. integrate() derives each *_externals.ld from trial_undefined() against the CURRENT ld_path, so its answer depends on how much of the linker script has ALREADY been rewritten. On a virgin splat .ld the apicard region is still the stub object (defining only firstfile2), so at the libmcrd stage 'firstfile' is undefined and gets an entry. On an already-rewritten .ld, A66.o is present and defines 'firstfile' at 0x80062248, the trial no longer reports it undefined, and the entry 'firstfile = 0x80061FA8;' is DROPPED -- after which LIBMCRD's jal binds to A66.o and main comes out 2 of 413,696 bytes different from retail (file 0x51674, VA 0x80060E74, retail jal 0x80061FA8 vs built jal 0x80062248). That is why main was green ONLY on the first build after a fresh extract, and it is why NO main draft could ever bank through an incremental gate: the baseline was already red before any draft was spliced. integrate()'s own comment already CLAIMED this operation was idempotent ('a re-run on an already-rewritten .ld only redoes syms'). This makes it true: the externals map is merged with the file's prior contents, newly-derived values winning on a name collision, names the new derivation no longer sees kept at their previous address. The file becomes a function of the tree, not of how many times this ran. It reports what it kept rather than doing it silently. VERIFIED, three builds: fresh extract + build ...... GREEN (unchanged) INCREMENTAL relink ......... GREEN (was RED -- the failing case) third relink ............... GREEN (monotonic across repeats) and the merge is observed firing: 'kept 6/15/2 extern(s) this re-run no longer saw as undefined' across the integrate stages. Root-caused by a Fable agent, verified here against the bytes. --- tools/psyq_integrate.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/tools/psyq_integrate.py b/tools/psyq_integrate.py index a14ff3ab5..fa8dc59ab 100644 --- a/tools/psyq_integrate.py +++ b/tools/psyq_integrate.py @@ -160,9 +160,41 @@ def integrate(elf_dir, ld_path, objdir, syms_path, stubs, lo=None, hi=None, externals[s] = recovered[s] else: missing.append(s) + # MONOTONIC MERGE — the externals file may only GROW for a given tree state. + # + # `trial_undefined` above is evaluated against the CURRENT ld_path, so its answer depends on how + # much of the linker script has ALREADY been rewritten. On a virgin splat .ld the apicard region + # is still the stub object (which defines only `firstfile2`), so at the libmcrd stage `firstfile` + # is undefined and gets an entry. On an ALREADY-REWRITTEN .ld, A66.o is present and defines + # `firstfile` at 0x80062248, the trial no longer reports it undefined, and the entry + # `firstfile = 0x80061FA8;` was DROPPED — after which LIBMCRD's `jal` binds to A66.o and main + # comes out 2 bytes different from retail (file 0x51674, VA 0x80060E74). + # + # That is why main was green only on the first build after a fresh extract and red on every + # incremental relink, and it is the true identity of the long-standing "main link defect" + # (2026-08-15): the extra C function did not break the link, the RELINK it forced did. + # integrate()'s own comment at the top already claimed this operation was idempotent + # ("a re-run on an already-rewritten .ld only redoes syms"); this makes that true. + # + # Newly-derived values WIN on a name collision (an address that legitimately moved must move); + # names the new derivation no longer sees are KEPT at their previous address. So the file is a + # function of the tree, not of the number of times this ran. Diagnosed P31 S68 (fable). + prior = {} + if os.path.exists(syms_path): + for ln in open(syms_path): + m = re.match(r"(\w+)\s*=\s*0x([0-9A-Fa-f]+)", ln) + if m: + prior[m.group(1)] = int(m.group(2), 16) + readded = sorted(set(prior) - set(externals)) + merged = dict(prior) + merged.update(externals) # new derivation wins where both have the name with open(syms_path, "w") as f: - for s, a in sorted(externals.items(), key=lambda kv: kv[1]): + for s, a in sorted(merged.items(), key=lambda kv: kv[1]): f.write(f"{s} = 0x{a:08X};\n") + if readded: + print(f" (kept {len(readded)} extern(s) this re-run no longer saw as undefined: " + f"{readded[:8]}{'...' if len(readded) > 8 else ''})") + externals = merged print(f"integrate {os.path.basename(elf_dir)}: {len(order)} objects in {len(blocks)} block(s) " f"-> stubs {stubs}; {len(nol)} NOLOAD sections; {len(externals)} externals -> {syms_path}") if missing: