01 · Foundations

The Binary: dgf, anatomized

One 8.3 MB ARM ELF contains the entire game: engine, rules, menus, physics and data loaders. It was built with GCC 4.8.3 (Linaro) in a Jenkins job, keeps its full symbol table, and, with one deliberate four-byte exception, runs unmodified on original hardware and on desktop Linux alike.

1.1Identity and build provenance

Everything in this table is measured from the file itself (readelf, nm, Ghidra), not inferred:

PropertyValue
Filedgf — ELF 32-bit ARM EABI5, hard-float (armhf), little-endian, dynamic (/lib/ld-linux-armhf.so.3)
TypeExecutable, non-PIE (fixed load addresses), not stripped
Ghidra image base0x00008000
CompilerGCC 4.8.3 — Linaro crosstool-NG, Jan 2014 (from .comment)
.text size2,651,706 bytes (~2.6 MB) of machine code
Functions12,135 T/t symbols; ~19,258 including thunks
Mangled C++ symbols17,041
Vtable/RTTI symbols3,273 (_ZTV/_ZTI/_ZTS)
C++ exceptionsPresent (.ARM.ex*, .eh_frame, .gcc_except)
Symbols retained~57,000 defined; 458 STT_FILE source-file records
DWARF debug infoPresent but covers only the dongle library (§1.3)

Embedded build paths confirm the origin: /home/inn/wkdir/type3/dgtype3_build_r16/… and /jenkins/workspace/dgf/…: a developer's Linux box feeding a Jenkins CI job. The game and its board image (Linux kernel uImage, 3.4.113, dated Dec 2017) were shipped together on the unit's 8 GB eMMC.

Address convention used in this report The binary is non-PIE: every function and global lives at a permanent address, which makes all addresses stable identifiers. We use Ghidra's normalized Thumb addresses with bit 0 clear (e.g. GameTask = 0x001e718c); nm prints many of the same entry points with bit 0 set (0x001e718d). Both refer to the same instruction stream.

Two shipped binaries, four patched bytes

Two copies of the game exist in this project's corpus, differing at exactly one instruction site: the SW2001 anti-clone dongle check (§1.5):

FileSHA-256State
dgf356d050b…b9773c10Playable — dongle check bypassed (4-byte patch)
dgf.orig688cddb5…cf0041142Stock, byte-exact original

All screenshots and live experiments in this report run the playable binary; everything else is untouched original code.

1.2What the symbol table reveals

Although the game was shipped without debug info for its own code, the linker kept the symbol table, and the 458 STT_FILE records preserve the original source-file names. They expose the module split of the game exactly as its authors organized it:

DomainSource files (verbatim from symtab)
Game logicgame_cond.cpp, game_judge.cpp, game_score.cpp, m_map_*.cpp, gamedata.cpp, saveData.cpp, playerdata.cpp
Menu / UImenu_*.cpp, mn_*.cpp, com_*.cpp, game_sp*.cpp
Train / worldtrainmain.cpp, eng_*.cpp, ev_*.cpp, raillib.cpp, rendmap.cpp
Platform / mediamoviemain.cpp, audiodec.cpp, videodec.cpp, usbdencon.cpp, usbshincon.cpp, usbRJcon.cpp
Aqualead runtimeALSystem*.cpp, ALScreenGLES.cpp, ALMaterialGLES.cpp, ALTextureGLES.cpp, ALSound.cpp, ALPad.cpp, …

The usb*con.cpp names confirm three physical controller families polled through evdev: the consumer mascon/brake unit (dencon), the two-handle cabinet (shincon), and a third-party type-RJ controller, each with its own driver module inside the game.

1.3The debug-info red herring

file dgf reports “with debug_info”. That is technically true and practically useless: the DWARF is ~2.5 KB, and its line tables reference only three files: the vendor library of the SW2001 anti-clone dongle:

sw2001_verify/src/example.c
sw2001_verify/src/interface.c
sw2001_verify/src/sw2001_aes.c
DWARF coverage: the only code in the binary with debug info is the dongle verification library; the entire 2.6 MB game has none.

So: names without types. Every function name is known; no struct layout, field type, or line number is. Everything structural in this report was recovered by decompilation and dynamic verification.

1.4Aqualead: the embedded engine

The game runs on Aqualead, a proprietary Japanese C++ middleware (its AL* classes fill the symtab). The game code does not use Aqualead's higher-level OOP frameworks; its scene graphs, widget, FSM, collision and particle systems are present as compiled code but never instantiated by game logic (decompiled vtable-level analysis across the whole binary). Instead, gameplay is written as plain procedural C-style functions (proc*, in*, out*, entry* ) that use only Aqualead's low-level primitives:

Platform backends follow an Imp naming pattern: one abstract class, one implementation per platform: ALSystemImpGLES (rendering), ALPadImpUnix (input), ALSoundImpOAL (audio), ALStreamImp (files/save). The port shim substitutes hardware at exactly these boundaries.

1.5The boot chain

From execve() to the title screen, control flows through six named stages:

main @ 0x001918c8
  -> ALInit @ 0x001987e0            ; 1280x720 default, ./Data, /var/lib/save, CLI flags
  -> heap + 120,000-byte fiber stack + GLES screen + platform init
  -> ALMain
       SW2001_verify                ; anti-clone dongle AES check (patched)
       DALInit -> MainTask @ 0x00198938
  -> MainTask
       open("/dev/input/event1")    ; power-switch watchdog fd
       load Warning.atx             ; caution screens
       load BG.aar, Effect.aar
       ps_main()
  -> ps_main                        ; task scheduler, controllers, save iface
       motherTask @ 0x001ffa18      ; the permanent 60 Hz frame boundary
         trainMain @ 0x00213a50     ; subsystem init, then task-99 process dispatch
