DoDonPachi SaiDaiOuJou Reverse Engineering Notes

The best way I found to work on SDOJ was to run the JTAG/RGH version on Xenia, while using Cheat Engine to debug it. The game is composed of 3 exectuable XEX files: default.xex which contains the main menu (and probably some I/O logic), CA022100.bin which contains ca022.exe (the arcade version of the game) and CA022110.bin which contains ca022_AR.exe (the 360 arranged version of the game).

Before doing any work, it is useful to apply the 1.01 Title Update which removes some of the unintended slowdown. Normally, the 360 applies this update at runtime, every time the game is started. This can be done in xenia-canary by placing the title update file (TU_11LK1V7_000000G000000.0000000000084) in the %USERPROFILE%\Documents\Xenia\content\435A07E7\000B0000\ directory.

However, for reverse engineering and patching purpuses, it is convenient to apply the update directly to the game files in a persistent way. To do this, I used dd and xextool. First, I extracted the xex patches out of the title update. These can be found by searching for the XEX2 string in the update file, and then splitting it with dd:

dd if=TU_11LK1V7_000000G000000.0000000000084 of=TU_11LK1V7_000000G000000.0000000000084.49000.xex bs=4096 skip=$((0x49))
dd if=TU_11LK1V7_000000G000000.0000000000084 of=TU_11LK1V7_000000G000000.0000000000084.2C000.xex bs=4096 skip=$((0x2c)) count=$((0x49-0x2c))
dd if=TU_11LK1V7_000000G000000.0000000000084 of=TU_11LK1V7_000000G000000.0000000000084.D000.xex bs=4096 skip=$((0xd)) count=$((0x2c-0xd))

Then, to apply the patches (backup your files before doing this):

xextool -p TU_11LK1V7_000000G000000.0000000000084.D000.xex -l -e u -c u CA022100.bin
xextool -p TU_11LK1V7_000000G000000.0000000000084.2C000.xex -l -e u -c u CA022110.bin
xextool -p TU_11LK1V7_000000G000000.0000000000084.49000.xex -l -e u -c u default.xex

Note that I added the -e u -c u flags to output decrypted and uncompressed XEX files, which can later be reverse engineered and modded.

Now that the game files are permanently updated, it is necessary to remove the title update from the 435A07E7\000B0000 directory (or simply rename the directory).

SDOJ Memory Map

Here are the most important pieces of the memory used by SDOJ:

0x42070030~0x4284002f: Heap memory (allocated by malloc)
0x82000000~0x820fffff: default.exe - header (from default.xex)
0x82100000~0x825bffff: default.exe - code
0x825c0000~0x827affff: default.exe - data
0x8227b000~0x8283ffff: default.exe - resources
0x88000000~0x8802ffff: ca022.exe - header (from CA022100.bin)
0x88030000~0x8819ffff: ca022.exe - code
0x881a0000~0x889bffff: ca022.exe - data

Xenia Memory Map

0x080000000~0x08fffffff: Functions entry points lookup table (PPC → x86)
0x0a0000000~0x0a0ffffff: x86 code (JIT compiled from PPC)
0x000000000~0x1ffffffff: Xbox 360 memory
  0x142070030~0x14284002f: Heap memory (allocated by malloc)
  0x182000000~0x1820fffff: default.exe - header (from default.xex)
  0x182100000~0x1825bffff: default.exe - code
  0x1825c0000~0x1827affff: default.exe - data
  0x18227b000~0x18283ffff: default.exe - resources
  0x188000000~0x18802ffff: ca022.exe - header (from CA022100.bin)
  0x188030000~0x18819ffff: ca022.exe - code
  0x1881a0000~0x1889bffff: ca022.exe - data
  0x190000000~0x19fffffff: mirror of 0x180000000~0x18fffffff, but writeable

Even though it uses a PPC64 processor, the Xbox 360 uses 32-bit address space. This allows Xenia to map the entire Xbox memory map in the range 0x100000000~0x200000000.

