GBA start routine and global variables initialization

This documentation contains both hardware and software knowledge.

GBA start routine

BIOS (power on)

GBA boot

When I turn my GBA, the first instruction executed is at 0. That’s because GBA uses ARM CPU (ARM7TDMI) and pc/r15 is 0 when power on. According to GBA Memory Map, 0 is in BIOS (System ROM). BIOS has protection, so it is a black box. However, Nintendo doesn’t protect it well, so it can be dumped and reversed. In addition, it can even be extracted from a 3DS VC title directly (agb.bin). I ripped one from “Pokemon Gold Version.cia” and checked its CRC32. It is the same as the official original GBA BIOS: 81977335. Now you know the reason why we can read code in it.

BIOS code

0 is _start. it jumps to reset_vector, and runs to _000000B4 eventually. 0x4000000 - 6 = 0x3fffffa = 0x3007ffa (Memory Mirror, useless pins). It checks the value at 0x3007ffa to decide wether to jump to ROM (0x8000000) for a cart game or jump to EWRAM (0x2000000) for a multiboot game.

crt0 (bootloader)

0x8000000 is _start. The instruction there is fixed: jump to start_vector (0x8000000C0). The data between them is cart header. Code at start_vector is not fixed. You can write your own bootloader and it works. Why not jump to game main function directly? Because main function cannot run now. It is written in C so it follows C convention and requires c runtime environment. For example, C function needs a stack but sp/r13 is not set yet. The entry point is runtime library called crt0 here. It is written in assembly because C code cannot run now.

Official crt0.s (Start Up Routine provided by Nintendo):

@-                      Reset                                       -
    .EXTERN     AgbMain
    .GLOBAL     start_vector
    .CODE 32
        mov     r0, #PSR_IRQ_MODE       @ Switch to IRQ Mode
        msr     cpsr, r0
        ldr     sp, sp_irq              @ Set SP_irq
        mov     r0, #PSR_SYS_MODE       @ Switch to System Mode
        msr     cpsr, r0
        ldr     sp, sp_usr              @ Set SP_usr
        ldr     r1, =INTR_VECTOR_BUF    @ Set Interrupt Address
        adr     r0, intr_main
        str     r0, [r1]
        ldr     r1, =AgbMain            @ Start & Switch to 16bit Code
        mov     lr, pc
        bx      r1
        b       start_vector            @ Reset
sp_usr: .word   WRAM_END - 0x100
sp_irq: .word   WRAM_END - 0x60

It sets stack, interrupt and jumps to AgbMain. However, it doesn’t initialize global variables in WRAM.

AgbMain (main function)

This is the main logic of the game. A main function in C doesn’t have to be named as main. For example, it is named as WinMain in Windows Applications. That totally depends on the crt0. It jumps to AgbMain at last, so it is named as AgbMain for GBA applications. AgbMain is written by the game developer, so it has no fixed pattern. It is different from game to game.

AgbMain in Fire Emblem 8

// clear RAM
DmaFill32(3, 0, (void *)IWRAM_START, 0x7F80); // reset the area for the IWRAM ARM section.
CpuFastFill(0, (void *)EWRAM_START, 0x40000);   

The first thing is does is to clear IWRAM & EWRAM. The data on a RAM chip is random when power on. It doesn’t have to be full of 0. If you doesn’t clear the RAM, the global variables have random initial values. Because the code here fills RAM with zero, all global variables will have a zero initial value.

AgbMain in Pokemon Ruby


The first thing it does is to trigger a RegisterRamReset. It also clears EWRAM & IWRAM. Therefore, all global variables are initialized to 0.

AgbMain in Demo Dolphin

void AgbMain(void)
	GameInit();		// initialize game
//			Game Initialization                         //
void GameInit()
    DmaClear(3, 0,   EX_WRAM,  EX_WRAM_SIZE,         32);  // CPU External Work RAM clear
    DmaClear(3, 0,   CPU_WRAM, CPU_WRAM_SIZE - 0x200,32);  // CPU Internal Work RAM clear
    DmaClear(3, 0,   VRAM,     VRAM_SIZE,            32);  //     VRAM clear
    DmaClear(3, 160, OAM,      OAM_SIZE,             32);  //      OAM clear
    DmaClear(3, 0,   PLTT,     PLTT_SIZE,            32);  // palette clear

It has a special function to clear WRAM.

AgbMain in Demo Yoshi

