DEV Community

Diogo Soares
Diogo Soares

Posted on Edited on

I Let AI Reverse Engineer a GBA ROM. It Found the Gravity, Not the Jump

I have one of those retro multi-game consoles with thousands of Atari, Master System and SNES games on it. I copied a single file out of it: Aladdin Game.gba, 3,987,712 bytes of ARM machine code. No source, no symbols, no documentation.

Then I asked an AI to tell me how the hero jumps.

Not "describe the game". I wanted numbers: how fast he walks, how hard gravity pulls him down, what changes when you hold the jump button. The kind of values a game programmer typed into a C file in the early 2000s and that nobody outside that studio has seen since.

This post covers how that went: the toolkit I used for reverse engineering with AI (an open-source MCP server called REA), what Claude Opus 5.5 actually recovered from the ROM, what it could not find, and the original browser game I built on the recovered physics parameters, since I can't show the real game converted. Here's what came out of that experiment:

Arena Run GamePlay

Why a GBA ROM is hard for reverse engineering with AI

A ROM is not a program in the way a .exe or a .js bundle is. There is no executable format to parse, no import table, no function names. The GBA loads it at a fixed address and starts executing.

What you usually have What a GBA ROM gives you
Function names and symbols Nothing. 1,700+ unnamed functions
A file format (PE, ELF, Mach-O) A 192-byte header, then raw bytes
Library calls you can recognise Writes to memory-mapped hardware registers
Floating point values Fixed point integers, 0x180 means 1.5 pixels

A language model alone can't do much with that. It can't run Ghidra in its head, and if you paste hex into a chat it will guess. The useful setup is a model that drives a real disassembler and decompiler and has to cite what the tools returned.

That is what REA does.

The toolkit: REA, an MCP server for reverse engineering

REA (Reverse Engineer Anything) is an open-source MCP server. It exposes reverse engineering tools to an AI agent: open a binary, list procedures, decompile a function, find who calls it, read bytes, follow cross references. Under the hood it drives Ghidra, the reverse engineering suite the NSA released as open source in 2019.

Two things in REA's design matter for this kind of work:

  • Every result is evidence. Each tool call returns an evidence_id, the SHA-256 of the file analysed, the Ghidra version and a list of limitations. When Claude says "this function sets gravity to 0x38", I can trace it back to a specific decompiler output.
  • The original file is never modified. Ghidra imports an ephemeral copy, and annotations live in a session database that is deleted on close unless you ask for a snapshot.

Getting it running on my Mac took more work than the analysis itself, and REA was honest about each problem:

  1. My Node was too old for REA, so I installed a newer one.
  2. Homebrew's Ghidra was too new for REA 6.8.0. REA's doctor command refused it up front instead of failing halfway through an analysis, so I installed Ghidra 12.1.4 next to it.
  3. The official Ghidra 12.1.4 zip has no native decompiler for Apple Silicon. Claude built it from the C++ source bundled in the zip, after a first build came out as x86_64 and had to be redone for arm64.

Once rea doctor was green, I registered the server in Claude Code, restarted, and the rea tools appeared in the session.

Full disclosure on the order of events: while REA was still refusing my Ghidra version, Claude did a first pass with Ghidra's headless scripts directly. Once REA worked, I had it redo the key steps through REA (load the ROM, the input routine, the gravity step, both state routines and their callers). The key values recovered through REA have evidence records, while supporting findings from the earlier headless analysis (the object layout, the apex flag, the BIOS calls) are identified separately.

Step 1: tell the tools what a GBA is

REA has no GBA loader, so the ROM goes in as a raw binary, and a raw binary needs the architecture and load address from the caller. So the first call was:

{
  "path": "Aladdin Game.gba",
  "format": "raw-binary",
  "architecture": "arm",
  "entry_point": "0x08000000"
}
Enter fullscreen mode Exit fullscreen mode

0x08000000 is where the GBA maps the cartridge ROM, so every address in this post starts with 08. The header confirmed what the file was: title ALADDIN, game code BADP, where the final P marks the European release. It is the Capcom port of the SNES game, released in Europe in March 2004.

Ghidra's auto-analysis found 1,732 procedures and 448 strings. In the first headless pass, almost all the code turned out to be Thumb, the GBA's compact 16-bit instruction set, which is what a C compiler of that era produced. The ROM also calls the GBA BIOS for LZ77 decompression, division and square roots, and waits for each vertical blank before drawing the next frame.

Step 2: find the controller

On the GBA the buttons are a single 16-bit hardware register at 0x04000130, called KEYINPUT. It is active-low: a bit is 0 while the button is pressed. Any game that reads input reads that address.

