← cd ../

~/labs/workshops/gba-firered/02-arm-e-thumb

Reading ARM and ThumbRegisters, instructions, and how to read the CPU's machine code.

The GBA CPU executes instructions. Each instruction is a piece of machine code, that is, a few bytes that tell the CPU to do one small thing: copy a value, add, compare, jump somewhere else. Reverse engineering is, at its core, reading those instructions and understanding the story they tell together.

This chapter teaches you how to read. Do not memorize everything. Come back here as a reference whenever a new instruction appears.

Registers

The CPU does not work directly in memory all the time. It has a set of fast slots called registers. In the GBA’s normal mode you see 16 of them:

r0  to r7    low registers, general-purpose
r8  to r12   high registers, general-purpose
r13 (sp)     stack pointer, points to the top of the stack
r14 (lr)     link register, stores a function return address
r15 (pc)     program counter, points to the current instruction

There is also a status register called cpsr. It stores comparison flags and the CPU mode. Two parts matter for us:

flags    N, Z, C, V. Set by comparisons and additions. They decide branches.
bit T    says whether the CPU is executing ARM (T=0) or Thumb (T=1).

Remember the aliases. When you see sp, lr, or pc in the disassembly, they are just r13, r14, and r15.

ARM and Thumb are the same processor

The ARM7TDMI executes two instruction formats:

ARM    each instruction takes 4 bytes (32 bits). More powerful per instruction.
Thumb  each instruction takes 2 bytes (16 bits). More compact, uses less ROM.

It is the same processor and the same registers. What changes is the size and encoding format. The GBA uses Thumb for most of the game because ROM space is expensive and Thumb takes half the space. Startup code is usually ARM.

The CPU knows which mode it is in through the T bit in cpsr. This creates the most important rule and the most common beginner error:

If you disassemble Thumb as ARM, or the opposite, you get garbage.

When Ghidra shows many invalid instructions or nonsensical flow, the issue is almost always this: the wrong mode at that address. Chapter 04 shows how to mark the right mode in Ghidra.

Little endian in practice

The GBA is little endian. That means a number stored in memory appears with its least significant byte first. You already saw the first ROM bytes:

offset 0x00000000:  7f 00 00 ea

To build the 32-bit ARM instruction, read the four bytes backward:

bytes in memory:    7f 00 00 ea
32-bit value:       0xea00007f

That value, 0xea00007f, is a branch instruction. The top 0xea is the opcode for an unconditional ARM branch, and the rest is the jump distance. The result is:

08000000:  b  0x08000204

Which is exactly what the disassembler confirms:

python3 tools/disasm_gba.py 0x08000000 --size 0x10
08000000: b        #0x8000204

You do not need to decode branches by hand during class. The point is to understand why the bytes look shuffled: little endian.

Instruction anatomy

Almost every instruction follows the same shape:

mnemonic   destination,   source(s)

The mnemonic is the short operation name. The destination usually comes first, then the sources. Examples:

mov  r0, #0x12      puts the number 0x12 in r0
add  r2, r0, r4     r2 = r0 + r4
ldr  r1, [r7]       loads into r1 the value at the address stored in r7

The # marks a literal number, called an immediate. Brackets [ ] mean “the value at this memory address,” not the address itself. That difference is central to ARM and is explained below.

ARM is a load/store machine

ARM does not do arithmetic directly in memory. To work with a value in RAM, it needs three steps:

1. load     bring the value from memory into a register (ldr, ldrh, ldrb).
2. operate  do the calculation in registers (add, sub, mov, cmp).
3. store    write the result back to memory (str, strh, strb).

The data size appears in the suffix of the memory instruction:

ldr / str    32-bit word (4 bytes)
ldrh / strh  16-bit halfword (2 bytes), h means halfword
ldrb / strb  one 8-bit byte

This detail is not decoration. When you see strh, you already know the written value is 16 bits. That is how, in RAM.md, we inferred that Pokemon HP is a u16: the instruction that writes HP is a strh.

A real ARM block, line by line

This is the real beginning of FireRed code, the start_vector. It is ARM. It was dumped directly from your ROM:

python3 tools/disasm_gba.py 0x08000204 --size 0x34
08000204: mov  r0, #0x12          puts 0x12 in r0
08000208: msr  cpsr_fc, r0        writes r0 into CPU status (changes mode)
0800020c: ldr  sp, [pc, #0x28]    loads stack pointer from a nearby constant
08000210: mov  r0, #0x1f          puts 0x1f in r0
08000214: msr  cpsr_fc, r0        changes CPU mode again
08000218: ldr  sp, [pc, #0x18]    configures another stack pointer
0800021c: ldr  r1, [pc, #0x1c]    loads an address into r1
08000220: add  r0, pc, #0x20      calculates a pc-relative address
08000224: str  r0, [r1]           writes r0 to the memory pointed to by r1
08000228: ldr  r1, [pc, #0x14]    loads the address of AgbMain into r1
0800022c: mov  lr, pc             stores the return address in lr
08000230: bx   r1                 jumps to the address in r1 (enters AgbMain)
08000234: b    0x08000204         if it returns, repeats everything

You do not need to understand every CPU mode. The story out loud is short:

sets up the stack, prepares interrupts, and jumps to the game's main.

Notice ldr sp, [pc, #0x28]. ARM does not have enough space inside the instruction to store a full 32-bit address. So it stores that address in a constant near the code and loads it with a pc-relative ldr. You will see this pattern all the time. In Ghidra it appears with the resolved value next to it, which helps a lot.

Thumb, the game’s main mode

Now the same kind of reading, but in Thumb, where most of FireRed lives. This is the start of AgbMain, the game’s real main:

python3 tools/disasm_gba.py 0x080003A4 --size 0x20 --thumb
080003a4: push {r4, r5, r6, r7, lr}   saves registers on the stack (function entry)
080003a6: mov  r7, r8                  moves r8 into r7
080003a8: push {r7}                    saves r7 on the stack too
080003aa: movs r0, #0xff               puts 0xff in r0
080003ac: bl   #0x81e3b80              calls a function (branch with link)
080003b0: movs r1, #0xa0               puts 0xa0 in r1
080003b2: lsls r1, r1, #0x13           shifts r1 left by 0x13 bits
080003b4: ldr  r2, [pc, #0xa0]         loads a constant into r2
080003b6: adds r0, r2, #0              r0 = r2 + 0
080003b8: strh r0, [r1]                writes 16 bits of r0 to the address in r1

Two things to notice.

First: push {r4, r5, r6, r7, lr} at function entry is the classic prologue. The function saves the registers it will use and the return address (lr) so it can restore everything at the end. When you see this, you are almost always at the start of a function.

Second: in Thumb, many arithmetic instructions end in s, such as movs, adds, subs, lsls. That s means “update the flags.” In Thumb this is often automatic. Keep it in mind for now, because flags are what decide the conditional branches that come next.

Branches and conditions

Branches are what give code its shape. The most common ones:

b    unconditional jump, always branches.
bl   function call, storing the return address in lr (branch with link).
bx   jumps to the address stored in a register.
ble, bgt, beq, bne, bpl, ...   conditional branches, read the cpsr flags.

Conditionals almost always come right after a comparison or an operation that changed the flags. The pair cmp followed by a conditional b is a real if:

cmp r0, r1
ble target      if r0 <= r1, go to target. otherwise, continue.

The Thumb bit in addresses

One last detail that confuses people and appears in every patch. When code jumps to a Thumb function using bx, the address has bit 0 set, meaning it adds 1.

The function starts at   0x080003A4
The address used is      0x080003A5

This +1 does not change where the function starts. It is a signal to the CPU: “enter Thumb mode.” The real address is still the even one. That is why, in the chapter 06 patch, when we jump back to the original code with bx, the address ends in 1. It is not a mistake; it is how we keep the CPU in Thumb mode.

Pocket table

Keep this near you during class:

mov   r0, #imm     r0 = number
movs  r0, #imm     same, and updates flags
add   rd, ra, rb   rd = ra + rb
sub   rd, ra, rb   rd = ra - rb
mul / muls         multiplication
lsl / lsls         shifts bits left

ldr   rd, [rn]         rd = 32-bit memory at rn
ldrh  rd, [rn, #off]   rd = 16-bit memory at rn+off
ldrb  rd, [rn]         rd = 1 byte at rn
str / strh / strb      same idea, but writes to memory

cmp   ra, rb       compares, only sets flags
b     target       always branches
bl    target       calls a function, return in lr
bx    rn           jumps to the address in rn
ble/bgt/beq/bne    branches if the flag condition matches

push {list}        saves registers on the stack (function entry)
pop  {list}        restores registers (function exit)

What to take from this chapter

- 16 registers. sp, lr, and pc are r13, r14, and r15.
- ARM has 4 bytes per instruction, Thumb has 2. The wrong mode produces garbage.
- Little endian: instruction bytes appear reversed.
- ARM loads from memory, operates on registers, and writes back.
- The h/b suffix tells you the data size. strh = 16 bits.
- cmp sets flags, the conditional b decides the branch. That is an if.
- A Thumb function address used in bx ends in 1.