Code injection

Adding a few instructions is easy. Adding a routine is not and the reason has nothing to do with the DOL format.

Why you cannot just append a section

A Wii game does not own the whole of MEM1. Its static data ends somewhere and everything above that point is the heap, which the game calls the arena. The lower bound of the arena is a value named arenaLo.

If you declare a new section above the game’s data, you are declaring it inside the heap. The game will allocate over your code within the first seconds of running and your code will be replaced by a bunch of zeroes (or maybe a crash, sometimes)

Warning

bss_start + bss_size is not arenaLo. Across every game measured, arenaLo sits roughly 0x10000 higher. Computing an injection address from bss gives you an address that is free at load time and allocated shortly after.

The fix is to move arenaLo up, so the heap starts above your code instead of on top of it. The game loses that much memory and everything else keeps working.

You are probably wondering: “Why can’t I put my code between the end of bss and arenaLo ?”. And the question is completely correct. There is a bunch of static data, maybe written by the compiler for fast access. In Super Mario Galaxy, Mario Object pointer is written in 0x806B7B40, between bss_end and start of arenaLo.

Finding arenaLo

arenaLo is not stored in a table. It is a constant compiled into the boot code, loaded into r3 and passed to the function that initialises the heap. The sequence is remarkably stable across games:

lis    r3, HI      ; 3c 60 ?? ??   <- the instruction to patch
addi   r3, r3, LO  ; 38 63 ?? ??
addi   r0, r3, 31  ; 38 03 ?? ??
rlwinm r3, r0, n   ; 54 03 ?? ??

The last two instructions round the value up to a 32-byte boundary, which is what makes the pattern recognisable: a bare lis/addi pair is everywhere, but one immediately followed by an align-to-32 is not.

site = dol.find_arena_lo_setter()
print(f"setter at {site:#010x}, arenaLo = {dol.read_arena_lo(site):#010x}")

The scan walks every text section four bytes at a time and returns the address of the lis. If no match is found it raises DolError.

Known values

If the scan fails, or if you want to check its result, these were measured by hand:

Game

ID

arenaLo

arenaLo minus bss_end

Setter address

Skyward Sword

SOUE01

0x806882C0

+0x10000

0x803A2AF0

Super Mario Galaxy

RMGE01

0x806BDFA0

+0x10010

0x804A16BC

Mario Strikers

RSBE01

0x806F6D20

+0x1001C

0x803B1F74

Mario Kart Wii

RMCE01

0x80394E00

+0x10004

0x8019FD28

Wii Sports (Rev1)

RSPE01

0x804D6A20

+0x10004

0x800EC978

Wii Sports (Rev0)

RSPE01

0x804F6D80

+0x1000C

0x800C65B4

Note

If you find a game where this pattern matches something that is not the arenaLo or arenaHi setter, please open an issue. The pattern has held on every title tested so far.

Injecting

inject_above_arena() does the whole dance: it finds the setter, reads the current arenaLo, places your sections above it and rewrites the setter to point past them.

from wiithon import DOL, WiiIsoPatcher
from wiithon.ppc import instructions as ppc

def patch(dol: DOL) -> None:
    routine = ppc.li(3, 42) + ppc.blr()

    shift, addresses = dol.inject_above_arena([routine])

    print(f"arena moved up by {shift:#x} bytes")
    print(f"routine lives at {addresses[0]:#010x}")

with WiiIsoPatcher("game.iso") as patcher:
    patcher.patch_dol(patch)
    patcher.build("patched.iso")

Each section is placed on a 32-byte boundary. padding_before leaves a gap between the old arenaLo and your first section, 0x100 bytes by default, which gives you room to grow without changing every address.

Tip

inject_above_arena returns 2 arguments. The first one is the shift amount in the RAM. If something was in 0x80481516 in the RAM and the shift is 0x200, so the value will be at: 0x80481716. You can reserve a fix region to always have the same shift. See below.

Reserving a fixed region

By default arenaLo is moved to just past your code, so every time your code changes size, every address moves. That makes iteration painful and it invalidates any address you hardcoded elsewhere.

reserved_size fixes the layout:

shift, addresses = dol.inject_above_arena([routine], reserved_size=0x4000)

arenaLo now always lands at base + 0x4000, whatever your code weighs. Grow your routine and the addresses stay put. Exceed the reservation and you get a ValueError telling you how much you used, instead of a silent corruption.

Tip

Reserve from the first day. Recompiling and discovering every address shifted by 0x20 is the most common source of wasted time in this workflow.

When the scan fails

Pass the setter address yourself:

shift, addresses = dol.inject_above_arena([routine], manual_arena=0x804A16BC)

Limits

Section slots. A DOL holds 7 text and 11 data sections. inject_above_arena takes a free text slot if there is one, falls back to a data slot and raises DolNoFreeSectionError when both are exhausted. Most retail games use 2 text and 8 to 9 data sections, so you have room, but not unlimited room. Concatenate your code into fewer sections rather than adding one per function.

Memory. Whatever you take from the arena, the game no longer has. A few kilobytes is invisible. A few megabytes will make a game that allocates near its limit fail in ways that look unrelated to your patch.

See also

PowerPC for patching for writing the code you are about to inject and Working with the DOL for code caves, which are the better option below a hundred bytes or so.