When several consoles are so well understood to the point where discussion revolves around cycle-accuracy, you wonder how much low-hanging fruit is out there. How about an obscure Japan exclusive educational console?

I’ll provide an overview about the Beena, mostly informed from static analysis of game cartridge ROM dumps. Along with janky hardware contraptions to interact with a JTAG port, we’ll see how this all plays into dumping the BIOS. If you just want to do your own dumps, jump to the OpenOCD section.

Spelunking time

Before starting this adventure, technical details documented in English were “uh… this has an ARM7TDMI”. Surely we could get something more out of Japanese sources, and searching for site:jp beena arm7tdmi returned some media coverage alluding to a large scale integration (LSI) chip named AP2010. Which was curious, since the chip on the board is marked “Sega Toys 9H0-0008”. Wikipedia had a link to a press release about AP2010 from the manufacturer Applause Technologies. Navigating the home page in Internet Archive, I was able to find the English version, along with a product catalog. Still, far from having a datasheet. But now we know that the 9H0-0008 is just an AP2010 bundled with Sega’s BIOS.

The catalog included these particular features:

  • ROM: 128KB
  • RAM: 16KB
  • Debug interface: JTAG

So we know how large the BIOS dump should be. What about that JTAG? A 20-pin port can be spotted at the lower-right corner of the board:

I got in touch with Team Europe, who had dumped Beena carts over a decade ago. They hadn’t explored the JTAG interface, but having figured out the cart’s pinout, making a flashcart was on the table. Since I would end up disassembling some ROMs to at least get an idea of memory layout and BIOS calls, it was worth considering this option as well.

Dissecting games

Using Soreike! Anpanman Hajimete Kaketa yo! Oboeta yo! Hiragana Katakana as an example, here’s the ROM header:

If we swap each pair of bytes, we get recognizable strings like SEGATOYS and edinburgh. The latter is part of some medieval theme for naming boards / chips: on the product catalog, AP2010 is marked “LANCELOT”, while its evaluation board is marked “EDINBURGH_EVAL”.

We can tell this ARM7TDMI is big endian from these hints:

  • OggS magic bytes are scattered across the rom. If we take their file offsets and search for the 2 least significant bytes, we can find some references with format 0x80XXYYZZ, where YYZZ are the bytes we searched for. This also gives us a base address of 0x80000000 for the ROM memory region. For example, 0x8002800c references 0x800544dc:
      00028000: e793 3f01 e7a3 0000 8004 ea2c 8005 44dc  ..?........,..D.
      ...
      000544d0: b701 0a2a 0029 0904 2f2f 1111 4f67 6753  ...*.)..//..OggS
    
  • At file offset 0 there’s a jump to 0x100 producing valid disassembly, which then follows through to the game’s reset handler:
      80000000 ea 00 00 3e     b          FUN_80000100
      ; ...
      80000100 e5 1f d1 00     ldr        sp,[GameHeader_80000008]
      80000104 e5 9f 00 54     ldr        r0,[PTR_FUN_8000010c+1_80000160] = 8000010d
      ; branch to reset handler in thumb mode
      80000108 e1 2f ff 10     bx         r0=>FUN_8000010c
    

Looking up strings brought up a test mode. In its technical overview you can see how inputs were discovered to activate it, which can be blandly summarized as “tracing memory accesses to candidate i/o addresses, then conditional logic for parsed values”.

The relevant part here is that it uses a format string based function to pick which font tiles to render. Some of the arithmetic operations (named ext_*) jumped to code outside of the ROM:

undefined8 vfprintf(byte *buf,byte *fmtstr,void **params) {
    // ...
    pcVar9 = &stack0xffffffdc;
    if ((sign) && ((int)val < 0)) {
      val = (dword *)-(int)val;
      flg = flg | 0x100;
    }
    /* cvt_radix() inlined, similar to https://github.com/7dog123/n64-sdk/blob/ffd658e0696d6f14f389973631fd5c2317870ace/ultra/GCC/MIPSE/SOURCE/PRINTF.C#L65 */
    do {
      iVar6 = ext_220_mod(val,ord_base);
      pcVar9 = (char *)((int)pcVar9 + -1);
      *pcVar9 = binasc[iVar6];
      val = (dword *)ext_218_int_div(val,ord_base);
    } while (val != (dword *)0x0);
    // ...
}
                     ext_220_mod
