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: 19The 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 _startProblem 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:
- Section 0 is a null entry in the section table which is ELF convention. Index 0 means “no section”, but sometimes it’s used to hold extra metadata for unusually large programs. In our case its size is 0, so it’s not used.
.textcontains executable instructions, while.rodataholds constants such as string literals..eh_frameholds stack-unwinding information, and.eh_frame_hdris an index for finding it. I can walk the stack using ther1back-chain instead for basic stack traces (as discussed in my dissertation)..data.rel.rocontains data that can be made read-only after relocations are applied..relro_paddingseparates that region from writable data at the required alignment..datacontains initialized writable variables..bssreserves space for zero-initialized variables without storing all those zeroes in the file.- The
.debug_*sections contain DWARF debugging information. .commentcontains toolchain metadata..symtablists symbol addresses and.strtabstores symbol names as strings.shstrtabstores the section names shown above.
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 _startWe 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 64The .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 = .;
} > MEM1The . 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.