Boot chain: addresses are non-PIE-fixed; names are from the binary's symtab.

The decompiled entry point: note the 120,000-byte fiber stack allocated inside main's own stack frame:

/* DGF process bootstrap. ALInit selects ./Data and /var/lib/save, creates the Aqualead
   heap/fiber and GLES screen, then ALMain starts the game. */

undefined4 main(undefined4 param_1,undefined4 param_2)
{
  ALSystemImp::IsALInit = 1;
  ALSystemImp::ArgParams = param_2;
  ALSystemImp::ArgCount = param_1;
  ALInit();                                          // flags, paths, screen size
  if (ALSystemImp::UseMemorySize == 0) {
    pAVar2 = ALHeapBaseImp::CreateMallocHeap();      // default: malloc-backed heap
  }
  else {
    pvVar4 = malloc(ALSystemImp::UseMemorySize);
    pAVar2 = ALHeap::Create(pvVar4,sVar1);
    ALHeapBaseImp::FreeFunc = 0x386295;
  }
  ALHeap::SetCurrent(pAVar2);
  ALFiber::InitStack(auStack_1d4d0,120000);          // 120 KB fiber stack on main's frame
  ALPropSetImp::Init();
  ALSystemImpGLES::InitOpenGLES(&pvStack_1d4d4);
  /* screen factory guard, ALScreenImp::Entry, ALSystemImp singleton … */
  ALInitPlatform();
  main::simp._1112_4_ = ALFiber::Create((_func_void_void_ptr *)0x3852d9,(void *)0x0);
  ALMain();                                          // dongle check, DALInit, MainTask
  ALSystemImp::ExitStatic();
  return *(undefined4 *)(ALSystem::_Imp + 0x440);
}
Exhibit: main @ 0x001918c8, Ghidra pseudo-C, names restored from symtab.

Command-line flags

ALInit parses dash-flags into globals that gate behavior all over the binary:

FlagGlobalEffect
-480screen-size globalsRender at 854×480 (SetSystemScreenSize(0x356,0x1e0)) instead of hardcoded 1280×720
-undeadUndeadFlagInvincible / no-fail debug mode
-debugdataDebugDataUnlock all diagrams; credits = 99,999,999
-debugjoinJoinDebugFlagDebug join (station-coupling) mode
-gameloopGameLoopFlagAuto-loop gameplay (attract mode)
-menuloopMenuLoopFlagAuto-loop menus (attract mode)

The SW2001 anti-clone dongle

The unit carries a small I²C AES authentication chip; SW2001_verify (in the only DWARF-covered code!) challenges it at boot and refuses to run the game on unauthorized hardware. The four-byte patch in the playable binary turns the verification into return 1 (the classic single-instruction license check bypass. A reimplementation simply omits this step; it has zero effect on gameplay.

1.6How this report was produced

Three instruments, cross-checked:

Methodology diagram: static decompilation, live runs, data autopsy
Methodology: static analysis in two decompilers, dynamic runs through a hardware shim, and byte-level data-format autopsies, each cross-checking the others.
What the live harness looked like The capture rig for this report: an 8.3 MB armhf ELF under qemu-user with a Debian armhf sysroot (Mesa llvmpipe → GLES 2.0 in software), a 100-package rootfs assembled from stock Debian packages, the port shim's faked evdev devices driven by XTEST keyboard events, and a guest-memory probe tool for reading game state live. The title screen below is the stock binary rendering through that stack.
Title screen captured through the emulation stack
Stock dgf title screen through qemu-user + llvmpipe + the shim: the entire game runs unmodified; only the four hardware touchpoints are replaced.

What the shim replaces, andand what that proves

The original hardware exposes exactly four game-facing services. Replacing each one without touching the binary is a live probe of the game's true platform contract:

Original hardwareReplacementWhat it proves
Mali-450 EGL fbdev_windowReal X11 window + host Mesa EGL (only eglGetDisplay/eglCreateWindowSurface intercepted)The game is a bog-standard GLES2 client; its EGL extensions are dead code
Train controller on /dev/input/event1 (brake) + event2 (mascon), polled with ioctl(EVIOCGKEY)Faked evdev devices synthesized from the keyboardInput is a pure key-bitmap contract; the game's own translation tables do the rest
Front-panel buttons (gpio-keys by-path node)Faked nodeVideo thread polls it forever; missing node would kill the process
Allwinner CedarX video stack (12 XPlayer* symbols + 11 DT_NEEDED blobs)libxplayer.so stub that self-completes each movieVideo is fire-and-forget with completion callbacks; nothing in game logic decodes frames

Capture-harness disclosure

Full transparency about how this report's screenshots were produced, because two of the techniques touch game state:

1.7The binary in numbers

MetricValue
Machine code2.65 MB .text
Functions12,135 named (+ thunks ≈ 19,258 analyzed)
C++ symbols17,041 mangled
Vtables / RTTI3,273
Total symbols~57,000 across 458 source files
Debug info for game codenone (2.5 KB DWARF = dongle library only)
Asset tree4,975 files, ~1.6 GB (Chapter 14)
Save payload6,980 bytes, Base64-in-XML, no checksum (Chapter 9)
Why not decompile-and-recompile? Decompiler output is a reading aid, not compilable source: raw pointer-offset arithmetic over pseudo-types, invented locals, CONCAT intrinsics, and gotos. Making even one function recompilable requires recovering its real struct layouts, calling conventions, and globals, multiplied by 12,135 functions and 3,273 vtables. Matching-decompilation projects for far smaller classics took teams years. The realistic path, and the one this report is written to enable, is a clean-room reimplementation against the documented behavior (Chapter 15).
← Overview