80026cc0 29 00           cmp        r1,#0x0
; if second parameter is zero, branch to error handler
80026cc2 d0 e1           beq        LAB_80026c88
; otherwise, branch to BIOS function stub, switching to ARM mode
80026cc4 47 78           bx         pc=>LAB_80026cc8
80026cc8 e5 9f f0 00     ldr        pc=>[DAT_80026cd0] = 00000220h
; branch to BIOS function at 0x220
80026ccc 47 70           bx         lr

It’s worth comparing this with another ARM7TDMI based console: the Game Boy Advance. We also find arithmetic functions offered by the BIOS, although called via software interrupt (SWI) instructions. Being able to jump to arbitrary places in the BIOS probably ruled out software based protections (we could jump past them), and also invited us to consider exploits based on return-oriented programming. But we still need to confirm if there’s any copy protection to begin with…

Attempt #1: Dumping via flashcart

Test mode provided a foundation for exfilling data to video output. At the very least, we should be able to loop over some addresses starting at 0x200 and display an hexdump of whatever was there.

I decided to patch this function in an existing game. Implementation-wise, it was similar to krystalgamer’s approach, in the sense that I wrote the code in C and built it with an appropriate target triplet instead of writing hand-tailored assembly. Some differences included not having to deal with ABI issues, using a linker script to hardcode offsets for compiled functions (which also allowed to call them by symbol name without further adjustments), and debugging emulated code in GDB, as long as I stubbed all BIOS calls.

Unfortunately, this was all cut short: There was some issue with the flashcarts that caused the Beena to reject them, even though they could be read just fine like regular carts. I love hardware.

Attempt #2: Dumping via JTAG

Back to that 20-pin port, which screams “ARM Standard JTAG”:

Using a multimeter in continuity mode, I identified ground pins by placing one probe on a metal shielded connector, and another probe on each pin. Then I took these voltage measurements, placing the COM probe on a ground pin, then the mAVΩ probe on each pin:

    20  19
 ┌──────────┐
 + GND  0   +
 +   0  0   +
 + GND  3.3 +
 + GND  3.3 +
 + GND  0   +
 + GND  3.3 +
 + GND  3.3 +
 + GND  3.3 +
 + GND  3.3 +
 + 3.3  0   +
 └──────────┘
     2  1

There’s a very close match for GNDs and VCC at pin 2. Pins pulled high are expected in the specification for TDI, TDO, TMS, TCK, nTRST, and nSRST.

Furthermore, nSRST can be “pulled strong-LOW to initiate a reset”. Seems like my multimeter was enough to do this, since taking a voltage measurement for pin 15 caused a running game to reset back to the first screen!

Still, we should confirm these pins, starting by soldering a 20-pin header to this port.

Unfortunately, I struggled to remove existing solder that filled all these unused through-holes. I applied some rosin flux, then a solder wick, using an iron with a flat tip, even added a bit of fresh solder, still nothing. My guess is that I wasn’t applying enough heat to remove lead-free solder, since my iron maxed around 340C. But I didn’t want to accidentally burn any of these connections, so I opted for a safer alternative (well, for a definition of safe):

There are also concerns with mixing different solder types, but my house still stands.

We find four columns on the back of the PCB, where the middle two have continuity on each row and match pins 2..20, while the outer two match pins 1..19.

The next steps were to follow Wrongbaud’s comprehensive guide on reversing JTAG.

Validating pinout with JTAGenum

I went with an Arduino Uno, adapting the included sketch file in the JTAGenum repo, according to “ARM7TDMI (Rev 3) Technical Reference Manual”:

 // Target specific, check your documentation or guess
 #define SCAN_LEN                 1890 // used for IR enum. bigger the better
-#define IR_LEN                   5
+#define IR_LEN                   4
 // IR registers must be IR_LEN wide:
