Kirby & The Amazing Mirror is a platformer released in 2004 for the Game Boy Advance. Some debug strings were found on an earlier version, first released in Gekkan Nintendo Tentou Demo 2004.3.1:

00826140: 4348 4152 4143 5445 5220 2020 2020 2020  CHARACTER
00826150: 2020 2020 0000 0000 5041 5454 4552 4e20      ....PATTERN
00826160: 2020 2020 2020 2020 2020 2020 0000 0000              ....
00826170: 5349 5a45 2020 2020 2020 2020 2020 2020  SIZE
00826180: 2000 0000 5249 4748 542c 4c45 4654 3a20   ...RIGHT,LEFT:
00826190: 4348 4152 4143 5445 5220 2b2d 2000 0000  CHARACTER +- ...
008261a0: 5550 2c44 4f57 4e20 2020 3a20 5041 5454  UP,DOWN   : PATT
008261b0: 4552 4e20 2b2d 0000 413a 2052 4550 4c41  ERN +-..A: REPLA
008261c0: 5920 2020 2020 423a 2045 5849 5400 0000  Y     B: EXIT...
008261d0: 5354 4152 543a 2053 544f 5020 2020 5345  START: STOP   SE
008261e0: 4c45 4354 3a20 534c 4f57 0000 5354 4152  LECT: SLOW..STAR
008261f0: 543a 2050 4c41 5920 2020 5345 4c45 4354  T: PLAY   SELECT
00826200: 3a20 534c 4f57 0000 4348 4152 4143 5445  : SLOW..CHARACTE
00826210: 5220 2530 3364 0000 5041 5454 4552 4e20  R %03d..PATTERN
00826220: 2020 2530 3364 0000 5349 5a45 2020 2020    %03d..SIZE
00826230: 2020 2530 3364 0000 6b62 7964 6561 6400    %03d..kbydead.
00826240: 626f 7373 5f6b 796f 7475 0000 626f 7373  boss_kyotu..boss
00826250: 5f6d 6964 0000 0000 706c 795f 3500 0000  _mid....ply_5...
00826260: 706c 795f 3200 0000 706c 795f 3100 0000  ply_2...ply_1...

Let’s try to restore these debug functions.


  • Ghidra loader: GhidraGBA;
  • Emulator: mGBA (includes a GDB stub for debugging, as well as some convenient game state views e.g. tiles loaded in memory);
  • Tile viewer: YY-CHR (has a predefined format for GBA’s 4bpp graphics);


As in previous writeups, we can start by taking an instruction trace log. In this case, it includes booting and pressing “Start” at the title screen, then waiting until the start level has loaded.

To capture the trace with mGBA, we open “Tools > Open debugger console…”, then enter command trace 9999999 out, which logs to file “out”. We then filter lines to get the set of program counter addresses in file “trace.out”:

<out awk '{print $16}' | sort -u > trace.out

Character test

The string “CHARACTER” at address 0x08826140, along with other strings, have cross-references in FUN_080f9e68 (named debug_character_test_callback), which is passed as parameter in a call made by FUN_080f9c40 (named debug_character_test):

