~/labs/workshops/gba-firered/04-analise-estatica
Static analysis in GhidraFollow the boot path from the only known address and infer the structure until you find the main loop.
Static analysis means studying the code without running the game. You read the instructions, follow jumps, and infer what each block does.
Before anything else, one warning applies to the whole chapter. We are not going to open the ROM and start naming things at random. That would be cheating. At the start we know exactly one address: 0x08000000, where the console begins executing. Everything else we discover by following code from there. Every name in this chapter appears after the analysis that justifies it, never before. The final, clean names are confirmed at the end by crossing our work with a public source, using exactly the same discipline chapter 06 uses.
The root README.md has the full boot map, with all addresses already resolved.
Use it as an answer key to check your work, not as a shortcut to skip deduction.
Step 1. Import the ROM
In Ghidra, with the project open:
File > Import File...
Choose rom/PokemonFireRed.gba
A GBA ROM does not have an executable format like a PC program. It is a blob of
bytes that the console maps. So Ghidra will call it Raw Binary. This is
expected, not an error.
Before confirming, configure three things. They are the most important settings in the whole chapter:
Language: ARM:LE:32 (ARM, little endian, 32-bit)
Base Address: 0x08000000
Compiler: default
LE means little endian, as chapter 01 explained. The base 0x08000000 is where
the console maps the ROM, so Ghidra must know this for addresses to match the
game.
Confirm. When Ghidra asks whether to analyze now, you can accept automatic analysis. It creates many functions by itself, but not all of them. Several regions will remain raw bytes, without a function. That is normal, and it is exactly the scenario we will work with: Ghidra helps, but we open the boot path by hand.
Step 2. Check that the base is right
Open the memory window:
Window > Memory Map
You should see a block starting at 0x08000000:
ram: 08000000 - 08ffffff
If the block starts at 0x00000000, the base is wrong. Reimport with base 0x08000000. This error breaks every address, so it is worth checking.
Step 3. The only thing we know
We do not know where main is, or the loop, or anything else. We know only one thing: the GBA starts executing at the first byte of the ROM, at 0x08000000. That is our starting point, and the only one.
Go there:
Navigation > Go To...
08000000
If you see loose bytes with ?? next to them, it means Ghidra loaded the bytes but
has not created the instruction there yet. Fix it like this:
1. Click the byte at 0x08000000.
2. If any data was created there, use right click, Clear Code Bytes.
3. Press D, or right click, Disassemble.
Ghidra joins the four bytes 7f 00 00 ea into the instruction:
08000000 b 0x08000204
Stop and read what this really is. b is an unconditional branch, as chapter 02
showed. Before doing anything else, the game jumps forward to 0x08000204. It skips
0x204 bytes immediately. The natural question is: why? What lives between
0x08000000 and 0x08000204 that the game does not want to execute?
The answer comes from looking at the bytes after the jump. See the bytes starting at 0x08000004:
08000004 24 ff ae 51 69 9a a2 21 3d 84 82 0a ...
This is not code. It is the cartridge header. The first 192 bytes of the ROM are
fixed data required by the GBA: the Nintendo logo (those 24 ff ae 51... bytes
are the compressed logo), the game title, product code, and checksum. The BIOS
reads and checks this area during boot. If the CPU tried to execute those bytes as
instructions, it would produce garbage.
So the first instruction has one job: skip the header and land on the first byte
of real code. We can call that entry address Start, because the analysis showed
that it is where execution is born.
Step 4. The real code, in ARM
Follow the jump. Go to the destination:
Navigation > Go To...
08000204
In a partial analysis, it is common for no function to exist here yet. If you get
raw bytes, use Clear Code Bytes if needed and press D. This block is ARM, so each
instruction takes 4 bytes:
08000204 mov r0, #0x12 IRQ mode
08000208 msr cpsr_fc, r0 switches the CPU to that mode
0800020c ldr sp, [pc, #0x28] IRQ mode stack (= 0x03007fa0)
08000210 mov r0, #0x1f System mode
08000214 msr cpsr_fc, r0 switches again
08000218 ldr sp, [pc, #0x18] System mode stack (= 0x03007e40)
0800021c ldr r1, [pc, #0x1c] r1 = 0x03007ffc
08000220 add r0, pc, #0x20 r0 = 0x08000248
08000224 str r0, [r1] writes 0x08000248 to 0x03007ffc
08000228 ldr r1, [pc, #0x14] r1 = 0x080003a5
0800022c mov lr, pc stores the return address
08000230 bx r1 jumps to 0x080003a5
08000234 b 0x08000204 if it returns, starts over
You do not need to memorize CPU modes. But you can read the whole story, and it is revealing. Three blocks:
First, the stacks. The mov plus msr pairs switch CPU mode, and each mode gets
its own stack pointer. The CPU is preparing the ground: each execution mode needs
a stack. Ghidra shows the resolved values next to the code, 0x03007fa0 and
0x03007e40, both in IWRAM.
Second, the interrupt. Look at the pair at 0x08000220 and 0x08000224. It
calculates address 0x08000248 and writes that address to 0x03007ffc. This
0x03007ffc is not just anywhere: it is the fixed slot where the GBA BIOS expects
to find the interrupt handler address. In other words, the game is telling the
BIOS “when there is an interrupt, jump to 0x08000248.” We have just discovered,
by deduction, that an interrupt handler exists at 0x08000248. We have not analyzed
it yet, but we already know what it is and where it is because we saw its address
being installed. We will call it intr_main.
Third, the jump into the game. At 0x08000228 the code loads 0x080003a5 into r1,
and at 0x08000230 it executes bx r1. This answers a question that might have
looked magical: how do we know where main is and that it is Thumb? Startup just
told us. It jumps to 0x080003a5. Remember chapter 02: when bx jumps to a Thumb
function, the address has bit 0 set, meaning it adds 1. So the real address is
0x080003a4, and the +1 is the signal “enter Thumb mode.” Startup handed us the
address of main and its mode.
Finally, the b 0x08000204 after the bx is a safety net: if main ever returns,
execution lands here and starts over. In a game, main should never return.
Create the function and give it the name justified by the analysis:
Click 0x08000204
Press F
Rename to start_vector
Summary of what this block does, out loud:
sets up stacks, installs the interrupt handler, and jumps to main in Thumb.
Step 5. Enter main
Startup sent us to 0x080003a4, in Thumb. Go there:
Navigation > Go To...
080003A4
Here is the most common trap. We already know, from startup analysis, that this block is Thumb. But if you only press D, Ghidra may try to read it as ARM and show garbage. First mark Thumb mode at that block:
Select starting at 0x080003A4
Right click > Set Register Values...
TMode = 1
If your Ghidra version does not have TMode, look for Set Processor Context or
something related to ARM/Thumb. The idea is always the same: tell Ghidra this is
Thumb before disassembling. Then press D. Now it comes out correctly, as 2-byte
instructions:
080003a4 push {r4, r5, r6, r7, lr} function prologue
080003a6 mov r7, r8
080003a8 push {r7}
080003aa movs r0, #0xff
080003ac bl ... starts a sequence of calls
080003b0 ...
That push {r4, r5, r6, r7, lr} on the first line is the classic prologue, the
way almost every Thumb function begins, saving registers and the return address.
That confirms we are at the start of a real function, not in the middle of one.
Create the function with F.
For now, the honest name is just “main”, because this is where startup jumped. The
canonical name AgbMain is applied at the end, after checking the public source.
Step 6. Read initialization
Scroll the Listing down from the push. What you see is a long row of calls, one
after another:
080003ac bl ...
080003ba bl ...
080003c6 bl ...
080003ca bl ...
080003ce bl ...
080003d2 bl ...
080003d6 bl ...
080003da bl ...
080003de bl ...
...
You do not need to enter each one now. The shape already tells the story. A function that does almost nothing except call dozens of other functions in sequence, with no loop, is an initialization routine. The game is turning on subsystems one by one: video, sound, controls, save system, and so on. We infer that from the shape, a straight line of calls, and confirm details later, function by function, when we need one of them.
The point here is not naming every call. It is recognizing the pattern: setup is a straight sequence of calls. The interesting part comes right after it.
Step 7. Find the main loop
This is the central deduction of the chapter. We want to find the game’s main loop without anyone telling us where it is.
Start with the reasoning. A game runs forever, one frame at a time, until you turn
it off. So somewhere there must be an infinite loop: a jump that goes backward and
has no way out. It is not an if, not a call. It is an unconditional b to an
earlier address. Finding that jump means finding the loop.
Keep scrolling down the Listing, past the initialization sequence. Around 0x08000418 the setup call row ends and a different-looking block begins, one that repeats. Continue down until you find a branch that points backward. It appears:
080004aa b 0x0800041a
This is the loop. An unconditional b that returns to 0x0800041a. No condition
before it, no exit after it. Execution enters 0x0800041a, runs down to 0x080004aa,
and returns to 0x0800041a forever. Nobody gave us the address: we followed control
flow and found the loop closing. This is the direct way to find a game loop.
Now look at the body of the loop from start to finish. The two ends say a lot:
0800041a bl 0x080005e8 first thing in the frame
... in the middle, current game-state logic
080004a6 bl 0x08000890 last thing before repeating
080004aa b 0x0800041a back to the top
Enter the first call in the loop, 0x080005e8, and the last one, 0x08000890, and
read what they do. The first reads controller button state. The last makes a BIOS
call and waits. The pattern is every game’s pattern: read input at the top of the
frame, process, and at the end wait for the screen to finish drawing before
starting the next frame. From their behavior, we can call them ReadKeys and
WaitForVBlank, and we confirm those names in the public source in step 8.
What remains is the middle of the loop, between reading controls and waiting for the frame. That is where the game decides what to show: menu, overworld, battle, shop. Notice that the loop calls the same function, 0x080004b0, several times. Go inside it and you will see that it does not contain battle logic directly. Instead, it calls an address stored in a variable, a function pointer. That is a callback.
This is the most useful idea here, and it explains how the whole game is organized:
The main loop is always the same.
The game state changes the callback pointer that the loop calls.
Menu, overworld, battle, and shop are different callbacks attached to the same loop.
That is why gameplay patches do not touch main or the loop. They touch battle logic, wild encounter logic, or shop logic, which are callbacks much later in the ROM. And that is exactly why, in chapters 05 and 06, we will not search for battle by walking down from main. We will use dynamic analysis to land directly on the HP instruction, then climb up to the function, using the same flow-following method you used here to find the loop.
Step 8. Confirm names in the public source
Everything we have done so far was deduction from code. We discovered the shape by ourselves: an entry that skips the header, an ARM startup that sets up stacks and interrupts and jumps to a Thumb main, and a main that initializes subsystems and falls into an infinite loop that reads controls and waits for the frame. The final names are confirmed by crossing our work with public material.
The pret/pokefirered project is a source reconstruction of the game, with original function names. Checking the boot path there, the shape you inferred matches, and the canonical names appear. Apply only the names your analysis has justified so far:
0x08000000 Start the entry that skips the header
0x08000204 start_vector the ARM startup
0x08000248 intr_main the interrupt handler installed by startup
0x080003A4 AgbMain the main that startup jumps to
0x080005E8 ReadKeys first call in the loop, reads controls
0x08000890 WaitForVBlank last call in the loop, waits for the frame
Notice what we did not do: we did not start naming the callbacks in the middle of the loop or the dozens of initialization functions. We have not analyzed those yet. Naming them now would be guessing again. When one of them matters to our goal, we go to it with the same method as always: find the instruction, find the function start, analyze, and only then name it. Chapters 05 and 06 do that with the HP function.
The full boot address list is in README.md, as an answer key.
Checking without Ghidra
To check any block without depending on Ghidra, use the project disassembler. It reads bytes directly from your ROM:
python3 tools/disasm_gba.py 0x08000204 --size 0x34 # startup, in ARM
python3 tools/disasm_gba.py 0x080003A4 --size 0x100 --thumb # main, in Thumb
The addresses it prints must match Ghidra. When they match, you know the mode and base are right.
What to take from this chapter
- Start from the only certain address: 0x08000000. Everything else is deduction.
- The first instruction is a branch that skips the 192-byte cartridge header.
- ARM startup sets up stacks, installs the interrupt, and jumps to main in Thumb.
- bx to 0x080003a5 gives you the main address and Thumb mode (the +1 is the T bit).
- Thumb needs TMode = 1 before D, otherwise you get garbage.
- Setup is a straight sequence of calls. The loop is an unconditional backward b.
- The loop is fixed. Game state lives in the callbacks it calls.
- Name things only after analysis justifies them. Confirm final names with pret.