Previously in this series of articles, we’ve set up an industrial-grade reverse-engineering setup, complete with servers and extensions. How about we finally start doing something with it?

Where to start?

As can be seen on Copetti’s write-up, the PlayStation’s CPU has access to 2 MiB of RAM as well as 1 KiB of scratchpad RAM. Even taking into account the extra ~1.5 MiB of RAM tucked away inside various subsystems, that’s two orders of magnitude less than a 650 MiB CD-ROM.

Like most CD-based consoles, games on the PlayStation need to juggle data between the RAM (fast but small) and the CD-ROM (large but slow). We’ve found several executables inside Rittai Ninja Katsugeki Tenchu: Shinobi Gaisen besides the one used to start the game, but we don’t know yet how the game uses them.

Let’s figure that out.

Address space recon

On the PlayStation, the RAM is mapped from 0x80000000 to 0x801fffff, with the stack likely located at the end of that range. We also know that the first 64 KiB from 0x80000000 to 0x8000ffff are reserved for the kernel, according to the Run-Time Library Overview of the official PlayStation SDK.

In order to get a feel where stuff is located, let’s build a memory map table of every section for all executables for this game:

Name .rdata .text .data .sdata .sbss .bss
SLPS_019.01 0x80011000
0x800132bb
0x800132bc
0x80043ec3
0x80043ec4
0x8004ccbf
0x8004ccc0
0x8004cda7
0x8004cda8
0x8004cdaf
0x8004cdb0
0x8004cfff
ENDING.EXE 0x80011000
0x800138bf
0x800138c0
0x80047897
0x80047898
0x800507d3
0x800507d4
0x8005091f
0x80050920
0x80050927
0x80050928
0x80050fff
MAIN.EXE 0x80011000
0x80016133
0x80016134
0x80086763
0x80086764
0x80097697
0x80097698
0x80097f87
0x80097f88
0x80097f8f
0x80097f90
0x80097fff
MENU.EXE 0x80011000
0x80013abf
0x80013ac0
0x800503e3
0x800503e4
0x8005b48b
0x8005b48c
0x8005b757
0x8005b758
0x8005b75f
0x8005b760
0x8005b7ff
TRIAL.EXE 0x80011000
0x8001593f
0x80015940
0x80087873
0x80087874
0x800970af
0x800970b0
0x8009798f
0x80097990
0x80097997
0x80097998
0x80097fff
RUN.EXE 0x80110000
0x80110a2b
0x80110a2c
0x8011956b
0x8011956c
0x8011af1f
0x8011af20
0x8011af2f
0x8011af30
0x8011af37
0x8011af38
0x8011afff

The PlayStation does not have a MMU, which means software on it run on physical memory and have full control of the console. Therefore, this memory map is merely indicative ; once it is loaded an executable is not constrained by it.

We can see that every executable is located at the same base address 0x80110000 and overlap each other, except for RUN.EXE which is at a higher base address 0x80110000. In other words, RUN.EXE and only one of the other executables can be loaded at the same time in memory.

That tells us what can co-exist in RAM at the same time, but to find out how we need to pop the hood.

Executable switcharoo

Back in part 2, we’ve determined that Rittai Ninja Katsugeki Tenchu: Shinobi Gaisen starts by booting the executable file SLPS_019.01, as specified by SYSTEM.CNF. Drilling down SLPS_019.01 with Ghidra, we can see that it does the following:

  • Initializes a bunch of stuff, most of which isn’t interesting for now ;
  • Plays \TENCHU\MOVIE\SME.STR (Sony Music Entertainment logo, the publisher) ;
  • Plays \TENCHU\MOVIE\ACQUIRE.STR (Acquire logo, the developer studio) ;
  • Calls a function at address 0x80016e10 with one parameter, 0x10.

The last point is interesting, as this function sets up a data structure at address 0x80100000, like so:

struct RunExeData {
    uint32_t signature; // always 0xdef0c0de
    char path[80];
    void* stack;        // always 0x801ffff0
    uint32_t data;      // always 0x0
};

It then loads \TENCHU\RUN.EXE and hands off execution to it. That program checks the signature of that data structure, then proceeds to load the executable located at path and jump into it.

What about that parameter with the value 0x10? It controls what executable is to be loaded:

Process ID File
0x10 \TENCHU\MENU.EXE
0x11 \TENCHU\MAIN.EXE
0x12 \TENCHU\ENDING.EXE
0x13 \TENCHU\TRIAL.EXE

Long story short, whenever the game wants to switch to another executable, it launches first \TENCHU\RUN.EXE out of the current executable’s way, which then launches the desired executable, overwriting the original one in the process:

Note that SLPS_019.01 isn’t part of the set of executables that can be switched to, which means it serves no purpose once the game has been bootstrapped.

Splitting the game into multiple executables reduces the amount of code and data that resides inside the RAM at any point. For example, the main menu doesn’t need to be loaded in memory at the same time as the current in-game level.

Run-time musical chairs

We’ve ascertained this mechanism through static analysis, but we can also observe it at work live. Here, I hook up GDB to DuckStation, set up a breakpoint that triggers at the entrypoint of RUN.EXE and display out the path at 0x80100004, then play through the first level until I die and go back to the main menu:

$ gdb-multiarch 
GNU gdb (Debian 10.1-1.7) 10.1.90.20210103-git
Copyright (C) 2021 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb) set architecture mips:3000
The target architecture is set to "mips:3000".
(gdb) target remote :1234
Remote debugging using :1234
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0xbfc00000 in ?? ()
(gdb) display (char*)0x80100004
1: (char*)0x80100004 = 0x80100004 ""
(gdb) hbreak *0x801193dc
Hardware assisted breakpoint 1 at 0x801193dc
(gdb) continue
Continuing.

Program received signal SIGINT, Interrupt.
0x801193dc in ?? (warning: GDB can't find the start of the function at 0x801193dc.

    GDB is unable to find the start of the function at 0x801193dc
and thus can't determine the size of that function's stack frame.
This means that GDB may be unable to access that stack frame, or
the frames below it.
    This problem is most likely caused by an invalid program counter or
stack pointer.
    However, if you think GDB should simply search farther back
from 0x801193dc for code which looks like the beginning of a
function, you can increase the range of the search using the `set
heuristic-fence-post' command.
)
1: (char*)0x80100004 = 0x80100004 "\\TENCHU\\MENU.EXE;1"
(gdb) continue
Continuing.

Program received signal SIGINT, Interrupt.
0x801193e0 in ?? (warning: GDB can't find the start of the function at 0x801193e0.
)
1: (char*)0x80100004 = 0x80100004 "\\TENCHU\\MENU.EXE;1"
(gdb) continue
Continuing.

Program received signal SIGINT, Interrupt.
0x801193dc in ?? (warning: GDB can't find the start of the function at 0x801193dc.
)
1: (char*)0x80100004 = 0x80100004 "\\TENCHU\\MAIN.EXE;1"
(gdb) continue
Continuing.

Program received signal SIGINT, Interrupt.
0x801193e0 in ?? (warning: GDB can't find the start of the function at 0x801193e0.
)
1: (char*)0x80100004 = 0x80100004 "\\TENCHU\\MAIN.EXE;1"
(gdb) continue
Continuing.

Program received signal SIGINT, Interrupt.
0x801193dc in ?? (warning: GDB can't find the start of the function at 0x801193dc.
)
1: (char*)0x80100004 = 0x80100004 "\\TENCHU\\MENU.EXE;1"
(gdb) continue
Continuing.

Program received signal SIGINT, Interrupt.
0x801193e0 in ?? (warning: GDB can't find the start of the function at 0x801193e0.
)
1: (char*)0x80100004 = 0x80100004 "\\TENCHU\\MENU.EXE;1"
(gdb) continue
Continuing.

It’s quite janky (GDB breaks twice on the breakpoint and spits out warnings), probably because DuckStation’s GDB stub hardly gets used and is therefore subject to regressions. Despite that, we can still observe the transitions:

  • From SLPS_019.01 to MENU.EXE, after the two company logos are displayed ;
  • From MENU.EXE to MAIN.EXE, after selecting the first level ;
  • From MAIN.EXE to MENU.EXE, after hitting Start on the game over screen.

Where’s my Grand Master rank?

We’ve uncovered the mechanism the game uses to switch from one executable to another, using a fixed buffer at a predetermined address to keep track of what to launch. However, this does not cover any data that the game probably wants to keep around across executables, like settings, level scores or a ninja’s inventory items.

There are ways to figure that out, but an easy hint comes from the GameShark codes that deal with the records screen and the items selection screen. All of them target addresses just beyond 0x80010000, which is right after the kernel reserved memory but before the game’s executables.

Now that we know where to look, we can see what’s up with these memory addresses inside the executables. Looking at MAIN.EXE, one of the very first things it does is call a function at address 0x8002328c. That function clears a buffer 3696 bytes long at 0x80010000 with memset() and then initializes a whole bunch of values there.

Figuring out the contents of that global structure would require further analysis outside the scope of this part, but the GameShark codes descriptions makes it obvious that this is a memory block used for book-keeping important variables, safely outside the lifecycle of the executables. Additionally, there are similar patterns of memory references across the executables, further substantiating that theory.

Conclusion

We’ve shed some lights on how the address space of the game is structured at a very high level:

  • 0x80000000-0x8000ffff: the kernel reserved memory, off-limits to the game ;
  • 0x80001000-0x80010e70: important data that the game wants to keep track of outside of the various lifecycles of the executables ;
  • 0x80011000-0x80097fff: the current executable ;
  • 0x80100000-0x8010005c: the inter-executable buffer for passing parameters to RUN.EXE ;
  • 0x80110000-0x8011afff: the RUN.EXE executable.

This is not an authoritative memory map, just an indicative one. In particular, it does not contain any information about memory range lifetimes.

For example, it’s possible that the in-game executable uses the memory range of RUN.EXE to store level data, as it is unused until RUN.EXE must be loaded in order to switch to another executable.

We’ve also found how the game juggles between its various executables and how it keeps important data across them.