← cd ../

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