void AgbMain(void)
	s16 i;
//AGBPrint("TEST \n");
	//The First Initialization
    DmaClear(3, 0,   EX_WRAM,  EX_WRAM_SIZE,         32);  // Clear CPU external Work RAM
    DmaClear(3, 0,   CPU_WRAM, CPU_WRAM_SIZE - 0x200,32);  // Clear CPU internal Work RAM
//  DmaClear(3, 0,   VRAM,     VRAM_SIZE,            32);  // Clear VRAM
//  DmaClear(3, 160, OAM,      OAM_SIZE,             32);  // Clear OAM
//  DmaClear(3, 0,   PLTT,     PLTT_SIZE,            32);  // Clear Palette

It also clears WRAM first.

SoftReset (L+R+A+B/A+B+Select+Start)

When you press the key combo, the game restarts. Why?

Read the code

// perform the game loop.
    while (1)

It is the main loop in AgbMain.

Read the code

void SoftResetIfKeyComboPressed(void)
    if ((u8)sub_8000D18() != 0)
        if (gKeyStatusPtr->heldKeys == (L_BUTTON | R_BUTTON | A_BUTTON | B_BUTTON))
        else if (gKeyStatusPtr->heldKeys == (A_BUTTON | B_BUTTON | SELECT_BUTTON | START_BUTTON))

It detects the key combo and trigger a SoftReset.

Read the code

SoftReset: @ 0x080D16B0
	ldr r3, =REG_IME
	movs r2, #0
	strb r2, [r3]
	ldr r1, =0x03007F00
	mov sp, r1
	swi #1
	swi #0

It is a library function provided by Nintendo. It triggers a RegisterRamReset(swi 1) and SoftReset(swi 0).

Read the code
or just read the doc.
SoftReset is a noreturn function. It doesn’t returns to its caller. It returns to _start (0x8000000 for a cart game) instead.

Therefore, the game restarts from crt0 and all global variables are initialized to 0 again.

global variables initialization

Read professional books if you want to understand its principle.

In order not to mislead readers, I read it before writing this part to ensure that I have understood it well myself.

In this part, I will give you the conclusion from experiment rather than teach theories in the book.


int a; // uninitialized global variable
int b = 0; // global variable with a zero initial value
int c = 1; // global variable with a non-zero initial value
int __attribute__((weak)) d = 2; // weak symbol
const int e = 3; // constraint variable
extern int f; // extern symbol
int sum()
	int g = 4; // local variable
	static int h = 5; // static local variable
	return a + b + c + d + e + f + g + h;

Symbol Type Explanation

g is invisible. It is in stack frame when the function is running.

The reason why they went to different sections is that they need to be allocated to different types of memory when thr program is running. Code and constants are read-only, so they are allocated to ROM. Variables are read & write, so they are allocated to RAM.

Linker script tells linker how to allocate address to variables. It maps sections to actual memory space. The variables are in WRAM, but their non-zero initial values are stored in the ROM. According to the analysis in the previous part, GBA start routine doesn’t carry data from ROM to WRAM. Therefore, global variables are uninitialized.

In fact, official games like Fire Emblem and Pokemon don’t use global variables with a non-zero initial value. That’s the reason.

To conclude, don’t define a global variable assigned a non-zero initial value when programming.

At least remember that even if you don’t understand this documentation.

However, we can add global variable initialization code into start routine to break that rule. Either crt0 or AgbMain is okay.

The initialization routine needs the address of initial value and variable to copy data from ROM to WRAM. You cann’t get the address before linking. Which came first, chicken or egg? The answer is linker script defined symbol. Leave a hole there and linker will fill it.

Example : devkitPro\devkitARM\arm-none-eabi\lib\gba_crt0.s

@ Clear BSS section to 0x00
	ldr	r0, =__bss_start__
	ldr	r1, =__bss_end__
	sub	r1, r0
	bl	ClearMem
@ Clear SBSS section to 0x00
	ldr	r0, =__sbss_start__
	ldr	r1, =__sbss_end__
	sub	r1, r0
	bl	ClearMem
@ Copy initialized data (data section) from LMA to VMA (ROM to RAM)
	ldr	r1, =__data_lma
	ldr	r2, =__data_start__
	ldr	r4, =__data_end__
	bl	CopyMemChk

Then you can initialize global variables to non-zero values.

Official crt0 provided by Nintendo doesn’t include that. Homebrew crt0 provided by devkitpro includes that.