-#define IR_IDCODE                "01100" // always 011
-#define IR_SAMPLE                "10100" // always 101
+#define IR_IDCODE                "0110" // always 011
+#define IR_SAMPLE                "1010" // always 101
 #define IR_PRELOAD               IR_SAMPLE

A logic level shifter is required to convert the Arduino’s operating voltage of 5V to the Beena’s 3.3V. My first option was using the TXB0108:

Pinout diagram (after OE pulled high):

    Beena JTAG
      20  19
   ┌──────────┐              TXB0108
   + GND  0   +           ┌────────────┐
   +   0  0   +        ┌──+ VCCA  VCCB +──────────────┐
   + GND  3.3 +────────│──+ A1      B1 +───────────┐  │
   + GND  3.3 +────────│──+ A2      B2 +──────────┐│  │
   + GND  0   +  ┌─────│──+ A3      B3 +─────────┐││  │
   + GND  3.3 +──│─────│──+ A4      B4 +────────┐│││  │
   + GND  3.3 +──│─────│──+ A5      B5 +───────┐││││  │
   + GND  3.3 +──│─────│──+ A6      B6 +──────┐│││││  │
   + GND  3.3 +──┘     │  + A7      B7 +      ││││││  │
 ┌─+ 3.3  0   +        │  + A8      B8 +      ││││││  │
 │ └──────────┘        +──+ OE     GND +──┐   ││││││  │
 │     2  1            │  └────────────┘  │   ││││││  │
 │                     │                  │   ││││││  │
 └─────────────────────┘                  │   ││││││  │
                                          │   ││││││  │
                ┌─────────────────────────┘   ││││││  │
                │┌────────────────────────────────────┘
                ││                            ││││││
                ││       Arduino Uno          ││││││
                ││   ┌─────────────────┐      ││││││
                ││   │             D19 +      ││││││
                ││   │             D18 +      ││││││
                ││   │            AREF +      ││││││
                ││   + NC          GND +      ││││││
                ││   + IOREF       D13 +      ││││││
                ││   + RESET       D12 +      ││││││
                ││   + 3.3V        D11 +      ││││││
                │└───+ 5V          D10 +      ││││││
                │    + GND          D9 +      ││││││
                └────+ GND          D8 +      ││││││
                     + VIN          D7 +──────┘│││││
                     │              D6 +───────┘││││
                     + A0           D5 +────────┘│││
                     + A1           D4 +─────────┘││
                     + A2           D3 +──────────┘│
                     + A3           D2 +───────────┘
                     + A4        TX/D1 +
                     + A5        RX/D0 +
                     └─────────────────┘

Compared to other converters, one difference is the behaviour controlled by the OE pin:

The output-enable (OE) input circuit is designed so that it is supplied by VCCA and when the (OE) input is low, all outputs are placed in the high-impedance state. To ensure the high-impedance state of the outputs during power-up or power-down, the OE input pin must be tied to GND through a pulldown resistor and must not be enabled until VCCA and VCCB are fully ramped and stable.

Indeed we don’t read any outputs while OE is low. After uploading the Arduino sketch, we can run picocom -b 115200 /dev/ttyACM0 on our host for serial communication with JTAGenum. If we type s to run a pattern scan, or i for an idcode scan, we don’t get any results.

However, after OE was pulled high, running those scans more than once always brought up different results, sometimes with obviously wrong data, such as reporting more than one device (this is possible since a single JTAG interface can be daisy-chained to connect several devices). Apparently other folks also experienced issues with this converter. I love hardware.

To rule out whatever variables are causing issues, I checked a router with a fairly understood JTAG port, and also got flaky readings there.

Afterwards, I tried some generic 8-channel logic level converter:

