~/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.