PPC code is JIT compiled to x86, and placed in the range 0xa0000000~0xa1000000. Every PPC function entry point is then stored at the address of the function itself. So for example, if a PPC function starting at 0x8222a9ac is compiled to x86 and placed at 0xa0011c30, then address 0x8222a9ac will contain 30 1c 01 a0 (little endian for 0xa0011c30)

Note that the position of these compiled x86 functions is not predictable.

Reverse Engineering in Ghidra

While it is possible to import the basefiles extracted from the xex files using xextool, I recommend dumping as much memory as possible from xenia using Cheat Engine and then importing it in Ghidra. Note that the Cheat Engine memory dumps include a 23 bytes header before the actual dumped memory data, which must be skipped when importing the files in Ghidra.

I also made a small Ghidra script to import Cheat Engine dumps of xenia into an already existing Ghidra database.

Reverse Engineering

The following addresses are relative to the Xbox 360 memory map - to obtain the addresses relative to the Xenia process, simply add 0x100000000 to each address.

Functions

0x88051460~0x880521db void main()

Main function, runs the entire game. Main loop starts at 0x88051da8, game is rendered at 0x8805219c. Then, the main loop waits at 0x880521d4 until it's time to create the next frame.

0x8217cdd0~0x8217ce53 uint32_t get_input(void *arg1, uint32_t arg2, uint32_t arg3)

This function fetches inputs directly from the Xbox 360 controller. The first argument is a pointer to some kind of structure which appears to always be the same across every call. The other 2 arguments appear to be indices or identifiers of the input that is being fetched.

0x880e34e8~0x880e3583 void *append_sprite_batch_entry(int sprite_index, int x, int y, int layer)

Appends a sprite to the batch of the sprites that will be rendered next frame. Returns a pointer to the entry in the sprite batch.

0x88034038~0x88034183 void render_sprite_batch()

Renders all the sprites that were appended to the sprite batch, and clears the sprite batch for the next frame.

Data

0x886115dc enemy_bullets

This address contains a pointer to a huge structure (293924 bytes) that holds all the enemy bullets. The structure itself is allocated somewhere randomly in 0x42070030~0x4284002f and the allocation is done at 0x880c6b14.

Patching the game

The easier way to make an entry point for a patch is to replace an function call (BL instruction).

For example, say we want to have an entry point at 0x88192800 which gets called once per frame. We could patch the instruction at 0x88052198, BL 0x88034038, to jump to 0x88192800.

Since BL is a relative jump, we need to find the relative offset of the entry point, which in this case is 0x88192800 - 0x88052198 = 0x140668. BL +0x140668 is encoded as 48 14 06 69, so we place those 4 bytes at 0x88052198.

The entry point for our patch could look like this:

/*
# Put these lines in the linker script to define these functions
render_sprite_batch = 0x88034038;
sdoj_savegpr_14 = 0x88048450;
sdoj_restgpr_14 = 0x880484a0;
*/


start: # Entry point for the patch
    mfspr 12,8  # Copy LR to R12
    bl sdoj_savegpr_14  # save GPRs r14-r31 and LR
    stwu 1,-0x100(1)  # push the stack frame (256 bytes)
    bl patch  # custom function which will be called once per frame
    bl render_sprite_batch  # call the function that we replaced
    addi 1,1,0x100  # pop the stack frame (256 bytes)
    b sdoj_restgpr_14  # restore GPRs r14-r31 and return

The code above uses 2 functions, sdoj_savegpr_14 and sdoj_restgpr_14, which are found in ca022.exe and are often used to push a backup of all registers on the stack. I wasn't able to use the _savegpr_14 and _restgpr_14 functions generated by GCC because they use a different calling convention and only backup 32-bit registers.

Hitboxes patch

I developed an example patch which allows you to toggle the visualization of some hitboxes by pressing the "back" button (key Z in Xenia): SaiDaiOuJou Hitbox Viewer Patch. It shows how C code can be compiled for PPC and injected into to game.

The patch can be compiled using powerpc-none-eabi-gcc under cygwin. First, edit the Makefile to change the path to the unpatched and patched CA022100.bin, then compile and install the patch as follows:

make clean
make
make install

The Makefile uses md5sum to check if the MD5 hash of the unpached file matches the expected hash of the updated, uncompressed, unencrypted CA022100.bin.