Pinout diagram:

                                              Beena JTAG
                                                20  19
                                             ┌──────────┐
                                             + GND  0   +
                                             +   0  0   +
                                             + GND  3.3 +
                                             + GND  3.3 +─────────────┐
                                             + GND  0   +  ┌─────────┐│
                                             + GND  3.3 +──│────────┐││
                                             + GND  3.3 +──│───────┐│││
                                             + GND  3.3 +──│──────┐││││
                                             + GND  3.3 +──┘      │││││
                                           ┌─+ 3.3  0   +         │││││
                                           │ └──────────┘         │││││
  ┌────────────────────────────────┐       │     2  1             │││││
  │┌───────────────────────────────│─┐     └───────────────────┐  │││││
  ││                               │ │                         │  │││││
  ││       Arduino Uno             │ │    Generic 8─Channel    │  │││││
  ││   ┌─────────────────┐         │ │  Logic Level Converter  │  │││││
  ││   │             D19 +         │ │     ┌────────────┐      │  │││││
  ││   │             D18 +         │ └─────+ VCCA  VCCB +──────┘  │││││
  ││   │            AREF +         └────┐  + GND    GND +         │││││
  ││   + NC          GND +      ┌───────│──+ A0      B0 +─────────││││┘
  ││   + IOREF       D13 +      │┌──────│──+ A1      B1 +─────────│││┘
  ││   + RESET       D12 +      ││┌─────│──+ A2      B2 +─────────││┘
  ││   + 3.3V        D11 +      │││┌────│──+ A3      B3 +─────────│┘
  │└───+ 5V          D10 +      ││││┌───│──+ A4      B4 +─────────┘
  └────+ GND          D9 +      │││││   │  + A5      B5 +
  ┌────+ GND          D8 +      │││││   │  + A6      B6 +
  │    + VIN          D7 +      │││││   │  + A7      B7 +
  │    │              D6 +──────││││┘   └──+ GND    GND +──┐
  │    + A0           D5 +──────│││┘       + VCCA  VCCB +  │
  │    + A1           D4 +──────││┘        └────────────┘  │
  │    + A2           D3 +──────│┘                         │
  │    + A3           D2 +──────┘                          │
  │    + A4        TX/D1 +                                 │
  │    + A5        RX/D0 +                                 │
  │    └─────────────────┘                                 │
  └────────────────────────────────────────────────────────┘

Well, this one worked! Running an idcode scan on the Beena returned exactly one result, where the digital pins match the expected ARM Standard JTAG pins, even the id itself is the one typical of ARM7TDMI chips:

> i
================================
Starting scan for IDCODE...
(assumes IDCODE default DR)
 ntrst:DIG_3 tck:DIG_4 tms:DIG_5 tdo:DIG_2 tdi:DIG_6  devices: 1
  0x3F0F0F0F
================================

Interfacing with OpenOCD

With the minimal set of JTAG pins confirmed, we can now connect a debug adapter. Searching around you can find a plethora of probes that are no longer sold… Fortunately, they aren’t really required:

All of the things that you can do with a buspirate can be done with various embedded Linux SBCs: Orange Pi 2/Zero/4 BeagleBone Green/Black/Micro Raspberry Pi 4 They have all of the same peripherals and plenty of tooling to get you started!

Makes sense, since OpenOCD can use sysfsgpio, a bitbang JTAG driver using Linux kernel’s sysfs to export GPIO lines. For this job, I picked BeagleBone Black, which already came with the latest image installed at the time of writing (AM3358 Debian 10.3 2020-04-06).

After adding my SSH public key to .ssh/authorized_keys via the Cloud9 terminal, I logged in via SSH to free up some space with apt remove c9-core-installer --purge so that I could build OpenOCD on the target. This was just being lazy, cross-compilation should also work.

We need to configure the test access port (TAP) interface for our chip (looking up some ARM7TDMI examples in the repo), then which GPIO pins should be mapped to JTAG pins.

9h00008.cfg:

# This is using the name on the LSI chip
if { [info exists CHIPNAME] } {
  set _CHIPNAME $CHIPNAME
} else {
  set _CHIPNAME 9h00008
}

# We know the endianess from disassembled game ROMs
if { [info exists ENDIAN] } {
   set _ENDIAN $ENDIAN
} else {
   set _ENDIAN big
}

# This is the TAP ID that we discovered in the previous step
if { [info exists CPUTAPID] } {
  set _CPUTAPID $CPUTAPID
} else {
  set _CPUTAPID 0x3f0f0f0f
}

transport select jtag

