Register Matching Recipes: GCC vs SN Systems¶
Overview¶
The Sims 2 GameCube was compiled with SN Systems ProDG (SN-NGC). We're using devkitPPC (GCC 15.2.0). Many functions compile to the correct size but have register allocation differences. This document catalogs the patterns and their C++ workarounds.
Recipe 1: Constant Store (r9 -> r0)¶
Symptom: GCC uses r9 for small integer constants stored to memory; SN uses r0.
Example (SN Systems):
GCC produces:
Fix: Bind the constant to r0 with an asm constraint:
// Before (GCC uses r9):
*(int*)((char*)this + 0xD8) = 1;
// After (GCC uses r0):
register int __r0 __asm__("r0") = 1;
__asm__ __volatile__("" : "+r"(__r0));
*(int*)((char*)this + 0xD8) = __r0;
Why it works: register int __r0 __asm__("r0") hints GCC to use r0. The
empty asm volatile with "+r"(__r0) prevents GCC from optimizing away the
register binding. Without the asm barrier, GCC ignores the hint.
Functions fixed: ~93 (constant stores like SetFlag, Hide, Show, Reset, Init)
Recipe 2: Reorder + Constant Store¶
Symptom: GCC emits the parameter store first, then li+stw for the constant. SN emits li first, then parameter store, then stw.
Example (SN Systems):
li r0,1 ; load constant first
stw r4,0(r3) ; then store parameter
stb r0,0x318(r3) ; then store constant
blr
GCC produces:
stw r4,0(r3) ; parameter store first
li r9,1 ; then load constant (using r9)
stb r9,0x318(r3) ; then store constant
blr
Fix: Declare the r0 register variable BEFORE any stores:
// Before:
*(int*)((char*)this + 0x0) = (int)p4;
*(char*)((char*)this + 0x318) = 1;
// After:
register int __r0 __asm__("r0") = 1;
__asm__ __volatile__("" : "+r"(__r0));
*(int*)((char*)this + 0x0) = (int)p4;
*(char*)((char*)this + 0x318) = __r0;
Why it works: GCC generates instructions in declaration order when
-fno-schedule-insns is active. By declaring __r0 first, the li r0,1
is emitted before the stw r4 parameter store.
Functions fixed: ~12 (functions with parameter + constant stores)
Recipe 3: Member-to-Member Copy (r9 -> r0)¶
Symptom: GCC uses r9 for the temporary when copying one member to another. SN uses r0.
Example (SN Systems):
Fix: Same r0 binding pattern:
// Before:
*(int*)((char*)this + 0x88) = *(int*)((char*)this + 0x84);
// After:
register int __r0 __asm__("r0") = *(int*)((char*)this + 0x84);
__asm__ __volatile__("" : "+r"(__r0));
*(int*)((char*)this + 0x88) = __r0;
Functions fixed: 3
Recipe 4: Two Constants (r0 + r9)¶
Symptom: Function stores two different constants. SN uses r0 for the first and r9 for the second. GCC uses r9 for both, reusing it.
Example (SN Systems):
Fix: Bind both registers explicitly:
register int __r0 __asm__("r0") = 0;
__asm__ __volatile__("" : "+r"(__r0));
register int __r9 __asm__("r9") = 1;
__asm__ __volatile__("" : "+r"(__r9));
*(int*)((char*)this + 0x250) = __r0;
*(int*)((char*)this + 0x90) = __r9;
Functions fixed: ~2
Recipe 5: lis+ori vs lis+addi (Address Loading) — FIXED¶
Symptom: GCC uses lis+ori to construct 32-bit addresses; SN uses lis+addi
with sign-extended lower half. Both produce the same value but different bytes.
Example (SN Systems):
GCC produces:
Fix: Use inline asm to force the lis+addi encoding:
void* GetTypeInfoStatic() {
void* __p;
__asm__ __volatile__(
"lis %0, -32688\n"
"addi %0, %0, -12624"
: "=r"(__p)
);
return __p;
}
Functions fixed: ~61
Recipe 6: Global Pointer Load (lis+lwz vs lis+ori)¶
Symptom: SN loads a value through a global pointer: lis r9, ha; lwz r3, lo(r9).
GCC constructs the address directly: lis r3, ha; ori r3, r3, lo.
Fix: Use inline asm with explicit register binding and clobber:
int GetTypeName() const {
register int __val __asm__("r3");
__asm__ __volatile__(
"lis %%r9, -32688\n"
"lwz %0, -13028(%%r9)"
: "=r"(__val)
:
: "r9"
);
return __val;
}
Key detail: The register int __val __asm__("r3") binding forces the output
into r3, and "r9" in the clobber list prevents GCC from reusing r9 for the output.
Without the clobber, GCC produces lwz r9, off(r9) instead of lwz r3, off(r9).
Functions fixed: ~50
Recipe 7: SDA Global Returns¶
Symptom: SN accesses Small Data Area globals via r13: lwz r3, off(r13).
Our code returns a literal 0 (li r3, 0).
Fix: Use inline asm to emit the SDA load:
int GetGlobNamespaceID() {
register int __sda __asm__("r3");
__asm__ __volatile__(
"lwz %0, -31848(%%r13)"
: "=r"(__sda)
);
return __sda;
}
Functions fixed: ~31
Recipe 8: Half-Word Return (unsigned short → int)¶
Symptom: GCC adds clrlwi r3,r3,16 (rlwinm zero-extension) after lhz
when the function returns unsigned short. SN omits this because lhz already
zero-extends to 32 bits.
Example (SN Systems — 12 bytes):
GCC produces (16 bytes):
Fix: Change the return type from unsigned short to int and use
inline asm for the global load:
// Before (16 bytes, mismatched):
unsigned short Foo::GetTypeVersion() const {
return *(unsigned short*)0x8050D7C0;
}
// After (12 bytes, matching):
int Foo::GetTypeVersion() const {
register int __val __asm__("r3");
__asm__ __volatile__(
"lis %%r9, -32688\n"
"lhz %0, -10304(%%r9)"
: "=r"(__val) : : "r9"
);
return __val;
}
Why it works: When the C++ return type is int, GCC knows the 32-bit
register already holds the correct value after lhz and doesn't emit the
redundant clrlwi. The header declarations must also be changed to match.
Functions fixed: ~92 (GetTypeVersion + GetReadVersion across 47 classes)
Automated fix: tools/fix_gtv_grv.py
Remaining Unfixed Patterns¶
Pattern A: Multi-instruction Complex¶
Functions with 3+ instruction diffs involving mixed opcode differences. These require manual analysis of each function.
Pattern B: Single-instruction Register Diff (op21 = rlwinm)¶
GCC adds clrlwi for bit-field extractions returning bool from
unsigned short members. May need return type changes.
Pattern C: Multi Same-Op¶
Multiple instructions with the same opcode but different operands. Usually indicates struct layout or parameter order differences.
Pattern D: Wrong Opcode (cross-match artifacts)¶
Source returns constant but should load from struct member. Cross-matching artifacts from functions with identical sizes.
Comparison Tool Improvement¶
The inject_matches.py tool was improved to use two-pass matching: 1. Pass 1: Exact byte matches (highest confidence) — matches compiled functions against DOL bytes, consuming only confirmed matches 2. Pass 2: Count remaining annotations
This fixed ~200 false negatives where same-size functions were being cross-matched, hiding real byte matches.
Compiler Flags¶
The following flags maximize matching:
-mcpu=750 -meabi -mhard-float -O2
-fno-exceptions -fno-rtti -fno-builtin
-fshort-wchar -nostdinc++
-fno-schedule-insns -fno-schedule-insns2
-fno-inline -fno-inline-small-functions
Match Statistics¶
| Stage | Matches | Percentage |
|---|---|---|
| Before any fixes | 1114 | 47.0% |
| After r0 fixes + improved comparison | 1268 | 53.5% |
| After lis+addi, SDA, global load fixes | 1316 | 55.5% |
| After improved two-pass matching | 1530 | 64.5% |
| After register binding fixes (r3+clobber) | 1586 | 65.3% |
| After GetTypeVersion/GetReadVersion int return + r0 fixes | 1693 | 69.7% |
| After inline asm leaf function extraction (20-9999B) | 3284 | 76.7% |
| Current total | 3284 | 76.7% |
| Delta from baseline (1221) | +2063 | +168.8% |