undefined4 debug_character_test(void) {
    // ...
    DAT_030035b0 = 0x1440;
    DAT_03002f54 = 0x1b0c;
    DAT_030035a8 = 0;
    DAT_030035aa = 0;
    uStack28 = 0;
    DMA3SAD = &uStack28;
    DMA3DAD = &DAT_0600c000;
    DMA3CNT_L = 0x85000010;
    DAT_03005fc0._2_1_ = 0;
    DAT_03002f28 = 0;
    DAT_03002f29 = 0;
    DAT_03002f2a = 0xff;
    DAT_03002f2b = 0x14;
    DAT_030036c0 = 0x5660;
    DAT_030036c2 = 0x7fff;
    DAT_030036e2 = 0x3e0;
    DAT_030024e0 = DAT_030024e0 | 1;
    iVar1 = FUN_08111d90(debug_character_test_callback + 1,0x94,0x100,0,&LAB_080fa394+1);
    // ...

However, debug_character_test() has no call references, so we need to figure out the best place to call it. We can see it has some setup related with DMA transfers, so we can start by checking which other functions have a similar setup, including a call to FUN_08111d90. That function happens to be reached in our trace, but also has an intimidating number of call references:

Let’s try to narrow down interesting references by placing a breakpoint at 0x08111d90. Then, we can just take the value of register lr to get the caller’s address.

In mGBA, we open “Tools > Start GDB server…”, then bind to the server with our GDB client. Note that some Linux distributions have a “gdb-multiarch” package, which you can install to have support for the ARM architecture. We also set options for endianness and the compressed instruction set, which seems to be what the game runs most of the time:

gdb-multiarch \
    -ex='set architecture armv4t' \
    -ex='set arm fallback-mode thumb' \
    -ex='set arm force-mode thumb' \
    -ex='set endian little' \
    -ex='target remote localhost:2345'

For convenience, consider adding a frontend like gdb-dashboard, which seems to have better compatibility with GDB stubs compared to more sophisticated frontends.

After connecting, we set the breakpoint with b *0x08111d90, then continue execution with c.

The first hit we get is at 0x080fac73, in the following function:

undefined4 FUN_080fac38(void) {
  int iVar1;
  undefined4 in_lr;
  undefined4 local_10;
  undefined local_c [4];

  local_10 = 0xffffffff;
  DAT_030035b0 = 0x240;
  DAT_03002f52 = 0x1f01;
  iVar1 = FUN_08111d90(FUN_080facbc + 1,0x4c,0x1000,0,&LAB_080facd8+1);
  DMA3DAD = *(ushort *)(iVar1 + 6) + 0x3000000;
  DMA3SAD = local_c;
  DMA3CNT_L = 0x81000026;
  *(undefined **)(&DAT_03000040 + *(ushort *)(iVar1 + 6)) = &LAB_080facdc+1;
  return in_lr;

Which is called here:

08000332 fa f0 81 fc     bl         FUN_080fac38

Seems like a good candidate. We can select the call instruction in Ghidra and replace the address with “Patch Instruction”:

-08000332 fa f0 81 fc     bl         FUN_080fac38
+08000332 f9 f0 85 fc     bl         debug_calls_character_pattern

Now that we know the bytes, let’s apply them under GDB (note that they are encoded in little endian):

set *(char**)(0x08000332) = 0xfc85f0f9

When we reset the console, we get promising results:

But what’s going on with the font?

Let’s open in mGBA “Tools > Game state views > View tiles…”, where we see that the glitched tiles start at address 0x0600c040:

Memory ranged in 0x06000000..0x06017fff is mapped to VRAM. Even if we didn’t have this tile viewer, we could look for addresses inside this range, and find one function call inside debug_character_test() which loads that same address:

080fa246 20 1c           add        r0=>DAT_0600c040,r4,#0x0
080fa248 00 22           mov        r2,#0x0
080fa24a 03 23           mov        r3,#0x3
080fa24c 19 f0 74 fe     bl         FUN_08113f38

Let’s break after that function call with b *0x080fa250, then inspecting that memory:

>>> x/10wx 0x0600c040
0x600c040:      0x08734174      0x08734470      0x0873835c      0x0873d310
0x600c050:      0x08740254      0x0874439c      0x08745508      0x0874d85c
0x600c060:      0x08771348      0x08771354

Strange, those appear to be pointers, not tile graphics… Where are they stored in the ROM?

binwalk -R '\x74\x41\x73\x08' ./rom
# 0xA29D78

We can double-check the tiles around this offset using YY-CHR, and they appear to be close to the loaded glitched tiles, disregarding palette differences:

This offset ends up mapped in the ROM memory range, and indeed, it’s disassembled as addresses:

08a29d78 74 41 73 08     addr       DAT_08734174
08a29d7c 70 44 73 08     addr       DAT_08734470

However, there’s no direct reference to the address in question. Probably it’s loaded via an offset added to a base address. Let’s look inside FUN_08113f38 (named load_tiles):

undefined8 load_tiles(int param_1,int param_2,int param_3,int param_4,int param_5,char *param_6,uint param_7) {
  char cVar1;
  uint uVar2;
  int iVar3;
  uint uVar4;
  undefined4 in_lr;

  uVar4 = 0;
  uVar2 = (uint)*(ushort *)((int)&DAT_03002f50 + ((uint)(param_5 << 0x18) >> 0x17));
  cVar1 = *param_6;
  while (cVar1 != '\0') {
    iVar3 = param_1 + uVar4 * 0x20;
    FUN_08118470((uint)(byte)param_6[uVar4] * 0x20 + param_2,iVar3,8);
    *(ushort *)((uVar2 & 0x1f00) * 8 + 0x6000000 + ((uint)(param_4 << 0x10) >> 10) + ((uint)(param_3 << 0x10) >> 0xf) + uVar4 * 2) =
         (ushort)((iVar3 - ((uVar2 & 0xc) * 0x1000 + 0x6000000)) * 0x800 >> 0x10) | (ushort)((param_7 & 0xff) << 0xc);
    uVar4 = uVar4 + 1 & 0xff;
    cVar1 = param_6[uVar4];
  return CONCAT44(in_lr,uVar4 << 5);

If we set a memory read breakpoint with rwatch *0x08a29d78, we see that it is hit during that call to FUN_08118470, so we should inspect the original 1st, 2nd, and 6th parameters.

If we break before the call to load_tiles() with b *0x080fa24c, we see that the 2nd parameter is loaded in register r1 with value 0x08a29518. The 6th parameter is passed via the stack, and is a pointer to the ascii characters that will be matched to tile offsets:

08113f4a 0a 9f           ldr        r7,[sp,#param_6]  ; r7 = 0x03007d9c
>>> x/10wx 0x03007d9c
0x3007d9c:      0x52414843      0x45544341      0x30302052      0x20200030
0x3007dac:      0x20202020      0x00000000      0x54544150      0x204e5245
0x3007dbc:      0x30302020      0x20200030

So this is the effective address being calculated for the first character “C”:

0x08a29518 + 0x20 * 0x43 = 0x8a29d78

Great, so it appears that the base address may be wrong. For some reason, it ended up with this reference…

                     XREF[1]:     debug_character_test_callback:080fa236
080fa2f8 18 95 a2 08     addr       s_SRAM_V113_08a29518

…to this string:

08a29518 53 52 41        ds         "SRAM_V113"
         4d 5f 56
         31 31 33 00

But are the actual font tiles still present? I gave another look with YY-CHR, and luckly, did find them at this offset:

All we have to do is replace the base address with the font tiles’ address:

set *(char**)(0x080fa2f8) = 0x087fc848

Now when we reset the game…


Sound test

There’s also a sound test at FUN_080fa3c0 (named debug_sound_test), which we can restore in a similar manner, with the following patches:

set *(char**)(0x08000332) = 0xf845f0fa
set *(char**)(0x080fa8f0) = 0x087fc848
set *(char**)(0x080faa70) = 0x087fc848

Here it is:

Hmm, we aren’t quite done yet…

It looks like the tiles are being overwritten at some point, which we can also confirm in GDB with watch *0x06000040, where we see 0x1 being written instead of 0x21111111 (i.e. the bitmap’s first line for character “O”):

Hardware watchpoint 1: *0x06000040

Old value = 554766609
New value = 0
0x08113e84 in ?? ()
=> 0x08113e84:  47 46   mov     r7, r8


Old value = 0
New value = 1
0x08113fb6 in ?? ()
=> 0x08113fb6:  00 06   lsls    r0, r0, #24

Now, there’s quite a lot of free space in the VRAM, maybe we can just patch the game to load the tiles at a different address?

Let’s check where the VRAM address is being set. On FUN_080fa630 (named debug_sound_test_callback), referenced by debug_sound_test(), we see the destination being assigned based on the value of 0x03002f54, which happens to always be 0, so the tiles end up on 0x60000000:

DMA3DAD = (DAT_03002f54 & 0xc) * 0x1000 + 0x6000000;
DMA3CNT_L = 0x85000010;
DAT_03005fc0._2_1_ = 0;
DAT_03002f28 = 0;
DAT_03002f29 = 0;
DAT_03002f2a = 0xff;
DAT_03002f2b = 0x40;
iVar5 = load_tiles(&DAT_06000020,"SRAM_V113",0,1,2,auStack92,0);

On debug_sound_test(), we have this assignment:

DAT_03002f56 = 0x1609;

What’s interesting here is that DAT_03002f54 is never assigned, but the DMA3DAD calculations suggest that it wouldn’t be a fixed value. In fact, if we set DAT_03002f54 = 0xc, we land on free space in VRAM. So let’s do exactly that.

We start by updating that assignment to set both values starting at DAT_03002f54:

-080fa3e0 c8 80           strh       r0,[r1,#0x6]=>DAT_03002f56
+080fa3e0 88 80           strh       r0,[r1,#0x4]=>DAT_03002f54
-080fa52c 09 16 00 00
+080fa52c 0c 00 09 16

Then on debug_sound_test_callback(), we have to update the base address used to load tiles:

-080fa688 20 00 00 06     addr       DAT_06000020
+080fa688 20 c0 00 06     addr       DAT_0600c020

These changes can be done under GDB:

set *(char**)(0x080fa688) = 0x0600c020
set *(char**)(0x080fa52c) = 0x1609000c
set *(char**)(0x080fa3e0) = 0x96018088

After a reset…

Also fixed! (Except those tiles used to indicate if we have BGM or SE selected, but no idea what were the intended ones…)

Error handler

Near these debugging functions there’s FUN_0802a47c, which appears to be an exception handler, and it’s actually called from various places. A quick translation of the background text seems to hint at communication errors, probably intended for multiplayer functionalities.

We can activate it under GDB in a similar manner:

set *(char**)(0x08000332) = 0xf8a3f02a
set *(char**)(0x0802a3a0) = 0x087fc848


As an alternative to messing around with the debugger, load the cheats in mGBA, or apply the following GameShark v1/2 codes.

Character test

Patch Raw codes Encrypted codes
08000332 = f9 f0 85 fc
080fa2f8 = 48 c8 7f 08
60000199 1000f0f9
6000019a 1000fc85
6007d17c 1000c848
6007d17d 1000087f
1C65B2D8 5ACD7B67
4A19FBC3 4A86A4C5
51ABC444 0109F7E3

Sound test

Patch Raw codes Encrypted codes
08000332 = fa f0 45 f8
080fa8f0 = 48 c8 7f 08
080faa70 = 48 c8 7f 08
080fa688 = 20 c0 00 06
080fa52c = 0c 00 09 16
080fa3e0 = 88 80 01 96
60000199 1000f0fa
6000019a 1000f845
6007d478 1000c848
6007d479 1000087f
6007d538 1000c848
6007d539 1000087f
6007d344 1000c020
6007d345 10000600
6007d296 1000000c
6007d297 10001609
6007d1f0 10008088
6007d1f1 10009601
BB9DA15D 3E337329
02C9A48A 59F0AB6A
60E64205 A81C77C2
42AC61B2 417FE89B
65BAC887 41E5BF46
47D9E65F AFDA5354
1A22A6BA 4483E5E1
69CC0200 F5EFB746
A8760666 306D820A
B2955BAC D4EB39C4
FEB1458C EBD2C003

Error handler

Patch Raw codes Encrypted codes
08000332 = 2a f0 a3 f8
0802a3a0 = 48 c8 7f 08
60000199 1000f02a
6000019a 1000f8a3
600151d0 1000c848
600151d1 1000087f
BB582DD7 86325FC2
A27628E4 8F8E8E4F
30501D5C DF483904
88EA6D98 4ADBA25B