# JTAG scan chain
# format L IRC IRCM IDCODE (Length, IR Capture, IR Capture Mask, IDCODE)
# based on samsung_s3c4510.cfg
# if board resets when halted, check aduc702x.cfg for a watchdog workaround
jtag newtap $_CHIPNAME cpu -irlen 4 -ircapture 0x1 -irmask 0xf -expected-id $_CPUTAPID

# Target configuration
set _TARGETNAME $_CHIPNAME.cpu
target create $_TARGETNAME arm7tdmi -endian $_ENDIAN -chain-position $_TARGETNAME

bbb.cfg:

#
# Config for using Beaglebone Black's expansion header
# based on https://github.com/jcmarsh/jtag_eval/blob/2423f55ed2f2322c7507b5ef50365fa9d47ccd80/openOCD_cfg/bbb.cfg
#
# This is best used with a fast enough buffer but also
# is suitable for direct connection if the target voltage
# matches BB's 3.3V
#
# Do not forget the GND connection
#
interface sysfsgpio

# Pins               GND    TCK    TMS    TDI    TDO
# Using         X  P8_02  P8_14  P8_12  P8_10  P8_16
# Number        X      X     26     44     68     46
sysfsgpio_jtag_nums 26 44 68 46

# At least one of srst or trst needs to be specified
# P8_08
sysfsgpio_trst_num 67
# P8_18
sysfsgpio_srst_num 65

Since BBB has an operation voltage of 3.3V, we don’t need any logic level conversion:

Pinout diagram:

          Beena JTAG
            20  19
         ┌────────────┐
      ┌──+ GND  0     +
      │  +   0  0     +
      │  + GND  nSRST +─────────────────┐
      │  + GND  TDO   +────────────────┐│
      │  + GND  RTCK  +                ││
      │  + GND  TCLK  +──────────────┐ ││
      │  + GND  TMS   +─────────────┐│ ││
      │  + GND  TDI   +────────────┐││ ││
      │  + GND  nTRST +───────────┐│││ ││
      │  + 3.3  0     +           ││││ ││
      │  └────────────┘           ││││ ││
      │      2  1                 ││││ ││
      └──────────────────────┐    ││││ ││
                             │    ││││ ││
        BeagleBone Black     │    ││││ ││
         P9          P8      │    ││││ ││
     ┌────────────────────┐  │    ││││ ││
     + 1   2 +    + 1   2 +──┘    ││││ ││
     + 3   4 +    + 3   4 +       ││││ ││
     + 5   6 +    + 5   6 +       ││││ ││
     + 7   8 +    + 7   8 +───────┘│││ ││
     + 9  10 +    + 9  10 +────────┘││ ││
     + 11 12 +    + 11 12 +─────────┘│ ││
     + 13 14 +    + 13 14 +──────────┘ ││
     + 15 16 +    + 15 16 +────────────┘│
     + 17 18 +    + 17 18 +─────────────┘
        ...          ...
     + 45 46 +    + 45 46 +
     └────────────────────┘

However, OpenOCD had some issue interacting with sysfs:

debian@beaglebone:~/openocd$ ./src/openocd -f ../beena/bbb.cfg -f ../beena/9h00008.cfg
Open On-Chip Debugger 0.12.0+dev-gfc30feb (2023-03-07-23:32)
Licensed under GNU GPL v2
For bug reports, read
	http://openocd.org/doc/doxygen/bugs.html
DEPRECATED! use 'adapter driver' not 'interface'
DEPRECATED! use 'sysfsgpio jtag_nums' not 'sysfsgpio_jtag_nums'
DEPRECATED! use 'sysfsgpio trst_num' not 'sysfsgpio_trst_num'
DEPRECATED! use 'sysfsgpio srst_num' not 'sysfsgpio_srst_num'
SysfsGPIO num: srst = 65
Info : auto-selecting first available session transport "jtag". To override use 'transport select <transport>'.
9h00008.cpu
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : SysfsGPIO JTAG/SWD bitbang driver
Error: Couldn't export gpio 26
Error: sysfsgpio: Operation not permitted
Error: Couldn't unexport gpio 26
Error: Couldn't unexport gpio 44
Error: Couldn't unexport gpio 68
Error: Couldn't unexport gpio 46
Error: Couldn't unexport gpio 67
Error: Couldn't unexport gpio 65

