The Binary: dgf, anatomized
1.1Identity and build provenance
Everything in this table is measured from the file itself (readelf, nm,
Ghidra), not inferred:
| Property | Value |
|---|---|
| File | dgf — ELF 32-bit ARM EABI5, hard-float (armhf), little-endian, dynamic (/lib/ld-linux-armhf.so.3) |
| Type | Executable, non-PIE (fixed load addresses), not stripped |
| Ghidra image base | 0x00008000 |
| Compiler | GCC 4.8.3 — Linaro crosstool-NG, Jan 2014 (from .comment) |
.text size | 2,651,706 bytes (~2.6 MB) of machine code |
| Functions | 12,135 T/t symbols; ~19,258 including thunks |
| Mangled C++ symbols | 17,041 |
| Vtable/RTTI symbols | 3,273 (_ZTV/_ZTI/_ZTS) |
| C++ exceptions | Present (.ARM.ex*, .eh_frame, .gcc_except) |
| Symbols retained | ~57,000 defined; 458 STT_FILE source-file records |
| DWARF debug info | Present 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.
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):
| File | SHA-256 | State |
|---|---|---|
| dgf | 356d050b…b9773c10 | Playable — dongle check bypassed (4-byte patch) |
| dgf.orig | 688cddb5…cf0041142 | Stock, 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:
| Domain | Source files (verbatim from symtab) |
|---|---|
| Game logic | game_cond.cpp, game_judge.cpp, game_score.cpp, m_map_*.cpp, gamedata.cpp, saveData.cpp, playerdata.cpp |
| Menu / UI | menu_*.cpp, mn_*.cpp, com_*.cpp, game_sp*.cpp |
| Train / world | trainmain.cpp, eng_*.cpp, ev_*.cpp, raillib.cpp, rendmap.cpp |
| Platform / media | moviemain.cpp, audiodec.cpp, videodec.cpp, usbdencon.cpp, usbshincon.cpp, usbRJcon.cpp |
| Aqualead runtime | ALSystem*.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
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:
- Fibers (
ALFiber) — cooperative mini-threads; the whole game is a set of fibers yielding to each other at 60 Hz. - Draw packets (
ALDrawPacketGLES) — a recorded command stream interpreted by a dedicated render thread. - Textures / sprites / sounds (
ALTexture,ALSprite,ALSound) — thin handles over GLES/OpenAL. - Archives and compression (
ALArchiveImp,ALLzDecoder) — the asset pipeline (Chapter 14).
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
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);
}
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:
| Flag | Global | Effect |
|---|---|---|
| -480 | screen-size globals | Render at 854×480 (SetSystemScreenSize(0x356,0x1e0)) instead of hardcoded 1280×720 |
| -undead | UndeadFlag | Invincible / no-fail debug mode |
| -debugdata | DebugData | Unlock all diagrams; credits = 99,999,999 |
| -debugjoin | JoinDebugFlag | Debug join (station-coupling) mode |
| -gameloop | GameLoopFlag | Auto-loop gameplay (attract mode) |
| -menuloop | MenuLoopFlag | Auto-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:
- Ghidra (with the ReVa MCP bridge) — primary decompiler; 19,258 functions analyzed; prior round-trip annotations preserved in the program database.
- Binary Ninja (BNDB) — second decompiler; every load-bearing claim in this report was verified to agree, and disagreements are flagged where they matter.
- Live runs — the stock binary executes on desktop Linux under a small
LD_PRELOADshim that replaces only the hardware touchpoints (§below). The game is not modified: menus, physics, scoring and rendering run original code, and every screenshot in this report comes from those runs.
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 hardware | Replacement | What it proves |
|---|---|---|
Mali-450 EGL fbdev_window | Real 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 keyboard | Input is a pure key-bitmap contract; the game's own translation tables do the rest |
| Front-panel buttons (gpio-keys by-path node) | Faked node | Video 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 movie | Video 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:
- Emulation — the stock armhf binary runs under qemu-user with a Debian
armhf sysroot (Mesa llvmpipe, stock OpenAL) at 0.5–10 fps. A guest-memory
peek/poke tool (guest base +
/proc/pid/mem) reads and writes game globals live (the same technique documented for the port project, used here to verify every address quoted in this report against the running game. - Caution-screen skip — the three boot caution screens are pure timed
fades (
DispBG: 60+240+60 frames each, no key read). The harness holds the-menuloopflag global (0x6c874c) high throughMainTask's check, which skips them (a documented flag path, not a code patch). - Departure assistance — the audio-completion deadlock (Chapter 6) was
released by writing
GameConditionBk/gameStatus[0], and two captures used the-undeaddebug flag (a shipped command-line option) so penalty spam during high-speed positioning could not end the run. The physics, judgments, HUD and rendering in every screenshot are unmodified game code reacting to real inputs; some of those inputs were memory writes instead of lever movements.
1.7The binary in numbers
| Metric | Value |
|---|---|
| Machine code | 2.65 MB .text |
| Functions | 12,135 named (+ thunks ≈ 19,258 analyzed) |
| C++ symbols | 17,041 mangled |
| Vtables / RTTI | 3,273 |
| Total symbols | ~57,000 across 458 source files |
| Debug info for game code | none (2.5 KB DWARF = dongle library only) |
| Asset tree | 4,975 files, ~1.6 GB (Chapter 14) |
| Save payload | 6,980 bytes, Base64-in-XML, no checksum (Chapter 9) |
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).