Claude followed the references to that register and landed on a small function at 0x08014d9c. It reads KEYINPUT, inverts it, and remaps the ten GBA buttons bit by bit into a layout of the game's own. My hypothesis, and I mark it as one, is that this is a porting layer: the game logic came from the SNES version and expects SNES-style button bits, so the GBA port translates its buttons at the door.

That one function mattered because every routine that later asks "is jump held?" reads its output, not the hardware.

Step 3: find gravity

This is where the AI earned its keep. Searching 1,700 nameless functions by hand for "the jump" is days of work. Claude went the other way: look for a small function that adds one field of an object to another every frame.

It found it at 0x08010024. Cleaned up, with my names instead of Ghidra's, it does this:

// 0x08010024, called once per frame for objects affected by gravity
void apply_gravity(GameObject *object) {
    object->velocity_y += object->gravity;   // +0x30 += byte at +0x34
}
Enter fullscreen mode Exit fullscreen mode

The offsets, together with what the headless pass found in the surrounding routines, gave the object layout: X position at +0x10, Y at +0x14, horizontal velocity at +0x28, vertical velocity at +0x30, and a one-byte gravity at +0x34. Positions use 1/256 of a pixel per unit, a common fixed-point choice on hardware without floating point.

REA's procedure_callers listed ten functions that call this gravity step. One of them, at 0x0800EFA0, was decompiled in the headless pass: it flips a "rising" or "falling" flag when vertical speed crosses 0x80. That is the apex of the jump.

The next question was who writes the gravity byte. The answer was a player state routine at 0x08018EE0:

// Simplified from Ghidra's output for 0x08018EE0
player->gravity = 0x38;
if (!other->flag_18 && (player->flags_32 & 0x80) && !another_flag) {
    player->gravity = 0x80;
}
Enter fullscreen mode Exit fullscreen mode

So the player has two gravities. My guess is that bit 7 of +0x32 means airborne. The lighter one (0x38, less than half of 0x80) is the default inside this routine. Variable jump height in platformers is usually built exactly like this: light gravity while you keep holding the button, heavy gravity once you let go. I implemented it that way, but the exact meaning of those flags is my reading of the code, not something the ROM states.

Step 4: find the run speed

The same cluster of player routines (between 0x08018348 and 0x0801ADC4, mapped in the headless pass) had one more gift. The routine at 0x080191E0 caps horizontal velocity:

// Simplified from Ghidra's output for 0x080191E0
if (!input->flag_19) {
    if (player->velocity_x > 0x180) player->velocity_x = 0x180;
} else {
    player->velocity_x = 0x200;
}
Enter fullscreen mode Exit fullscreen mode

I read flag_19 as the run button, but that is an interpretation. 0x180 is 1.5 pixels per frame, 0x200 is 2 pixels per frame. At 60 frames per second that is 90 and 120 pixels per second on a 240-pixel-wide screen.

What it found and what it didn't

Value Result Source
Walk speed cap 0x180 (1.5 px/frame) ROM, 0x080191E0
Run speed 0x200 (2 px/frame) ROM, 0x080191E0 (my reading of the flag)
Light gravity 0x38 per frame ROM, 0x08018EE0
Heavy gravity 0x80 per frame ROM, 0x08018EE0
Apex threshold 0x80 ROM, 0x0800EFA0 (headless pass)
Jump velocity not found my choice, -0x480
Max fall speed not found my choice, 0x500

Two of the most important numbers are missing. The jump impulse is probably loaded from a data table instead of written as a constant, and static analysis didn't connect the table to the routine. Running the game in an emulator with a debugger attached would likely answer it quickly. I chose to stop at static analysis and say so, rather than invent a value and call it recovered.

Why I didn't convert the real game

My first plan was to rebuild the opening level of the real game in JavaScript. Claude declined: the characters, art, levels and music belong to their owners, and a browser recreation would be a copy of them, credits or not. For the same copyright reasons, I won't show the real game converted here.

Physics constants are a different thing. "Gravity is 0x38" is a fact about how a program behaves, not a creative work. So we kept the numbers and built a new game around them, mixing worlds: the movement of a 2004 platformer on a Roman aqueduct at sunset.

The first draft had a running fox as the hero, and the graphics came out looking a lot like Sonic. I liked the mix of worlds, but I wanted a hero of my own, so the fox became a gladiator.

Arena Run: a browser game on the recovered physics

The final game is called Arena Run. You play an original gladiator crossing the aqueduct to the arena, with hooded bandits who chase you and lunge with a dagger, boars that charge when they see you, and crows flying in waves. One sword hit takes any of them down. Touching them costs a heart.