Which was odd, since the user did have permissions, and it worked if done manually:

echo 26 > /sys/class/gpio/export
echo 26 > /sys/class/gpio/unexport

I suppose there was some contention involved. On a whim, I added a sleep before writing to the sysfs file:

diff --git a/src/jtag/drivers/sysfsgpio.c b/src/jtag/drivers/sysfsgpio.c
index ee254d66a..696877b02 100644
--- a/src/jtag/drivers/sysfsgpio.c
+++ b/src/jtag/drivers/sysfsgpio.c
@@ -41,6 +41,7 @@
 #include "config.h"
 #endif
 
+#include <unistd.h>
 #include <helper/time_support.h>
 #include <jtag/interface.h>
 #include <transport/transport.h>
@@ -109,6 +110,7 @@ static int setup_sysfs_gpio(int gpio, int is_output, int init_high)
        if (!is_gpio_valid(gpio))
                return ERROR_OK;
 
+    sleep(1);
        snprintf(gpiostr, sizeof(gpiostr), "%d", gpio);
        ret = open_write_close("/sys/class/gpio/export", gpiostr);

After a few tries, it finally exported all those gpio pins, and we were now debugging the Beena:

debian@beaglebone:~/openocd$ ./src/openocd -f ../beena/bbb.cfg -f ../beena/9h00008.cfg
Open On-Chip Debugger 0.12.0+dev-gfc30feb (2023-03-07-23:32)
Licensed under GNU GPL v2
For bug reports, read
	http://openocd.org/doc/doxygen/bugs.html
DEPRECATED! use 'adapter driver' not 'interface'
DEPRECATED! use 'sysfsgpio jtag_nums' not 'sysfsgpio_jtag_nums'
DEPRECATED! use 'sysfsgpio trst_num' not 'sysfsgpio_trst_num'
DEPRECATED! use 'sysfsgpio srst_num' not 'sysfsgpio_srst_num'
SysfsGPIO num: srst = 65
Info : auto-selecting first available session transport "jtag". To override use 'transport select <transport>'.
9h00008.cpu
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : SysfsGPIO JTAG/SWD bitbang driver
Info : Note: The adapter "sysfsgpio" doesn't support configurable speed
Info : JTAG tap: 9h00008.cpu tap/device found: 0x3f0f0f0f (mfg: 0x787 (<unknown>), part: 0xf0f0, ver: 0x3)
Info : Embedded ICE version 1
Info : 9h00008.cpu: hardware has 2 breakpoint/watchpoint units
Info : starting gdb server for 9h00008.cpu on 3333
Info : Listening on port 3333 for gdb connections
Info : accepting 'telnet' connection on tcp/4444

On another terminal, we can read the start of the ROM header mapped in the corresponding memory region:

debian@beaglebone:~# telnet localhost 4444
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Open On-Chip Debugger
> halt
target halted in ARM state due to debug-request, current mode: Supervisor
cpsr: 0x20000013 pc: 0xc00ce8bc
> mdw 0x80000000 10
0x80000000: ea00003e 00000000 c00fff80 ffff8619 00000000 00000000 00800000 00000000
0x80000020: 6564696e 62757267

Now we check the memory region where we expect to find the BIOS:

> mdw 0 50
0x00000000: e59ff038 e59ff038 e59ff038 e59ff038 e59ff038 e1a00000 e59ff034 e59ff034
0x00000020: 6564696e 62757267 68000000 00000000 32303034 31303135 31333030 4c41534a
0x00000040: 00000254 20003fd0 20003fd8 20003fe0 20003fe8 20003ff0 20003ff8 00000000
0x00000060: 00006770 00006798 000067d0 0000686c 000068a4 000068c8 000068d4 000068ec
0x00000080: 00006904 00006998 00006a44 00006a54 00006a94 00006aa0 00006ba0 00006bb8
0x000000a0: 00006bfc 00006c08 00006c28 00006c40 00006c58 00006cec 00006da4 00006de4
0x000000c0: 00006df0 00006e00

