Wii Development From Scratch: Episode 3

We’re not ready to hello world yet

Turns out there’s still a lot more setup required before we can start writing actual code.

Let’s read the headers off the ELF which gets compiled from what we have so far:

[sam@rustbox:~/src/wraith]$ readelf zig-out/bin/wraith.elf -h
ELF Header:
  Magic:   7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, big endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           PowerPC
  Version:                           0x1
  Entry point address:               0x45aa0
  Start of program headers:          52 (bytes into file)
  Start of section headers:          1408228 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         8
  Size of section headers:           40 (bytes)
  Number of section headers:         21
  Section header string table index: 19

The entry point address is the address of _start in main.zig which I can confirm by searching the symbols in the ELF:

[sam@rustbox:~/src/wraith]$ readelf -s zig-out/bin/wraith.elf | rg _start
   324: 00045aa0     8 FUNC    LOCAL  DEFAULT    4 main._start
  1385: 00045aa0     8 FUNC    GLOBAL DEFAULT    4 _start

Problem is that the Wii has two distinct memory banks, 24MB MEM1 and 64MB MEM2. Each bank has cached and uncached virtual address mappings to the same physical memory. MEM1 sits within the SoC, and is slightly faster, so we should place our code there. According to the WiiBrew Wiki, the cached MEM1 range is 0x80000000 to 0x817FFFFF which maps physically on 0x00000000 to 0x017FFFFF

How come the program ran?

I’ve not bothered testing this on real hardware, and I’ve also not checked Dolphin’s source code, but this could be a quirk of Dolphin’s ELF loader allowing physical addresses by OR-ing 0x80000000 with 0x45aa0.

Compiles, but links to the wrong addresses

Although Zig can compile for the Wii’s powerpc architecture, the linker misses some of the Wii’s specific requirements. In my dissertation, I used devkitpro’s linker which resulted in an uglier build.zig file. Unfortunately, Zig doesn’t support everything we need through its build API. It has stuff for entrypoint and base address, but we want fine control over every section of the ELF.

Understanding the ELF file

Let’s look at what sections we already have:

[sam@rustbox:~/src/wraith]$ readelf -S ./zig-out/bin/wraith.elf
There are 21 section headers, starting at offset 0x157ce4:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .rodata           PROGBITS        00010138 000138 003d70 00 AMS  0   0  8
  [ 2] .eh_frame_hdr     PROGBITS        00013ea8 003ea8 001434 00   A  0   0  4
  [ 3] .eh_frame         PROGBITS        000152dc 0052dc 00503c 00   A  0   0  4
  [ 4] .text             PROGBITS        0002a318 00a318 055614 00  AX  0   0  4
  [ 5] .data.rel.ro      PROGBITS        0008f930 05f930 0035e0 00  WA  0   0  8
  [ 6] .relro_padding    NOBITS          00092f10 062f10 0000f0 00  WA  0   0  1
  [ 7] .data             PROGBITS        000a2f10 062f10 000004 00  WA  0   0  4
  [ 8] .bss              NOBITS          000a2f40 062f14 001000 00  WA  0   0 64
  [ 9] .debug_loc        PROGBITS        00000000 062f14 06bfd6 00      0   0  1
  [10] .debug_abbrev     PROGBITS        00000000 0ceeea 00082e 00      0   0  1
  [11] .debug_info       PROGBITS        00000000 0cf718 037631 00      0   0  1
  [12] .debug_ranges     PROGBITS        00000000 106d49 0095f8 00      0   0  1
  [13] .debug_str        PROGBITS        00000000 110341 00f991 01  MS  0   0  1
  [14] .debug_pubnames   PROGBITS        00000000 11fcd2 00514a 00      0   0  1
  [15] .debug_pubtypes   PROGBITS        00000000 124e1c 00219b 00      0   0  1
  [16] .debug_line       PROGBITS        00000000 126fb7 02433c 00      0   0  1
  [17] .comment          PROGBITS        00000000 14b2f3 000013 01  MS  0   0  1
  [18] .symtab           SYMTAB          00000000 14b308 0056a0 10     20 1385  4
  [19] .shstrtab         STRTAB          00000000 1509a8 0000db 00      0   0  1
  [20] .strtab           STRTAB          00000000 150a83 00725e 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), v (VLE), p (processor specific)

The sections marked with an A flag are allocated in memory when the program runs. The others are mostly there for debugging, and won’t be loaded/executed by the program. Here’s what each one is for:

Linker Script