It is plain JavaScript and Canvas 2D at the GBA's native 240x160 resolution, scaled up with nearest-neighbour pixels. No game engine. The physics module carries the ROM values with a comment saying where each came from:

// Every value is in subpixels (1/256 px) per frame, at 60 frames per second.
export const PhysicsTuning = Object.freeze({
  walkSpeed: 0x180,     // ROM: speed cap in the routine at 0x080191E0
  runSpeed: 0x200,      // ROM: run branch of the same routine
  risingGravity: 0x38,  // ROM: gravity byte (+0x34) set at 0x08018EE0
  fallingGravity: 0x80, // ROM: written at 0x08018EE0 when three flags line up
  apexThreshold: 0x80,  // ROM: rising/falling flag at 0x0800EFA0
  jumpVelocity: -0x480, // not found in the ROM, my choice
  maxFallSpeed: 0x500,  // not found in the ROM, my choice
});
Enter fullscreen mode Exit fullscreen mode

Two details keep it faithful to the hardware:

  • Integer subpixels. Positions and velocities are integers in 1/256 px, like the original, so there is no floating-point drift between a jump in my browser and a jump in your browser.
  • A fixed 60 Hz timestep. The GBA runs game logic once per vertical blank (about 59.73 times per second). The game loop accumulates real time and advances the physics in whole frames, so a 144 Hz monitor doesn't make the gladiator jump higher.

The enemies, hearts, sword and level are my design. The ROM gave me how the hero moves, not what he fights.

One more honest note about the clip at the top: it is a bot. I wrote a small search script that tries input sequences until it reaches the arena, mostly to prove the level can be finished. It does it in 22.8 seconds with 36 of 38 coins and every heart intact, and the recording replays its inputs in the real page frame by frame.

What impressed me, and where it stops

I expected a chatbot that talks confidently about ARM assembly. What I got was closer to a junior reverse engineer who works very fast:

  • It worked from evidence. The gravity and movement values recovered through REA map to evidence records tied to the ROM's hash. Values from the headless pass and my own tuning choices are identified separately. When something wasn't in the evidence (jump velocity), it said so.
  • It chose good search strategies. Looking for "a function that adds one byte field to another every frame" is how a human would find gravity. Doing it across 1,700 functions far faster than I could by hand is not.
  • It handled the boring setup. Version mismatches, a missing native decompiler, a JDK upgrade. That is where most reverse engineering sessions die.

And the limits:

  • Static analysis has a ceiling. Data tables, values computed at runtime and anything behind function pointers need a debugger or an emulator. REA has debugger tools for native processes, but answering this would need an emulator debugger such as mGBA's.
  • Interpretation is still on you. The decompiler shows that gravity becomes 0x80 when three flags line up. Saying "this is the jump button release" is a human judgement.
  • It is not a "convert this game" button. Getting from constants to a playable game took far more work than finding the constants, and copying the original's art and levels was never on the table.

FAQ

Is it legal to reverse engineer a ROM?
Analysing how software behaves for research and interoperability is treated differently from redistributing it, and the rules vary by country. What I published here are a few numeric constants and short, simplified decompiler excerpts. No ROM, no assets, and nothing you could run.

Could the AI do this without REA?
Without a decompiler, no: a model reading hex produces plausible-looking nonsense. Without REA, partly: the first pass here used Ghidra's headless scripts directly. What REA added is an evidence record with the file hash for every claim, which is what made the findings checkable.

Why Ghidra and not IDA or Hopper?
REA supports several providers. Ghidra is free and open source, which made the whole setup reproducible without a license.

How long did it take?
The setup took longer than the analysis. Repeating the key findings through REA took a handful of tool calls, because by then we knew where to look.

Takeaway

  • An AI reverse engineering a binary is only as good as its tools. REA plus Ghidra turned guesses into cited evidence.
  • Search by behavior, not by name. "Adds a byte to the vertical speed every frame" found the gravity routine among 1,700 unnamed functions.
  • Say what you didn't find. Jump velocity and max fall speed are missing, and the game says so in its own source code.
  • Facts are portable, creative work isn't. Numbers about how a program behaves can live in a new game. Characters and levels can't.

Which old game would you point this at first, and what would you want to know about how it works?


I'm Diogo Soares, a Staff AI Engineer working on AI agents and legacy modernization. If this was useful, follow me here on DEV.to and on Instagram at @diogosoares.dev, where every article becomes a visual carousel you can save. This article also gets a video walkthrough on my YouTube channel, Diogo Soares Dev, so subscribe there to see Arena Run in action. Let's also connect on LinkedIn, or check out diogosoares.dev.

Top comments (0)