> mdw 0x200 50
0x00000200: ea0017c3 ea0017d8 ea0017e0 ea0017f0 ea001801 ea001824 ea001c3c ea001ca5
0x00000220: ea001cae ea001cb1 ea001cba ea001cbc ea001cbd ea001ccb ea001cdf ea001d0f
0x00000240: ea001d20 ea001d31 ea001d5c ea001d72 ea001d7a e59f0148 e321f0d1 e240d000
0x00000260: e321f092 e240df48 e321f0d7 e240df48 e321f0db e240df48 e321f0d3 e240df48
0x00000280: eb000001 eb000018 ea00004f e59f0114 e59f1114 e3a0200c e4903004 e2522001
0x000002a0: e4813004 1afffffb e12fff1e e51ff004 00006740 e51ff004 00006744 e51ff004
0x000002c0: 00006748 e51ff004

If we repeat these memory reads but without inserting a cartridge, we get the same results, excluding the cart’s region which never gets initialized:

> mdw 0x80000000 10
0x80000000: ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff
0x80000020: ffffffff ffffffff

Each memory region has a specific size, and we can’t read past it:

> mdw 0x1fff0 4
0x0001fff0: 00000000 00000000 00000000 00000000
> mdw 0x1fff0 5
memory read caused data abort (address: 0x0001fff0, size: 0x4, count: 0x5)

We get exactly 0x20000 (1024*128) bytes mapped at address 0, and 0x4000 (1024*16) at 0x20000000. These match the sizes reported by the chip’s manufacturer for the on-chip ROM and RAM!

Is this the real deal?

Let’s do a few more checks. We yank the supposed BIOS with dump_image 0.bin 0 0x20000, and look at the first bytes, noticing various 4-byte aligned values:

According to the ARM7TDMI documentation, we should see the exception vector table at address 0x00000000, which seems to be the case:

                     Reset
00000000 e5 9f f0 38     ldr        pc=>LAB_00000254,[PTR_LAB_00000040]
                     UndefinedInstruction
00000004 e5 9f f0 38     ldr        pc=>LAB_20003fd0,[PTR_LAB_00000044]
                     SupervisorCall
00000008 e5 9f f0 38     ldr        pc=>LAB_20003fd8,[PTR_LAB_00000048]
                     PrefetchAbort
0000000c e5 9f f0 38     ldr        pc=>LAB_20003fe0,[PTR_LAB_0000004c]
                     DataAbort
00000010 e5 9f f0 38     ldr        pc=>LAB_20003fe8,[PTR_LAB_00000050]
                     Unused
00000014 e1 a0 00 00     mov        r0,r0
                     IRQInterrupt
00000018 e5 9f f0 34     ldr        pc=>LAB_20003ff0,[PTR_LAB_00000054]
                     FIQInterrupt
0000001c e5 9f f0 34     ldr        pc=>LAB_20003ff8,[PTR_LAB_00000058]
                     s_edinburgh_00000020
00000020 65 64 69        ds         "edinburgh"
         6e 62 75
         72 67 68 00

There are various branches starting at 0x200, aligned with the external addresses seen in game disassemblies:

00000200 ea 00 17 c3     b          LAB_00006114
00000204 ea 00 17 d8     b          LAB_0000616c
00000208 ea 00 17 e0     b          LAB_00006190

If we follow the reset handler, we have the typical stack setup for each execution mode:

                     LAB_00000254
00000254 e5 9f 01 48     ldr        r0=>DAT_20000c4c,[PTR_DAT_000003a4]
                     FIRQ mode | IRQ disabled | FIQ disabled
00000258 e3 21 f0 d1     msr        cpsr_c,#0xd1
0000025c e2 40 d0 00     sub        sp,r0,#0x0
                     IRQ mode | IRQ disabled
00000260 e3 21 f0 92     msr        cpsr_c,#0x92
00000264 e2 40 df 48     sub        sp,r0,#0x120
                     Abort mode | IRQ disabled | FIQ disabled
00000268 e3 21 f0 d7     msr        cpsr_c,#0xd7
0000026c e2 40 df 48     sub        sp,r0,#0x120
                     Undefined mode | IRQ disabled | FIQ disabled