Good news is Zig supports GNU linker scripts out of the box.

The script is a .ld file. This has the final say in how the ELF file should turn out. We can start by explicitly specifying _start as the entrypoint:

ENTRY(_start)

Move Allocations to MEM1

We can declare the region that maps to MEM1, the memory bank we want to use.

MEMORY
{
  MEM1 (rwx) : ORIGIN = 0x80000000, LENGTH = 0x017fc000
}

And now the fun part. We get to declare our own ELF sections. We’ll start with .text which is where the code is:

SECTIONS
{
  .text ALIGN(32) :
  {
    *(.text)
    *(.text.*)
  } > MEM1
}

This takes Zig’s output .text and wraps it into a 32-byte aligned section (also named .text) stored in MEM1. The other .text.* is to capture any potential sections that are prefixed with .text. I’m aligning the sections to 32 bytes because the Wii’s CPU cache lines are 32 bytes, but it’s not necessary.

Setup in Zig

We can tell Zig to use the linker script. I’ve added another function to config/platform/wii/build.zig:

pub fn link(b: *std.Build, exe: *std.Build.Step.Compile) void {
    exe.setLinkerScript(b.path("config/platform/wii/link.ld"));
}

Now when I build we see that _start is still the entrypoint but now it lives within MEM1!

[sam@rustbucket:~/src/wraith]$ readelf ./zig-out/bin/wraith.elf -h | rg "Entry point"                                          0ms
  Entry point address:               0x8001b788
[sam@rustbucket:~/src/wraith]$ readelf ./zig-out/bin/wraith.elf -s | rg _start                                                 3ms
   324: 8001b788     8 FUNC    LOCAL  DEFAULT    1 main._start
  1385: 8001b788     8 FUNC    GLOBAL DEFAULT    1 _start

We can do the same with the other allocated sections. Here’s the rest of SECTIONS after .text.

  .rodata ALIGN(32) :
  {
    *(.rodata)
    *(.rodata.*)
  } > MEM1

  .eh_frame ALIGN(32) :
  {
    *(.eh_frame)
    *(.eh_frame.*)

  } > MEM1

  .eh_frame_hdr ALIGN(32) :
  {
    *(.eh_frame_hdr)
    *(.eh_frame_hdr.*)
  } > MEM1

  .data ALIGN(32) :
  {
    *(.data)
    *(.data.*)
  } > MEM1

  .bss ALIGN(64) (NOLOAD) :
  {
    __bss_start = .;

    *(.bss)
    *(.bss.*)

    *(.sbss)
    *(.sbss.*)

    *(COMMON)

    __bss_end = .;
  } > MEM1
}

Each *(.name) collects input sections with that exact name, while *(.name.*) also picks up variants with a suffix. As with .text, > MEM1 explicitly places the resulting sections in MEM1.

The .rodata rule collects read-only data. The .data rule collects writable data, including the old .data.rel.ro because it matches .data.*. This layout doesn’t make that data read-only after relocation, so the original .relro_padding isn’t carried over.

The .bss rule collects zero-initialized data from .bss, .sbss, and any COMMON symbols. NOLOAD reserves space in memory without putting zeroes in the ELF file. The __bss_start and __bss_end symbols mark the region that startup code will need to clear before using those variables. It’s 64-byte aligned because the section requires it in the Al column:

[sam@rustbucket:~/src/wraith]$ readelf -S ./zig-out/bin/wraith.elf | rg ".bss|Name"
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 6] .bss              NOBITS          80062e40 072e08 001000 00  WA  0   0 64

The .eh_frame and .eh_frame_hdr rules keep the read-only unwinding information in its own sections rather than folding it into .rodata. Both are explicitly placed in MEM1.

Allocate a Stack

Functions need a stack for call frames and local data, so we should reserve some of MEM1 by adding another section:

.stack ALIGN(32) (NOLOAD) :
{
    __stack_bottom = .;
    . += 64K;
    __stack_top = .;
} > MEM1

The . is where the linker’s current address is. The order of the sections in the script is also the order they appear in memory, so if I place the .stack rule at the end, the . will be the address after all the previous sections + alignment. I’ve gone ahead and bumped the current address by 64K which means that we’ve reserved 64K for the stack. And the . is also used to get the values for __stack_bottom and __stack_top.

It’s NOLOAD, so the ELF file won’t contain 64K; like the .bss section. So it’s up to the startup code to setup the stack pointer correctly. We’ll cover startup code in the next chapter.