Through the Looking-Glass, and What Kirby Found There
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.
Tooling
- 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);
Analysis
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;
FUN_08118470(&local_10,&DAT_05000000,0x1000080);
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…
PTR_s_SRAM_V113_080fa2f8
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…

Fixed!
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;
printf(auStack92,"COUNT:0x%04x",*psVar11);
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
TL;DR
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 66B8A5A3 6FAFBC3F 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 FA9B7B6A 42D5CD5B 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 |