00000270 e3 21 f0 db     msr        cpsr_c,#0xdb
00000274 e2 40 df 48     sub        sp,r0,#0x120
                     SVC mode | IRQ disabled | FIQ disabled
00000278 e3 21 f0 d3     msr        cpsr_c,#0xd3
0000027c e2 40 df 48     sub        sp,r0,#0x120

Remember that ROM header string edinburgh? In function 0x00000868, this signature is checked on 3 memory regions (including the one for game carts):

void FUN_00000868(void) {
  // ...
  edinburgh = PTR_s_edinburgh_00001114;
  uVar7 = read_volatile_4(DAT_60020020);
  uVar7 = uVar7 & 0xff;
  if (((((uVar7 == 0x54 || uVar7 == 0x56) || uVar7 == 0x58) || uVar7 == 0x57) || uVar7 == 0x73) || uVar7 == 0x75) {
    // ...
    iVar6 = strcmp(&UNK_c0000020,edinburgh,0x10);
    if (iVar6 == 0) {
      // ...
      pvVar1 = (void *)read_volatile_4(0xc0000008);
      goto_user_entry(&DAT_c0000000,pvVar1);
      // ...
    }
  }
  // ...
  iVar6 = read_volatile_4(DAT_a0000000);
  if ((iVar6 != 0 && iVar6 != -1) && (iVar6 = strcmp(&DAT_a0000020,edinburgh), iVar6 == 0)) {
    // ...
    pvVar1 = (void *)read_volatile_4(DAT_a0000008);
    goto_user_entry(&DAT_a0000000,pvVar1);
    // ...
  }
  // ...
  if (iVar6 = strcmp(0x80000020,edinburgh,0x10), iVar6 != 0) goto fail;
  // ...
  goto_user_entry((void *)0x80000000,&SP);
fail:
  load_nocart();
  *(uint *)PTR_DAT_0000110c = *(uint *)PTR_DAT_0000110c | 2;
  uVar7 = read_volatile_4(DAT_40000010);
  write_volatile_4(DAT_40000010,uVar7 & 0xfffffffe);
  do {
    /* WARNING: Do nothing block with infinite loop */
  } while( true );
}

When the signature is found, it setups the stack address and jumps to the corresponding entry function in Supervisor mode:

                     goto_user_entry
000002e0 e3 21 f0 13     msr        cpsr_c,#0x13
000002e4 e1 a0 d0 01     mov        sp,r1
000002e8 e1 a0 f0 00     mov        pc,r0

If no region contains that signature (e.g. no cart was inserted), it will show an error screen and end in an infinite loop at 0xdf8. We know that load_nocart() needs to decompress graphics for the error screen, since it calls the same BIOS function dcx() that games branch to (via address 0x214) for their own graphics:

00006530 e5 9f 41 f8     ldr        r4,[->w_nocart] = c0100100
00006534 e5 9f 11 f0     ldr        r1=>nocart_meta,[->nocart_meta] = 0000dda8
00006538 e1 a0 00 04     mov        r0=>w_nocart,r4
0000653c eb ff ff 5a     bl         dcx
; ...
00006594 e5 9f 11 9c     ldr        r1=>nocart_tiles,[->nocart_tiles] = 0000ddd4
00006598 e1 a0 00 04     mov        r0=>w_nocart,r4
0000659c eb ff ff 42     bl         dcx

When a debugger is connected via JTAG, the console doesn’t turn off automatically, which normally happens after a few seconds of showing the error screen. This allows us to halt the CPU at 0xdf8, then take a dump of address 0xc0100100, finding the decompressed tiles used in this screen:

Might as well see the actual rendered tiles. There are 3 byte arrays involved:

  • palette (BGR555) @ 0xdd8c;
  • tiles (compressed palette indexes) @ 0xddd4;
  • metadata (size + compressed tile indexes + flip transforms) @ 0xdda8;

A Unicorn script decompresses tiles and metadata, writing the results to files that are then parsed by another script to render composed tiles:

This was enough to convince me. Next stop, Beena emulation?