Skip to content

Join unclaimed data ranges into their referencer (when only one referencer exists) - #146

Closed
StilesCrisis wants to merge 3 commits into
encounter:mainfrom
StilesCrisis:join-anon-data-to-referencing-unit
Closed

Join unclaimed data ranges into their referencer (when only one referencer exists)#146
StilesCrisis wants to merge 3 commits into
encounter:mainfrom
StilesCrisis:join-anon-data-to-referencing-unit

Conversation

@StilesCrisis

@StilesCrisis StilesCrisis commented Aug 30, 2026

Copy link
Copy Markdown

When decomping a function with a switch, the switch's jump table can sometimes land far away from the code in a different split. This prevented labels inside the function body from being represented properly; instead, the jump table would represented each location as myfunction + offset.

This isn't broken, but it's suboptimal, and it also prevents tools like m2c from decompiling the switch at all; it asserts because it expects the jump table to point directly at real labels.

This PR adds new logic which detects single-owner relocation tables like this, and ensures that they travel along with the function itself. In the project that I'm working on, this allowed 4 previously-generic auto_XX units to be absorbed into their real owning units (concretely, from 679 to 675 objects), meaning four new switch statements that will now m2c properly.

Separately, I sent a PR to the m2c repo which solved the issue on their end, and they asked me to investigate a dtk fix first. (That's at matt-kempster/m2c#360 )

create_gap_splits() currently buckets every address range not covered
by an explicit split entry into a generic auto_XX_ADDR_section unit,
even when every relocation inside that range points into exactly one
already-split function (e.g. an anonymous jump table living in its
own .rodata gap, entirely referenced by one .text function).

Since that jump table's home object is a different translation unit
than the function's, its entries can't use a GAS local (.L_) label to
address something in another object - the only cross-object-visible
name available is the enclosing function's own symbol, so write_asm's
existing label synthesis (util/asm.rs) falls back to plain
`funcName+offset` for every entry, including ones that alias another
label's exact address. That's what breaks m2c's switch/jump-table
handling downstream (matt-kempster/m2c#360) - m2c has to special-case
parsing `symbol+offset` jtbl entries because dtk's own asm output
never had a real label to give it in the first place.

Add single_referencing_unit(), which returns the split unit already
proven to be the sole address-owner of a candidate range (skipping
relocations that don't yet resolve to a known split, and refusing to
propose a unit that already owns a non-adjacent piece of this same
section - ObjInfo::add_split merges same-unit/same-section splits by
taking their min..max span, which is only correct for genuinely
adjacent pieces).

A generic gap can be large and contain many unrelated anonymous
blobs, not just one function's data, so the whole-gap check alone
rarely fires on real projects. ownership_run_end() finds the largest
symbol-aligned prefix of a gap that agrees on one owner, evaluating
ownership per symbol (a small, independent range) rather than
re-probing single_referencing_unit over a growing prefix - the latter
would hard-fail permanently on the first unresolvable relocation
anywhere in the gap, however far it sits from the actual data in
question.

create_gap_splits() now uses this to both narrow a gap's boundary and
name the resulting split in one step, instead of always minting a
fresh auto_ unit.

Verified against a real GameCube retail DOL (a community decomp
project's full ~8MB main.dol, 3694 functions, 679 objects at
baseline): output is unchanged except for 4 previously-generic
auto_XX units being absorbed into their real owning units (679 -> 675
objects; identical total .fn/.obj symbol count; no new "Unsplit data"
or other split_obj/validate_splits errors on a full run). The
motivating jump table - previously 21 entries of
`.4byte fn_800EB828+0x38` etc., all in a separate auto data unit -
now lives in the same unit as the function and emits real
`.rel fn_800EB828, .L_800EB860`-style relocations against real local
labels, including for entries that alias another entry's exact
target address.
@StilesCrisis
StilesCrisis force-pushed the join-anon-data-to-referencing-unit branch from 6dcfcd2 to 4739dc5 Compare August 30, 2026 21:45
@StilesCrisis StilesCrisis changed the title split: join unclaimed data ranges into their sole referencing unit split: join unclaimed data ranges into their referencer (when only one referencer exists) Aug 30, 2026
@StilesCrisis StilesCrisis changed the title split: join unclaimed data ranges into their referencer (when only one referencer exists) Join unclaimed data ranges into their referencer (when only one referencer exists) Aug 30, 2026
@StilesCrisis

Copy link
Copy Markdown
Author

@encounter Friendly ping, do you have any questions about this PR? I'm running with it locally and it's improved output for some switches.

@encounter

Copy link
Copy Markdown
Owner

Thanks for the PR! It looks directionally okay to me. Could you replace every comment with a hand-written one that better explains the logic? They read very Claude to me

@StilesCrisis

Copy link
Copy Markdown
Author

Absolutely, I can do that. Thanks for getting back to me :)

…ownership_run_end

Trim inline and doc comments to be more concise, and move the
add_split-merge rationale out of single_referencing_unit's doc
comment since it's an implementation detail rather than part of the
function's contract.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018n9Wm7wVbnqadSbbThVc7c
@StilesCrisis

Copy link
Copy Markdown
Author

Done.

@StilesCrisis

Copy link
Copy Markdown
Author

Actually, please hold off on merging this briefly. Re-running it against my project, I think there might be a bug. I'll keep you posted.

- create_gap_splits() joined an unclaimed range into its referencing unit on relocation evidence alone, never checking link order
- resolve_link_order() needs a unit's chunks in every section to agree on one global position; 4 of 37 joins on a real ~1300-object project contradicted that and made it cyclic
- moved the graph construction into link_order_graph(), which can also take not-yet-applied splits plus a candidate
- create_gap_splits() now rejects a join that would make the order cyclic and falls back to a plain auto_ split with no boundary trim

Apologies for the Claude-written prose in the previous version of this message on a human-facing PR; that was uncalled for.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018n9Wm7wVbnqadSbbThVc7c
@StilesCrisis
StilesCrisis force-pushed the join-anon-data-to-referencing-unit branch from d745249 to aa790c2 Compare September 4, 2026 22:08
@StilesCrisis

Copy link
Copy Markdown
Author

Yeah, this has a bug when running it against a sufficiently large codebase. It ends up causing “cyclic reference” errors. I’ll keep digging, but will close this for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants