The Symbian & N-Gage Bible

Symbian OS 9.3 and N-Gage 2.0 under the hood, read from a real Nokia 5320 ROM: every claim checkable at an address, every table generated from evidence.

46findings — 31 established, 8 measured, 7 cross-reference
42 of 269execs established
25struct layouts, 160 fields
31walls crossed

How to read this

Notes from building an N-Gage / Symbian OS 9.3 emulator against a real Nokia 5320 ROM, with Metal Gear Solid Mobile as the integration test. Every claim here was established by disassembling shipped binaries — ekern.exe, EUser.dll, efile.exe, EFSrv.dll and the game's own images — and each carries the address you can check it at. What the documentation says was never the source; where it agrees, it is cited as a second opinion.

One rule shapes everything below: a value with no evidence does not exist. Facts live in relational tables with a required evidence column (emulation/ngage/db/os/*.tsv), code is generated from them, and audits scan the live emulator for the silent-fail patterns that make an emulator look like it works — a default 0, a swallowed exception, a switch with a non-throwing default. The reward is defined so that fabrication cannot score: progress means the recorded halt in db/frontier.json moved and new evidence rows justify it. A test that goes green because something started returning a plausible number is not progress.

Sources rank in a fixed order. First the real binaries: the ROM, ekern.exe, EUser.dll, efile.exe and the game's images. Second the machine ISA oracle, LLVM's ARMv6 decoder, for what an instruction is. Third, and only ever as a second opinion, EKA2L1 and published documentation: a cross-reference can corroborate or contradict a value, it can never be the source of one.

Every count in this reference travels with the method that produced it, and the method is the part to read before contradicting the number. That rule was learned the hard way: a re-measurement of the ROM's image-header count omitted one filter of the recorded method, got a different number, and publicly called a correct measurement bogus. Both numbers were right; they answered different questions.

Where something is still unknown, it says so. Those gaps are the honest part, and the last section lists them.

The second half of this page is projected mechanically from the evidence tables the emulator is generated from: every exec the kernel dispatches with its handler address, every struct layout, every server and its ops, every wall the boot has hit and how it was crossed, and the game's own object layouts. It grows with every frontier, without anyone editing prose.

established
Read out of the shipped binary, with the address and the instructions given.
measured
Observed in a run of the real ROM or over the real files, reproducible by re-running it.
cross-reference
Corroborated against EKA2L1 or public sources. Never the source of a value here, only ever a second opinion.

Images and loading

TRomImageHeader is 120 bytes, not 128

established

Every XIP module in a Symbian ROM is preceded by a TRomImageHeader of 0x78 bytes, immediately followed by its code. Get the size wrong and the image still lands at the right address — but every VA read out of the header is skewed.

Check it at
  • 5320.rom: for each of EUser.dll, libc.dll, libz.dll, libdl.dll, libpthread.dll and ekern.exe, code_address − (0x80000000 + the file offset of the module's first 96 bytes) = 120
  • EUser export ordinal 196 = 0x801A18D3 (CTrapCleanup::New) and ordinal 585 = 0x801B2D35 (the function issuing svc #0x9f, ROM word 0x8019C318 = EF00009F)
  • mgs_ngi.exe code offset 0x51FC — the import slot holding 0x249 = 585
  • the misread that hid it: the handler of slow exec 0x3 is at 0x800822A8 (ldr r0,[r0,#0x28] ; bx lr); 0x800822B0 (mov r0,#0xf ; bx lr) is exec 0x4's

Both sweep numbers are right; they are one sweep with and without one filter. This paragraph is worth reading twice, because a draft of it got that wrong in an instructive way. The project's own struct.tsv records 1,425 with the uid2 filter written out beside it. A re-measurement omitted that filter, got 2,881, and concluded the 1,425 came from an unstated method that does not reproduce — then drew the moral that a number must travel with its method. The method was travelling with the number. It went unread, and a correct measurement was publicly called bogus on the strength of a sweep that answered a different question. Carrying the method is necessary; reading it before contradicting it is the other half, and it is the half that failed.

Why it hides. The earlier model here used a 128-byte header plus a compensating ROM_CODE_SHIFT of 8. The two cancel — codeAddress − 128 + 8 equals codeAddress − 120 — so the memory picture was already correct and the emulator ran. What was wrong was every VA read out of the header: the export directory was located 8 bytes too far into the file, two export slots off, so ordinal N silently resolved to exports[N+1]. All 95 firmware imports of the game were wrong; euser ordinal 1360 landed on 0x801B05C5, which happened to return 0, instead of the real UserHeap::SetupThreadHeap at 0x801B2D01. An emulator that calls a wrong-but-valid function does not crash; it misbehaves somewhere else entirely, much later.

The instrument was poisoned too. The disassembly tool carried the same 128-byte constant, so it printed the bytes 8 ahead — which is how the evidence table came to state that slow exec 0x3 is mov r0,#0xf ; bx lr, the handler of exec 0x4. The real handler is ldr r0,[r0,#0x28] ; bx lr, RChunk::MaxSize. An evidence column is only as good as the tool that produced it; audit the instrument against a second, independent path — here a flat disassembly straight over 5320.rom, which needs no header at all, plus the live dispatch table read out of the ROM.

Cross-checks after the fix: ordinal 196 resolves to 0x801A18D3, the CTrapCleanup::New the kernel model already called successfully, and ordinal 585 to 0x801B2D35, the function that issues svc #0x9f — 585 being exactly the euser ordinal mgs_ngi.exe imports at code offset 0x51FC. Ordinals 196, 585, 891, 1360, 1600 and 2060 all land on a Thumb push prologue with no fixup scan, which retired the old ±12-byte prologue search that had been papering over the misread. Removing the shift dropped the run from 5,963 to 340 instructions, and every later gain is real work.

Counts, with the method beside each
valuewhatmethod
2,881ROM image headers whose code_address equals headerVA + 0x78, with zero at any other delta — 2,554 DLL and 327 EXE headers, 2,531 distinct UID3sscan every 4-byte offset of 5320.rom whose uid1 is 0x10000079 or 0x1000007A; keep those whose code_address is headerVA + 0x78; every survivor also carries a codeSize that fits inside the ROM
1,425the same sweep with a second predicate — uid2 in {0x1000008D, 0x1000008B}, the standard EXE/DLL second UIDs — 1,361 of them DLLs; this is the number db/os/struct.tsv records, with the filter written out beside itas above, plus the uid2 filter
6 of 6standalone sys/bin modules whose first 96 bytes occur exactly once in the 68 MB ROM, each at a header VA exactly 120 bytes before its code_addressbyte search of each module file's first 96 bytes over 5320.rom
Sources
  • emulation/ngage/db/os/struct.tsv — romimageheader
  • emulation/ngage/db/os/struct_field.tsv — romimageheader.*
  • emulation/ngage/db/frontier.json — history: null function-pointer call at backend.dll 0x82e462a8
  • emulation/ngage/web/src/cpu/rom.js — HDR = 0x78

An E32 image's export directory is at a file offset

established

E32ImageHeader::iExportDirOffset is an offset from the start of the uncompressed image, not from the code section.

Check it at
  • MetalGearSlibdl.dll (UID3 0xF01C9970, codeBase 0x8000) inside the MGS <Game> SIS at !:\sys\bin\MetalGearSlibdl.dll — read the directory at exportDirOffset and at codeOffset + exportDirOffset and count the entries inside [codeBase, codeBase + codeSize)
  • a ROM module's directory is the same shape: EUser's at 0x801B4F04 (2,229 entries), libc's at 0x82E816CC (560); ordinal N is the word at +4·(N−1), bit 0 marks Thumb

One reading is coherent, the other is 97 percent coherent — precisely the kind of nearly-right that survives casual testing and breaks on the tail.

The entries are linked virtual addresses, so ordinal N is exports[N−1], plus the relocation delta if the image was loaded somewhere other than its linked base. An ordinal outside the table is refused rather than answered with 0 into the caller's import slot — that silent 0 is how the earlier frontier came to be inflated by 154 imports that returned nothing.

Counts, with the method beside each
valuewhatmethod
1,310 of 1,310export entries inside the code section when the directory is read at exportDirOffsetloader/e32.js getExports over the inflated image; each entry compared against [codeBase, codeBase + codeSize)
1,272 of 1,310entries inside the code section when the directory is read at codeOffset + exportDirOffset insteadthe same count under the other interpretation
Sources
  • emulation/ngage/db/frontier.json — history: unimplemented import metalgearslibdl@3
  • emulation/ngage/web/src/loader/e32.js — getExports
  • emulation/ngage/db/os/struct_field.tsv — romimageheader.export_dir_address, export_dir_count

Only one field distinguishes a decompressed E32 from a compressed one

measured

A deflated E32 image carries compressionType = 0x101F7AFC at offset 0x1C and an uncompressedSize at 0x7C. A tool that decompresses an image in place zeroes the compression word and leaves everything else alone.

Check it at
  • E32ImageHeader offset 0x1C (compressionType) and 0x7C (uncompressedSize)
  • the DLL inflated out of the MGS <Game> SIS (34,395 bytes deflated, uncompressedSize 57,692, 57,848 bytes flat) against samples/from-hexenwerkzeug/metalgearslibdl_decompressed.bin

The whole code section is byte-identical. Useful corollary: if you already parse the container, you never need the salvaged copy — the container is the source.

Counts, with the method beside each
valuewhatmethod
4 of 57,848bytes that differ between the container's inflated MetalGearSlibdl.dll and the separately salvaged decompressed copy — offsets 0x1C to 0x1F, the compression word: FC 7A 1F 10 in the inflated image, zero in the salvaged oneinflate the DLL from the <Game> SIS with loader/e32.js flatImage, then byte-diff against the salvaged file
Sources
  • emulation/ngage/db/frontier.json — history: unimplemented import metalgearslibdl@3
  • emulation/ngage/web/src/loader/e32.js — flatImage

Writable data does not live at data_address

established

For an XIP module, TRomImageHeader.data_address points at the read-only initialised-data template in ROM. The module's writable .data and .bss live at data_bss_linear_base, header offset 0x60 — a page-aligned RAM address distinct per module — and total_data_size at 0x6C is exactly data_size + bss_size.

Check it at
  • TRomImageHeader +0x18 data_address, +0x24 data_size, +0x28 bss_size, +0x60 data_bss_linear_base, +0x6C total_data_size
  • libc 0x3FFB7000, libm 0x3FFB6000, libdl 0x3FFB5000, LibCIpcClient 0x3FFBF000, backend.dll 0x3FFC0000, ekern 0xC8000000 (= TRomHeader.kern_data_address); EUser, libz and libpthread, which have no data, carry the placeholder 0x00400000
  • libc total 0x7138 = 0x908 + 0x6830; libdl 0x204 = 0xC + 0x1F8; backend.dll 0x8C4 = 0x10 + 0x8B4; ekern 0x1C84 = 0x1F0 + 0x1A94
  • the null member the boot once died on, 0x3FFC0094, dereferenced by code at 0x82E462A8 — both inside backend.dll: its data window [0x3FFC0000, +0x8C4) and its code

The old loader zeroed bss at data_address — for libc that is immediately followed by libc's own TRomDllRefTable in ROM, so the loader was wiping the module's dependency table as if it were bss. The clincher for the real location came from the frontier itself: the word the ledger recorded as a null member sits inside backend.dll's window and the faulting code sits inside backend.dll's code. The module was reading its own uninitialised globals.

Sources
  • emulation/ngage/db/os/struct_field.tsv — romimageheader.data_address, data_bss_linear_base, total_data_size
  • emulation/ngage/db/frontier.json — history: null function-pointer call at backend.dll 0x82e462a8

The codeseg dependency walk marks before it recurses

established

DCodeSeg::TraverseDeps at ekern 0x800874D8 marks a codeseg, then recurses into its dependencies, then appends itself: deps first, self last. Marking first is load-bearing, because a module's TRomDllRefTable can name the module itself and a mark-after-recursion walk never terminates.

Check it at
  • ekern 0x800874D8 DCodeSeg::TraverseDeps; exec 0x9F's handler 0x80089994 walks DProcess::iCodeSeg (+0x108) through it
  • TRomDllRefTable: flags (u16) at +0, count (u16) at +2, TRomImageHeader* entries from +4; libc's table at 0x82E86320 (first entry 0x82E460E0 = backend.dll), libz 0x82E460C4, libpthread 0x82E885EC, libdl 0x82E99B88, backend.dll 0x82E55D2C; 0 for EUser, LibCIpcClient, libm and ekern
  • a ROM module's entry point: tst pc,#0 ; ldr ip,[pc,#4] ; bx ip ; padding ; the _E32Dll address — EUser 0x80199078, libc 0x82E63FB8, libz 0x82E3BDD8, libdl 0x82E99458, libpthread 0x82E863B8

Nothing about DCodeSeg is materialised in guest memory: it is kernel-private, so modelling it in RAM would be invention. Only TRomImageHeader and TRomDllRefTable get struct rows, because only they are actually read from memory. Dedupe by header VA when walking a table — it can name the module itself.

The veneer loads r12 itself and needs no register setup, so it must be called verbatim, never at +8. Calling it at +8 lands on the bx ip with r12 unset — the exact shape of an experiment this project ran and refuted before the veneer was read.

Counts, with the method beside each
valuewhatmethod
8entry points exec 0x9F publishes for mgs_ngi.exe: seven firmware DLL veneers and the EXE lastrun-rom.mjs asserts exec 0x9F is dispatched exactly once with count 8 and capacity 256, and that all seven DLL veneers execute exactly once each with r0 = 2
3modules the walk reaches that exist only in the ROM and not as standalone sys/bin files: backend.dll, LibCIpcClient.dll, libm.dllfollow each module's own TRomDllRefTable from the five sys/bin firmware libraries
1,422 of 1,425 and 1,359 of 1,361ROM images whose entry_point equals code_address, and DLLs that begin with the identical self-contained veneer; the two exceptions, 0x8376EDA0 and 0x83F74030, start with Thumb code and are exactly the two DLLs whose entry_point differs from code_addressthe uid1 + uid2 header sweep, reading each header's entry_point and first four code words
Sources
  • emulation/ngage/db/os/struct_field.tsv — romimageheader.entry_point, romdllreftable.*
  • emulation/ngage/db/os/exec-impl.tsv — slow 0x9f
  • emulation/ngage/web/src/kernel/codeseg.js

XIP versus RAM: where a game's own DLLs live

cross-reference

Firmware modules are XIP — mapped in place from the ROM, executed where they lie, exports read straight out of the image. A game's own DLLs ship inside the game container as ordinary relocatable E32 images the loader places in a RAM code chunk; where they land is the loader's allocation decision, not a property of the binary.

Check it at
  • mgs_ngi.exe and MetalGearSlibdl.dll (UID3 0xF01C9970) are both linked at codeBase 0x8000 and overlap; the DLL's own 2,664-byte relocation block is what moves it
  • EKA2L1 trace C:\tools\eka2l1\traces\mgs-2026-09-02.log: a code chunk at 0x70000000 for mgs_ngi.exe (max 0x2B000) and 0x70100000 for metalgearslibdl.dll (0xE000)
  • this project's slot allocator, loader/ramcode.js: 0x70000000 + (n+1) × 0x100000, so MetalGearSlibdl.dll lands at 0x70100000 with codeDelta 0x700F8000

For MGS the <Game> SIS carries mgs_ngi.exe plus five further binaries, including MetalGearSlibdl.dll. Both the EXE and that DLL are linked at 0x8000, so one must be relocated, which is what the image's own relocation block exists for. A relocated image carrying data or bss is refused rather than placed: its writable-data run address is not modelled and would have to be invented.

Two independent emulators that agree on placement produce traces that line up instruction for instruction, which turns 'does this look right' into 'where is the first divergence'. That is why an independently chosen slot allocator landing on EKA2L1's address matters — not because the address is a fact about the device.

Counts, with the method beside each
valuewhatmethod
5ordinals mgs_ngi.exe imports from MetalGearSlibdl.dll, a dlopen/dlsym shimthe EXE's import block for metalgearslibdl, read by loader/ramcode.js
10instructions retired inside the DLL's mapped span on the first run that entered it (37,330 → 41,695 instructions overall)run-rom.mjs asserts ramSteps > 0, so a regression that stops entering the DLL fails the gate
Sources
  • emulation/ngage/db/frontier.json — history: unimplemented import metalgearslibdl@3
  • emulation/ngage/web/src/loader/ramcode.js
  • emulation/ngage/tools/oracle-diff.mjs

Metal Gear Solid Mobile's code is not in mgs_ngi.exe

established

The E32 image the SIS installs at !:\sys\bin\mgs_ngi.exe is a 175 KB Symbian loader stub. The game lives in mgs_ngi.xe3u under the private directory — an XE3U/S3E image whose code byte 0 loads at VA 0x100000, with code and rodata to 0x1ED98C, .data to 0x1F244C and .bss to 0x21B700.

Check it at
  • samples/mgs/extracted/game/drive_!/private/20010925/mgs_ngi.xe3u — the XE3U header's section table
  • load base 0x100000 (db/game/mgs/image.tsv); code + rodata 0x100000..0x1ED98C, write-protected by the loader; .data to 0x1F244C; .bss to 0x21B700

Every game-side address in this reference is an HLE VA of that image, and every one is checked by two arbiters. The binary: tools/mgs_re.py tables --check simulates the instruction range each derive column names through a symbolic Thumb evaluator and fails on any size, offset, address or vtable that differs from the stored column. The relations: tools/audit-game.mjs checks schema, keys and evidence, requires every cited VA to lie inside the image's sections, walks every watch chain to its declared type, and exits 2 NOT MEASURED when the binary is absent rather than passing over it.

A row that cannot be evidenced from the binary is deleted, not kept as static-only. Nothing in those tables has been observed live: the game has not run past its front end under either engine.

Sources
  • emulation/ngage/db/game/mgs/image.tsv
  • emulation/ngage/tools/mgs_re.py
  • emulation/ngage/tools/audit-game.mjs

The game imports eleven DLLs from three different places, and not one of them is a graphics library

established

mgs_ngi.exe names eleven DLLs in its import block: seven are XIP firmware inside the ROM, three ship in the N-Gage launcher's own SIS, and one, MetalGearSlibdl.dll, travels in the game's container. None of the game's six binaries imports libgles_cm, Ws32 or m3gcore — whatever draws the frame, the shipped game does not link against it.

Check it at
  • mgs_ngi.exe, the Marmalade S3E loader stub: Thumb, 0x2AE38 bytes of code at base 0x8000, file offset 0x9C; the import block names ArenaEnv{2000106d} ordinals 1 and 2, euser 23 imports, libc 60, MetalGearSlibdl[f01c9970] ordinals 1, 2, 3, 4 and 1308, libpthread 31, libz ordinals 33 to 36, runtime{2000107a} ordinals 9 to 12, ngiutil{2000106b} ordinal 14, dfpaeabi 13, drtaeabi 52, drtrvct2_2 12
  • the XIP modules, TRomEntry to TRomImageHeader: euser 0x80199000, efsrv 0x801EBC50, libc 0x82E63F40, libpthread 0x82E86340, libz 0x82E3BD60, libdl 0x82E993E0, DRMCommon 0x816A1A20, DrmRights 0x816B2890, apgrfx 0x80A10A70, caf 0x802656B0, drtaeabi 0x8018DFA0 (code 0x8018E018), dfpaeabi 0x801918E0 (code 0x80191958), drtrvct2_2 0x801942C0 (code 0x80194338), scppnwdl 0x80198E60, PlatformEnv 0x82EC10E0, ApMIME 0x80A071A0, Bafl 0x805567C0, CHARCONV 0x80339470, ESTLIB 0x80534D80
  • samples/mgs/extracted/game/drive_!/sys/bin — mgs_ngi.exe 179,640 bytes uid3 0x20010925; ngifilemanagerfactory_20010925.dll 763,380 (0x20010926); MetalGearSlibdl.dll 34,395 (0xF01C9970, 1,310 exports); MGSfileaccess.dll 23,880 (0x20003802); MetalGS_Crack.dll 4,547 (0xF4F06529, 122 exports, a caf.dll proxy); AS_20010925.exe 5,029 (0xE7B8FA33, capabilities TCB and SwEvent)
  • the RVCT runtime modules drtaeabi, dfpaeabi, drtrvct2_2 and scppnwdl carry uid3 0 and exist only inside the ROM: they are neither among the extracted sys/bin files nor separable from one another by a uid3-to-name map
  • ArenaEnv.dll as the launcher installs it is a 340-byte stub with 2 exports and no imports at all; ngiNAF.dll carries the same uid3 0x2000106D
importwhat mgs_ngi.exe takeswhere the module lives
euser{000a0000}23 ordinalsXIP, TRomImageHeader 0x80199000
libc{00010000}60XIP, 0x82E63F40
libpthread{00010000}31XIP, 0x82E86340
libz{00010000}ordinals 33 to 36XIP, 0x82E3BD60
dfpaeabi13XIP, 0x801918E0, uid3 0
drtaeabi52XIP, 0x8018DFA0, uid3 0
drtrvct2_212XIP, 0x801942C0, uid3 0
ArenaEnv{2000106d}ordinals 1 and 2launcher SIS — 340 bytes, 2 exports, no imports; ngiNAF.dll carries the same uid3
ngiutil{2000106b}ordinal 14launcher SIS — 1,430 bytes, 29 exports
runtime{2000107a}ordinals 9 to 12launcher SIS, inside an embedded controller — 10,471 bytes, 41 exports
MetalGearSlibdl[f01c9970]ordinals 1, 2, 3, 4 and 1308the game's own container — 34,395 bytes, 1,310 exports

Three of the eleven are not on the device at all until the N-Gage launcher is installed, which is why a title cannot be booted from its own container alone. And runtime.dll does not even come out of the launcher package by the obvious route: it sits in an embedded controller, which the container part below covers.

The stub is the only thing that touches the platform. The game's own code — the XE3U image the stub unpacks — imports its EGL and s3e entry points from the stub by name, so the block above is the complete static picture of what the shipped game links against, and it contains no graphics library.

One caution about the uid3-0 modules. drtaeabi, dfpaeabi, drtrvct2_2 and scppnwdl are RVCT runtime pieces with no third UID, so any tool that identifies a ROM module by uid3 merges them with each other and with anything else carrying 0 — this reference nearly published a thunk attributed to a wholly unrelated DLL that way. Attribute them by header VA.

Counts, with the method beside each
valuewhatmethod
11DLLs mgs_ngi.exe imports — 23 ordinals from euser, 60 from libc, 52 from drtaeabi, 5 from MetalGearSlibdlparseHeader, flatImage and parseImports from web/src/loader/e32.js over each of the six binaries in the game container's sys/bin
0 of 6game binaries importing libgles_cm, Ws32 or m3gcorethe same sweep, reading every import block after inflation — the images are deflated, so a byte search of the files on disk finds nothing and proves nothing
7, 3 and 1of the eleven: XIP firmware (euser, libc, libpthread, libz, dfpaeabi, drtaeabi, drtrvct2_2), launcher-SIS modules (ArenaEnv, ngiutil, runtime) and the game's own container (MetalGearSlibdl)each name looked up in the ROM header sweep of 5320.rom, in samples/launcher/installed/sys/bin and in the container's own sys/bin
Sources
  • emulation/ngage/web/src/loader/e32.js — parseImports
  • emulation/ngage/db/game/mgs/image.tsv
  • emulation/ngage/samples/launcher/PROVENANCE.md

The kernel and its executive

The fast exec table is indexed count-first, and fast exec 0 never touches it

established

ekern's SVC vector decodes the immediate with lsls ip,ip,#9 — carry is bit 23 (a fast exec), zero means the number is 0 — branches fast exec 0 to Exec::WaitForAnyRequest before any table is read, and indexes the fast table with word 0 as the count and word n as the handler of exec n. The slow table keeps its count at base − 0xC and eight-byte {flags, handler} entries.

Check it at
  • ekern 0x80068120 __ArmVectorSwi: 0x80068130 lsls ip,ip,#9 ; 0x80068134 blo slow ; 0x80068138 ldr r1,[fp,#0x130] (TScheduler.iCurrentThread) ; 0x8006813C beq 0x80068708
  • 0x80068140 ldr r2,[r1,#0x60] (NThread::iFastExecTable) ; 0x80068148 ldr r3,[r2],ip,lsr #7 ; 0x8006814C ldr r2,[r2] ; 0x80068150 cmp r3,ip,lsr #9 ; 0x80068154 bxhi r2
  • slow path: 0x80068174 ldr r4,[sb,#0x64] ; 0x80068180 add r6,r4,ip,lsr #6 ; 0x80068184 ldr r5,[r4,#-0xc] ; 0x8006818C ldmhi r6,{r5,r6} ; 0x80068190 ldrls pc,[r4,#-8] ; 0x800681C8 ldrne pc,[r4,#-4] when flags bit 29 is set
  • the user-thread tables: fast at 0x80069E1C (count 0x1C), slow base 0x80069E98 (count 0xF1 at base − 0xC, invalid-number handler 0x800700A0 at base − 8, argument preprocessor 0x8006A6C4 at base − 4); installed by DThread::DoCreate from the literals 0x8009380C / 0x80093810 (0x80092A3C..0x80092A44) and NThreadBase::Create 0x8006F384..0x8006F3A8
  • the preprocessor's pseudo-handles: KCurrentThreadHandle 0xFFFF8001 → the current DThread at 0x8006A748; KCurrentProcessHandle 0xFFFF8000 → the current DProcess at 0x8006A75C

The previous, hand-made version of the table walked the fast table from word 0, so every fast row carried the next exec's handler: its 'fast 0x0 = 0x8006A674' is exec 1 (the thread allocator, DThread+0x48), its 0x8006A684 and 0x8006A68C are 5 and 6 (ActiveScheduler get and set), 0x8006A694 and 0x8006A69C are 8 and 9 (TrapHandler get and set), and its 'fast 0x1B = 0xF1' is the slow table's count word. Reading the vector instead of trusting the table is what settled it.

A slow entry's flags word says how the argument preprocessor treats r0 before the handler runs: 0xE0000003 resolves a chunk handle to its DChunk, 0xE0000001 a thread handle, 0xE0000002 a process handle, 0xB0000009 a session handle. That is why a handler like slow 0x4C can open with ldr r4,[r0,#0x20] — r0 is already the kernel object.

Counts, with the method beside each
valuewhatmethod
28 fast and 241 slowexec numbers the user-thread tables dispatchtools/gen-exec-table.mjs walks both tables exactly as the vector does and HALTs if any anchor instruction is not the one cited; the result is docs/data/ekern-exec-table.tsv, re-read from the ROM
Sources
  • emulation/ngage/docs/data/ekern-exec-table.tsv — header
  • emulation/ngage/db/os/exec-impl.tsv — fast 0x00, fast 0x01, fast 0x02, slow 0x26, slow 0x51
  • emulation/ngage/db/frontier.json — history: fast exec 0x0 Exec::WaitForAnyRequest

User::WaitForRequest is a fast-semaphore decrement, and every completion ends in ThreadRequestSignal

established

Fast exec 0 decrements NThread::iRequestSemaphore.iCount and returns at once while the result stays non-negative; otherwise it blocks the thread on that semaphore and reschedules. The only wake-ups are the semaphore's Signal and SignalN, reached from NKern::ThreadRequestSignal — slow exec 0x3B, and the tail of every request completion.

Check it at
  • ekern 0x80068708 wait_for_any_request, eleven instructions: ldr r2,[r1,#0x20] ; subs r2,r2,#1 ; str r2,[r1,#0x20] ; bxge lr ; then iKernCSLocked (TScheduler+0x10C) = 1, iRescheduleNeededFlag (+0x108) = 1, iWaitObj (NThread+0x28) = the semaphore, iNState (NThread+9) = 2 EWaitFastSemaphore, bl 0x800687AC (unready), b 0x80068C40 (reschedule)
  • 0x80068C40 saves SP at NThread+0x68 and runs the head of the highest-priority ready queue: a two-word bitmap at TScheduler+0, queues at +8 + 4p, new entries linked at the tail by 0x800688A8..0x800688B4
  • NFastSemaphore::SignalN 0x80068750 and Signal 0x8006880C; NKern::ThreadRequestSignal 0x8006EA50; slow exec 0x3B at 0x80082BA8 is mov r1,r0 ; mov r0,#0 ; b into it
  • an IPC completion: Exec::MessageComplete 0x8006AB9C unlinks the message from the session queue, decrements iMsgCount and DThread+0x20, then DThread::RequestComplete 0x8006E2AC writes the reason into *aStatus and signals
  • euser User::WaitForRequest 0x801A4594: mvns r4,#0 ; loop: adds r4,r4,#1 ; blx 0x8019BD58 ; ldr r0,[r5] ; cmp r0,r6 ; beq loop — spinning on KRequestPending, literal 0x801A48C0 = 0x80000001
  • Exec::ThreadCreate 0x800839FC; EpocThreadFunction 0x8006A868 (0x8006A89C..0x8006A920 sets the first user context); DProcess::iReentryPoint at +0x118, stored by 0x80091E00; NKern::ThreadResume 0x8006E5D4; DThread::Resume 0x80093828

That loop is why an emulator cannot fake a completion by returning from the exec: the thread re-reads the status word and waits again until something writes it. The model in web/src/kernel/threads.js reproduces exactly this — thread contexts, the request semaphore, a FIFO ready queue and a reschedule that HALTs as scheduler-idle when nothing is ready, naming the blocked thread, its semaphore count and the queued message only a server could complete. Before the file server existed at the session boundary, the run halted there with the semaphore at −1 and *aStatus still KRequestPending, both asserted in run-rom.mjs.

Thread creation was re-derived on the way. Exec::ThreadCreate validates iTotalSize (total − 0x30 below 0xD1 and 8-aligned) and iUserStackSize (at least 0x200), else −6; forces type 3 EThreadUser, priority −5 EThrdPriorityNormal and no supervisor stack; and the new thread's first user context is fixed by EpocThreadFunction: the stack filled with 0x29, the info copy at the stack top with SP = top − iTotalSize, r4 = 1 and PC = DProcess::iReentryPoint, the codeseg entry point. It is created ESuspended with iSuspendCount −1 until NKern::ThreadResume brings it to 0, and no context switch happens on resume — the current thread keeps running until it blocks.

Sources
  • emulation/ngage/db/os/exec-impl.tsv — fast 0x00, slow 0x3b, slow 0x67, slow 0x28
  • emulation/ngage/db/os/struct_field.tsv — nthread.*, tscheduler.*, nfastsemaphore.*, sthreadcreateinfo.*
  • emulation/ngage/web/src/kernel/threads.js

A synchronous send returns before the server answers, and its return value is not the reply

established

Exec::SessionSendSync validates, records aStatus in the thread's embedded RMessageK without ever dereferencing it, queues the message on the server and returns 0. euser presets the TRequestStatus to KRequestPending, returns the exec's error only when it is non-zero, and otherwise waits — so RSessionBase::CreateSession's result is the status word the server later writes, not the exec's return.

Check it at
  • ekern 0x8006AC80 Exec::SessionSendSync: ldr r4,[r0,#0x20] reads DSession::iServer, the field DSession attach writes at 0x8008A3AC ; 0x8006AC84 cmn r1,#2 → −6 KErrArgument ; 0x8006AC8C..0x8006ACA0 → −15 KErrServerTerminated for a null server or one whose iAccessCount (+8) is 0
  • the message: 0x8006AC94 sub ip,sb,#0x1D4 against 0x8006AD54 sub sb,sb,#0x2C0 is DThread+0xEC, the RMessageK embedded in the thread; 0x8006AC98 ldr r7,[ip] ; cmp r7,#0 ; 0x8006ACB8 bne 0x80089E50 = mov r0,#0x1E ; b 0x8008D468 — KERN-EXEC 30 when a sync message is still outstanding: a panic, not an error return
  • 0x8008D468 is the panic helper: mov r2,r0 (code) ; sub r0,r0,#0x2C0 (current thread) ; mov r1,#2 (category) ; b 0x80092638
  • 0x8006ACA8..0x8006ACB4 store aSession, aFunction and aStatus at msg +0x20, +8 and +0x30 — aStatus is only recorded
  • KConnectMessage (−1) at 0x8006ADC8: DSession+0x4C and +0x2C must both be null, else panics 9 and 0x39 (0x80089DE0 / 0x80089DE8); the message is stored at +0x4C (0x8006ADE0)
  • tail 0x8006AD50..0x8006ADC4: iMsgCount (+0x34) +1, the client DThread's +0x20 +1, the client stamped at msg+0x2C, the message appended to DSession+0x3C/+0x40, then ldmne r2,{r1,r3,r6,r7,r8} ; stm r2!,{r1,r3,r6,r7} ; strh r8,[r2] — four argument words and a halfword of type flags, which euser's wrapper preloads at 0x8019C590
  • delivery 0x8006AE44: four instructions of SDblQue append onto DServer+0x34/+0x38 while DServer+0x28 (a thread blocked in Receive) is null; the non-null hand-off at 0x8006DFAC MMU-switches to the receiver and is not RE'd
  • euser send wrapper 0x801A6C58: presets KRequestPending (literal 0x801A6F0C = 0x80000001, iFlags 2), returns a non-zero exec result directly, otherwise calls User::WaitForRequest 0x801A4594 and returns ldr r0,[sp] — the status

The key finding removes a worry that had stalled the file server for a session: implementing this exec faithfully does not mean fabricating the server's reply. KErrNone here is the firmware's own answer to the send, and the status was deliberately left pending. What the run asserts permanently: exec 0x4C dispatched exactly once; the server's queue holding exactly one message with function −1, on the session exec 0x7E minted, held simultaneously in DSession+0x4C and in the thread's sync slot with iMsgCount 1; the TIpcArgs type-flags halfword reading 0; and the guest word at *aStatus still reading 0x80000001 at the halt, so no reply was invented.

Sources
  • emulation/ngage/db/os/exec-impl.tsv — slow 0x4c
  • emulation/ngage/db/frontier.json — history: slow exec 0x4c Exec::SessionSendSync
  • emulation/ngage/web/src/kernel/servers.js

The ROM header names its two boot files, and the file server is the second

established

TRomHeader carries iPrimaryFile at +0xA0 and iSecondaryFile at +0xA4, each a TRomEntry whose iAddressLin is a real TRomImageHeader VA: ekern.exe and efile.exe. The F32 file server is the first user-side process the kernel starts. Its server name is not typed anywhere in this project — it is read out of efile.exe's own TLitC16 — and the kernel's check on a leading '!' is the protected-server convention.

Check it at
  • TRomHeader +0xA0 → TRomEntry 0x800458E0 {iSize 0x37CB8, iAddressLin 0x80067F88, iNameLength 9, 'ekern.exe'}; +0xA4 → 0x800462F0 {iSize 0x205D4, iAddressLin 0x801CB600, 9, 'efile.exe'}
  • TLitC16 0x801E7EC0 inside efile.exe: length 11, '!FileServer'; the only other occurrence of that UTF-16 text in the ROM is 0x801F2FC0 in EFSrv.dll (header 0x801EBC50), the client library whose RFs::Connect 0x801EC4AE hands it to RSessionBase::CreateSession
  • efile.exe 0x801D2D74 loads the literal into a TFindServer and 0x801D2D80 calls TFindServer::Next; 0x801D2A6E allocates the server (0x70 bytes, priority 0x3E8) and 0x801D2AAA..0x801D2AB0 calls CServer2::StartL with r1 = [0x801D2BB8] − 0x38 = this literal
  • ekern's container-add helper 0x8008C22C has twelve call sites; r1 = 7 (EServer) at exactly one, 0x8008A8E0, inside slow exec 0x7D Exec::ServerCreate 0x8008A80C, which does ldrb r0,[r0] ; cmp r0,#0x21 at 0x8008A850 and writes aMode to DServer+0x3C at 0x8008A8A8 — the byte DSession attach reads back at 0x8008A37C and Exec::SessionShare compares at 0x8008A6B8
  • Exec::SessionCreate 0x8008A928: two 0x50-byte TKName scratch buffers, container index 7, DObjectCon::FindByName 0x8008BD64 (skips iAccessCount ≤ 0 at 0x8008BE10 and a null name at 0x8008BE1C, matches with TDesC::Match 0x800992C0, −1 KErrNotFound at 0x8008BEBC); DSession::New 0x8008A27C bounds aMsgSlots to [−1, 0x200] and aMode to 0..2, allocates 0x60 bytes and stores aMode at +0x32; 0x8008A424 adds the session to container 8 and 0x80090DE0 returns the new handle
  • TRomHeader is 0x120 bytes: +0x7C iRestartVector (5320.rom holds 0xEA0006DF there; ekern's restart path 0x80071B9C..0x80071BA8 jumps to it), +0x80 iTime, +0x8C iRomBase 0x80000000, +0x90 iRomSize 0x04200000, +0x94 iRomRootDirectoryList 0x800458D0, +0x98 kernel data 0xC8000000, +0x9C kernel limit 0xC9100000, +0xCC iVariantFile 0x80045950, +0xD0 iExtensionFile 0x800458FC, +0xF4 iUncompressedSize 0x04193000 = the byte length of 5320.rom, +0x100 eight trace-mask words copied to the superpage at kernel entry 0x80072A8C..0x80072AA4

web/src/kernel/servers.js verifies every structural expectation around the name before registering the server — that iSecondaryFile really names efile.exe, that its uid1 really is 0x1000007A, that the literal lies inside that image, that the text is eleven characters and '!'-prefixed — and HALTs otherwise. A server can therefore only be registered when the ROM itself declares it.

Every case Exec::SessionCreate does not fix in the firmware HALTs rather than defaulting: a non-null aPolicy (the six in-range arms of the TSecurityPolicy validator 0x8009A66C are unread), aMsgSlots above 0 (the dedicated message pool at 0x8008A2FC is unmodelled), aMode above 0, a wildcard name (only the '*' comparison of TDesC::Match 0x80099048 has been read), and a state with no ROM behind its memory.

Counts, with the method beside each
valuewhatmethod
2occurrences of the UTF-16 text '!FileServer' in the whole ROMsearch all 68,759,552 bytes of 5320.rom for the UTF-16LE encoding
12call sites of ekern's container-add helper 0x8008C22C, of which exactly one passes container 7 EServersweep of every bl to 0x8008C22C in ekern's code, reading r1 at each
0x40000029the first session handle the boot receives, which the very next exec — 0x4C with function −1 — carries: the boot went through RFs::Connect, not around itrun-rom.mjs asserts the handle, the EServer container holding exactly one object sourced from efile.exe, and the following exec's arguments
Sources
  • emulation/ngage/db/os/struct.tsv — tromheader, tromentry
  • emulation/ngage/db/os/struct_field.tsv — tromheader.*, tromentry.*
  • emulation/ngage/db/os/hle-server.tsv
  • emulation/ngage/db/os/exec-impl.tsv — slow 0x7e
  • emulation/ngage/web/src/kernel/servers.js

Both euser and ekern decode a descriptor's type nibble through the flags register

established

A TDesC's first word holds the type in bits 31..28 and the length in bits 27..0. The decoders in EUser.dll and ekern.exe turn that nibble into NZCV with eor r1,r1,r1,lsr #1 ; msr cpsr_f,r1 and pick the data pointer with conditional instructions — so an emulator whose msr cpsr_f is a silent no-op decodes every TLitC to the word after it.

Check it at
  • euser 0x8019DDF4: ldr r1,[r0],#4 ; cmp r1,#0x50000000 ; eor r1,r1,r1,lsr #1 ; msr cpsr_f,r1 ; addcs / ldrle / addeq
  • ekern 0x8006B934, the head decoder Kern::KUDesGet uses: 0x8006B944 bic ip,r3,#0xF0000000 masks the length; a type word of 0x50000000 or more panics the calling thread at 0x8006B954; 0x8006B960 ldrths ip,[r0],#4 reads iMaxLength only when C is set — types 2 EPtr, 3 EBuf and 4 EBufCPtr
  • the boot's live TBuf8 {0x3000000B, 0x50, '!FileServer'} decodes to type 3 EBuf

A TLitC — what _LIT expands to — is layout-identical to type 0 EBufC, so the same accessor serves both. The max-length word exists only for types 2, 3 and 4; reading it for a type 0 or 1 descriptor reads the first characters as a length.

How it hid: the emulator's MSR class matched the instruction and did nothing, so the flags survived from the preceding cmp (N set), ldrle fired, and every literal decoded wrong; UserHeap::SetupThreadHeap rejected its chunk name with KErrBadName. This is the fidelity scanner's blind spot worth naming — it catches a default 0 and a no-throw default, but a bare return on a decoded-and-recognised instruction reads as deliberate. The regression now replays the real euser sequence.

Counts, with the method beside each
valuewhatmethod
5TDesType layouts the flag trick distinguishes: 0 EBufC, 1 EPtrC, 2 EPtr, 3 EBuf, 4 EBufCPtrweb/src/kernel/descriptor.js reproduces 0x8006B934 instruction for instruction and is exercised on all five
Sources
  • emulation/ngage/db/os/struct.tsv — tdesc
  • emulation/ngage/db/os/struct_field.tsv — tdesc.type_length, tdesc.max_length
  • emulation/ngage/web/src/kernel/descriptor.js
  • emulation/ngage/db/frontier.json — history: null function-pointer call at backend.dll 0x82e462a8

Loading a DLL is a lock protocol, and the exec table hides it

established

Symbian's own SVC-name tables list exec 0x9B as E32Loader::ProcessCreate. It is nothing of the kind: exec 0x9B acquires, and exec 0x9C releases, the calling process's DLL lock — a DMutex at DProcess+0x114 whose name in the ROM is literally 'DLL$LOCK'. The whole 0x98..0xA1 family is this one loader protocol.

Check it at
  • the stub: euser 0x8019C2F8 is a bare svc #0x9b ; bx lr with every register left over from the caller, so the exec takes no arguments
  • ekern 0x80089704 (0x9B): ldr r0,[pc,#-0x988] → [0x80088D84] = 0xC800043C, TheScheduler ; ldr r0,[r0,#0x130] iCurrentThread ; sub r0,r0,#0x2c0 the embedding DThread ; ldr r4,[r0,#0x30]! iOwningProcess with write-back ; add r0,r0,#0x108 → DThread+0x138 ; bl 0x8009D2D8 SDblQue::IsEmpty ; moveq r0,#0x25 → KERN-EXEC 37 when the queue is not empty ; tail-call 0x80087C18: LockSystem 0x80068BA8, ldr r0,[r4,#0x114], bl 0x800865A8 (DMutex::Wait), UnlockSystem 0x80068B3C
  • exec 0x9C is the identical handler with panic 43, tail-calling 0x80087C3C, which signals the same mutex via 0x80085BF4 (DMutex::Signal, this table's slow 0x12)
  • 'DLL$LOCK' at 0x8009E280, order 0xFE, created by DProcess::Create 0x800917C4 through K::MutexCreate 0x80082024; its sibling at DProcess+0x110 is '$LOCK' at 0x8009E274, order 0x48
  • euser proves the pairing: the loader-side function at 0x801A78FE issues svc 0x9b at 0x801A793E, tests the result, and issues svc 0x9c at 0x801A7964 on the failure path
  • the correction was already on disk: exec-impl.tsv's slow 0x11 row records ekern 0x80082438 b 0x800865a8 as DMutex::Wait — months before 0x9B was looked at

The disassembly says so three ways at once: the stub takes no arguments, the handler creates nothing, and euser calls it and its neighbour as a matched pair. Both handlers first require the doubly-linked queue at DThread+0x138 — libraries pending detach — to be empty, and panic KERN-EXEC 37 (acquire) or 43 (release) otherwise.

The trap that hid it is a single character.

ldr r4, [r0, #0x30]!      ; write-back
add r0, r0, #0x108

The exclamation mark makes r0 become DThread+0x30, so the +0x108 that follows addresses DThread+0x138, not DProcess+0x108 as it reads at a glance. DProcess+0x108 is iCodeSeg and is genuinely never touched here. One mis-read addressing mode turns a lock protocol into a plausible-looking accessor — which is exactly what happened in this project's own notes before the disassembly was redone. The lesson generalises past Symbian: check the table you are about to extend.

The model: a DMutex on the process, recursive on the same thread (DMutex::Wait increments iHoldCount), with the pending-library queue kept as a real list so the KERN-EXEC 37/43 precondition is checked rather than skipped. The blocking leg — a lock held by another thread — HALTs rather than pretending to acquire.

Sources
  • emulation/ngage/db/os/exec-impl.tsv — slow 0x9b, slow 0x9c, slow 0x11, slow 0x12
  • emulation/ngage/db/os/struct_field.tsv — dprocess.process_lock, dprocess.dll_lock, dthread.pending_library_q
  • emulation/ngage/db/frontier.json — history: unimplemented exec 0x9b at 41,865 instrs

exec 0x8E is the loader's library create-or-open, and the handle it returns names a DLibrary

established

Slow exec 0x8E, ekern 0x80088F64, takes a 16-byte in/out block — a DCodeSeg pointer, a thread handle, an out-slot for the handle, and an owner selector — allocates or reuses a 0x40-byte DLibrary for the calling process, attaches the codeseg closure and returns the handle in the block. euser-svc-map's name for it, E32Loader::CodeSegClose, is refuted: it creates.

Check it at
  • euser 0x8019C290: the raw word 0xEF00008E = svc #0x8e followed by bx lr (verified with rompeek, closing the efile → euser chain)
  • ekern 0x80088F64; the block is copied in at 0x80088F7C and back out at 0x800891B4; r0 returns a TInt error and the handle rides in the block: w0 = DCodeSeg*, w1 = a thread handle, w2 = out handle, w3 = owner selector — 0 puts the handle in the process table [DProcess+0x48], non-zero in the thread table [DThread+0x40] with bit 0x40000000 stamped in
  • euser passes tldrinfo+0x0C, which it sets to 1 at 0x801A791E, so the game's library handles are thread-owned with bit 30 set
  • the DLibrary: 0x40 bytes allocated at 0x80088B70, vtable [0x80088D80] = 0x8009F894, owner the client DProcess, registered in the {DCodeSeg*, DLibrary*} array at DProcess+0x74; state byte at +0x24 (3, or 1 when the codeseg's +0x54 has bit 0x02000000); its DCodeSeg at +0x30
  • 0x8009D018 is DObject::Open; the thread it opens is closed again at 0x80089190..0x800891A0 through vtable+8
  • the two handle resolvers differ in register convention: [0x8006B410] = 0xE51F20A8 ldr r2,[pc,#-0xa8] then 0xE090C000 adds ip,r0,r0 — handle in r0, r1 the type, resolved against TheScheduler's current thread; [0x8006B2AC] = 0xE091C001 adds ip,r1,r1 — handle in r1
  • the create branch 0x800890D0 requires [DThread+0x166] == 3 and returns −13 otherwise; a reuse hit never reads it
  • 0x80 is the validator's window for a DCodeSeg — a bound check at 0x80091138 and a 0x80-byte copy at 0x8008847C — not its size

Those two fields, the state byte at +0x24 and the codeseg at +0x30, are exactly what exec 0x9D reads after looking the handle up as object type 3. That settles a question the previous frontier had left open: object type 3 is this library container.

The core survived an adversarial pass; several supporting details did not, and they are recorded so that nobody implements them. '0x8E closes nothing' is wrong — it opens the thread and closes it again. The two handle resolvers are not the same shape. The [DThread+0x166] == 3 check is not a precondition on the whole exec, only on the create branch. 0x80 is a window, not sizeof — any struct row must say 'RE'd extent only'. And efile's request offsets are +0x48, +0x4C, +0x54 and +0x58, not +0x44; the copy direction at 0x801DFC06 is not yet settled.

Sources
  • emulation/ngage/db/frontier.json — current (exec 0x8e, established 2026-09-03)
  • emulation/ngage/docs/data/euser-svc-map.tsv — slow 0x8e, the refuted name

exec 0x9D is the entry-point list of one freshly loaded library

established

Slow exec 0x9D, ekern 0x80089774, takes a library handle, a capacity and an out-buffer, requires the process DLL lock to be held by the calling thread, and returns one entry-point word per codeseg of the library's dependency closure, deps first — exec 0x9F's list for a single library instead of the whole process. The client then calls each with EDllProcessAttach.

Check it at
  • euser 0x8019C30C issues it once the loader has returned the library handle; the client's walker is at euser 0x801B2B8E
  • ekern 0x80089774: ldr r6,[r0,#-0x290] takes iOwningProcess (NThread − 0x290 = DThread+0x30); ldr r0,[r6,#0x114] ; ldr r0,[r0,#0x3c] ; cmp r0,r4 ; bne requires the DLL$LOCK DMutex at DProcess+0x114 to be held by the current DThread; DThread+0x138 is re-checked with SDblQue::IsEmpty, KERN-EXEC 38 (0x26) when neither that nor [sp+0xc] holds
  • mov r2,#3 ; mov r1,r5 ; mov r0,r4 ; bl 0x8006B2AC looks arg0 up in the current thread's handle table as object type 3, KERN-EXEC 0 on failure; ldrb r0,[r4,#0x24] reads the state byte compared against 3; ldr r2,[r4,#0x30] takes the DCodeSeg
  • the return path 0x80089824..0x80089898: a state of 3 skips to the tail; otherwise an SDblQue head is self-linked on the stack (str sp,[sp] ; str sp,[sp,#4]), 0x80087BC4 builds the list from (DProcess, &queue, r2, 2), the count is compared with the caller's capacity at [sp+0xc] — 0x27 = KERN-EXEC 39 when longer — a count of 0 or less sets the state to 3, otherwise 0x80088798 copies the list out to the caller's buffer, 0x80087884 releases it and the state becomes 2
  • 0x80087BC4 calls 0x800874D8, DCodeSeg::TraverseDeps; 0x80088798 walks the queue, takes the object at entry − 0x10 (the link sits at +0x10) and writes one 4-byte word per entry from object+0x24 via 0x8006AFF0 (mov r2,#4) — DCodeSeg+0x24, the entry point, the same word exec 0x9F publishes
  • the deferred-codeseg-delete guards the handler brackets its work with: 0x8006EC84, 0x80087304, 0x80087328, 0x8006ECE4

This is lock semantics proven by instructions, not by a name: the handler dereferences the mutex's owning-thread word and compares it with the current thread. It corroborates the DLL-lock reading of execs 0x9B and 0x9C from an independent site, and it constrains the model — the handle the loader hands back must name an object with a state byte at +0x24 and a codeseg at +0x30, which is what exec 0x8E's DLibrary provides.

The slice that follows is mechanical, and it is one slice, not four: map the resolved library as a codeseg (hal.dll, the first library the game asks for, is XIP), give the DLibrary its state byte and its codeseg, and answer 0x9D with the closure's entry points, the capacity check and the state transition to 2. It was deliberately not implemented before the return value was read, because a guessed number in the one place the client checks is exactly the fabrication the tables exist to prevent.

Sources
  • emulation/ngage/db/frontier.json — current (exec 0x9d)
  • emulation/ngage/db/os/exec-impl.tsv — slow 0x9b, 0x9c, 0x9f
  • emulation/ngage/web/src/kernel/codeseg.js — CodesegTable._attach

exec 0x9F runs the static constructors; exec 0xA0 walks a detach queue that is usually empty

established

Constructors come from slow exec 0x9F, ekern 0x80089994, which publishes the entry points of the process's codeseg closure off DProcess::iCodeSeg, deps first and the EXE last, once per process. Slow exec 0xA0, ekern 0x80089A84, walks the pending-library-detach queue at DThread+0x138 and returns −25, KErrEof, when it is empty — the faithful answer for a process that has closed no RLibrary.

Check it at
  • ekern 0x80089994 (0x9F): 0x800899B8 ldr r0,[r4,#0x2c] ; 0x800899BC tst r0,#0x10 ; 0x800899C0 bicne r0,r0,#0x10 — a one-shot bit consumed on the first call; 0x800899C4 mvneq r5,#0x14 = −21 when already consumed; the bit is set on a process's initial thread at 0x80091AC0 orr r1,r1,#0x12 into [DThread+0x2C]
  • ekern 0x80089A84 (0xA0): 0x80089AE4 mvn r4,#0x18 = −25 loaded as the default; 0x80089AE8 add r0,r5,#0x138 ; bl 0x8009D2D8 = SDblQue::IsEmpty (ldr r1,[r0] ; cmp r1,r0 ; movne r0,#0 ; moveq r0,#1); 0x80089AF0 cmp r0,#0 ; bne — returns −25 exactly when the queue is empty; the ldr r0,[r6,#0x114] ; ldr r0,[r0,#0x3c] ; cmp r0,r5 at 0x80089AB8 is the loader-thread validity check that panics 0x28, not a list head
  • the queue is self-linked empty by the DThread constructor at 0x800927F8 add r1,r0,#0x138 and fed only by DLibrary::Close 0x80088CEC..0x80088D08
  • euser: the static-init runner 0x801A6174 loops StaticCallList → run constructors → StaticCallDone until either returns −25 (adds r0,#0x19 ; beq); the 0xA0 wrapper 0x801A617C passes r0 = 3; the 0x9F caller is ordinal 585 at 0x801B2D35; each entry point is called with r0 = 2 EDllProcessAttach and the last, the EXE, is skipped
  • ROM 0x8019C310 svc #0x9f, 0x8019C318 svc #0xa0, 0x8019C320 svc #0xa1 — an 8-byte-stride table of ARM wrappers, each exactly once

This corrected a ledger entry that had been wrong for a year. The 2025 note said exec 0xA0 walks a codeseg init-list and built a refuted experiment on it — publishing entry_point + 8 and calling with r0 = 3 — which made the boot worse (1,614 instructions, down from 5,963). Both halves were wrong for the same reason: entry_point + 8 is the bx ip of a self-contained veneer whose ldr ip,[pc,#4] sits at +4, so it must be entered at +0; and r0 = 3 selects the destructor branch, whose guard word in this ROM is literally 0.

With the real exec 0x9F answered from the codeseg closure, LibCIpcClient's constructor at 0x82E61CD6 and libc's at 0x82E78CEC ran, the game's own walker 0x83AC → 0x22ADC still ran afterwards, and the word the frontier had recorded as a null member, 0x3FFC0094, read 0x1000. The boot count went down from 5,963 to 5,581 — de-fabrication — and halted at a named unimplemented exec instead of a null branch.

Counts, with the method beside each
valuewhatmethod
8entry points published for mgs_ngi.exe: euser, libc, libz, libdl, libpthread, backend.dll, LibCIpcClient.dll and the EXErun-rom.mjs asserts count 8 with capacity 256, every DLL veneer executed exactly once with r0 = 2, and that euser's and libc's _E32Dll both reach their .init_array walkers
Sources
  • emulation/ngage/db/os/exec-impl.tsv — slow 0x9f, slow 0xa0
  • emulation/ngage/db/frontier.json — history: exec 0xa0 mis-identified as the C++ constructor path; null function-pointer call at backend.dll
  • emulation/ngage/web/src/kernel/codeseg.js

EUser: the library everything imports

EUser never inlines an svc: every exec call goes through one contiguous ARM thunk table

established

EUser.dll's Thumb wrappers do not carry svc instructions. They blx into a single run of 8-byte ARM thunks, each one svc #n followed by bx lr, at 0x8019BD58 through 0x8019C577 — 28 fast execs then 232 slow ones, in exec order. So deciding whether a euser export is an exec wrapper, and naming its exec, is a branch-target lookup rather than a reading of the code, and it cannot be got wrong.

Check it at
  • the run: 0x8019BD58..0x8019C577, 8 bytes per entry, each entry the two words 0xEF0000nn (svc #nn) and 0xE12FFF1E (bx lr)
  • fast 0x800000 through 0x80001B at 0x8019BD58..0x8019BE30, then slow 0x00 through 0xED at 0x8019BE38..0x8019C577, skipping exactly 0x25, 0x34, 0x4C, 0xA7, 0xE7 and 0xE8
  • two further inline copies sit OUTSIDE the run: 0x8019C714 issues svc 0xDC and 0x8019C730 issues svc 0x72
  • the sweep window is EUser's own code section — TRomImageHeader 0x80199000, code_address 0x80199078, code_size 0x32588
  • the already-known anchor: ordinal 585 = 0x801B2D35 reaches 0x8019C318, and the word there is 0xEF00009F

The practical consequence is that two engine tracks stop guessing. An HLE implementation that meets an unimplemented euser import no longer has to read the export to find out what it is: if the export reaches the run, the row already names the exec, and the exec's ekern handler is one join away in the exec table. An LLE trace can symbolise the same call the same way.

It also settles a small thing that had been asserted from the kernel side only. CActiveScheduler::Current and CActiveScheduler::Install are nothing but fast execs 5 and 6: ordinal 427 is a call to 0x801B747E, which is blx 0x8019BD80, and ordinal 428 is blx 0x8019BD88. The installed active scheduler is a per-thread kernel slot, not a euser global — so a JavaScript implementation of the scheduler has to keep it where the kernel keeps it, or Current will disagree with Install across a thread switch.

The six missing slow numbers are missing from the run, not from the kernel: 0x25, 0x34, 0x4C, 0xA7, 0xE7 and 0xE8 have handlers in ekern's slow table but no plain wrapper in euser, so their callers marshal arguments first and the wrapper is inline. That is the same reason the two copies outside the run exist.

Counts, with the method beside each
valuewhatmethod
262thunks of that exact two-word shape in EUser's code section, of which 260 form the single contiguous runa halfword-aligned sweep of [0x80199078, +0x32588) for a word matching 0xEF000000 in its top byte immediately followed by 0xE12FFF1E
28fast thunks, 0x800000 to 0x80001B with no gapthe same sweep; independently equal to the count word 0x1C that sits at word 0 of the fast exec table 0x80069E1C, which the SVC vector indexes count-first
51of the 212 euser ordinals the game and the N-Gage runtime import that reach a thunkdisassembling each export and testing every bl and blx target for membership of the run; recorded per ordinal in db/os/euser-export.tsv
Sources
  • emulation/ngage/db/os/euser-export.tsv — the header states the sweep
  • emulation/ngage/docs/data/ekern-exec-table.tsv — the handler VA per exec
  • emulation/ngage/db/os/exec-impl.tsv

What the game and the runtime actually ask of EUser: 212 ordinals, and a quarter of them are under nine instructions

measured

The nine RAM-loaded images of Metal Gear Solid Mobile and the N-Gage runtime it needs import 212 distinct EUser ordinals between them. Classified by disassembling every one: 51 are exec wrappers, 54 contain no call at all, and 107 call other euser code. 24 of the call-free ones are eight instructions or fewer — the cheapest work available to the HLE track, because for those the export body IS the specification.

Check it at
  • the export directory: TRomImageHeader 0x80199000, export_dir_count 0x8B5 = 2229 at +0x3C, export_dir_address 0x801B4F04 at +0x40, ordinal N at 0x801B4F04 + 4*(N-1), bit 0 = Thumb
  • five anchors that check the indexing end to end: 196 to 0x801A18D3, 585 to 0x801B2D35, 650 to 0x801A599B, 55 to 0x801ACC8B, 1360 to 0x801B2D01
  • the shortest exports measured: ordinal 1510 is ldr r0,[r0] ; bx lr, ordinal 1089 is movs r0,r1 ; bx lr, ordinal 1035 is three instructions that build a descriptor head
execreached by euser ordinalwhat the wrapper is doing
fast 0x6428install the active scheduler; fast 5 is the matching read, through 0x801B747E
slow 0x5674, 2065a tick or timer read, taken twice on independent paths
slow 0x44855, 859two different exports, one exec — the wall-clock read
slow 0x35, 0x36, 0x83, 0x84929, 928, 925, 930the RTimer family: create, after, cancel, high resolution
slow 0xBD, 0xBE, 0xBF, 0xC0, 0xC61448, 1452, 1449, 1441, 1444publish and subscribe: attach, subscribe, cancel, get, set
slow 0x2F and 0x59613one export, two execs — the exception handler installs and then runs
slow 0xAD1854, 1973, 1979three exports, one exec: the security-info block is read once and sliced three ways

That table is the forward map both tracks were missing. Every row is an exec the boot has not reached yet but which the imports say it will, with the ordinal that will issue it — so the next kernel slice can be chosen by what the game is going to ask for rather than by what happened to halt first.

The counting rule matters as much as the count. Extent open means the disassembly did not reach a return inside the window, so for those 23 exports call-free is a lower bound and nothing more; extent closed means the walk found the return. The distinction is in the relation as its own column rather than folded into a prose caveat, because the difference between proven and not-yet-contradicted is exactly the difference this reference exists to keep.

One column is deliberately absent. EKA2L1's epoc9.def carries a symbol for 206 of these 212 ordinals and it is a good lead — it agrees with every identity established here by disassembly — but it is a cross-reference, so it lives in exactly one place, the def_name column of the generated db/hle/import-target.tsv, and is not restated in the evidence relation. The name column of db/os/euser-export.tsv is empty unless the behaviour was read out of the binary.

Counts, with the method beside each
valuewhatmethod
212distinct euser ordinals imported by the game's six images plus the N-Gage runtime modules they pull inparsing the E32 import section of each image and resolving the ELF-format import offsets to ordinals, transitively from the game's own images; nine modules import euser at all
51 / 54 / 107exec wrappers, call-free bodies and bodies that call other codedisassembling each export VA and testing every bl and blx target against EUser's SVC thunk run
31of the 54 call-free bodies whose function extent was also closed, i.e. proven call-free rather than merely call-free so farthe disassembly walked to a pop with PC, a bx lr or a backward branch beyond the furthest forward branch; the other 23 ran out of a 220-instruction window and are recorded as extent open
29distinct execs these imports reach that db/os/exec-impl.tsv does not yet implementthe union of the exec column of db/os/euser-export.tsv minus the keys of exec-impl.tsv; the 15 that are implemented all agree
Sources
  • emulation/ngage/db/os/euser-export.tsv
  • emulation/ngage/db/hle/import-target.tsv — the EKA2L1 lead, generated
  • emulation/ngage/db/hle/import-impl.tsv

The active-object protocol, read off the binary: five panics, three vtable slots and one flags word

established

CActiveScheduler and CActive are 0x14 and 0x18 bytes, the scheduler holds a priority queue at +8 and a stack of loop tokens at +4, and every precondition the pair enforces is a numbered E32USER-CBase panic. The whole runnable test is two comparisons: the object's flags bit 0 must be set and its TRequestStatus must no longer equal KRequestPending, which is the literal 0x80000001.

Check it at
  • the run loop, ARM, 0x8019BA28..0x8019BA60 — and its 0x8019BA34 ldmdane lr,{r2,ip,lr} with lr = the link fixes iStatus, the flags and iNext in one instruction, while 0x8019BA30 sub r0,lr,#0xc gives the link offset
  • KRequestPending twice over: the literal at 0x8019F744 that RunIfReady compares against, and the immediate in 0x8019BA3C cmp r2,#0x80000001
  • CActiveScheduler::Add 0x8019F121, Install 0x8019F3E9, Current through 0x801B747E, Start 0x8019F523, Stop 0x8019F527, the loop driver 0x8019F49C, RunIfReady 0x8019F555, the constructor 0x8019F38B and the destructor 0x8019F3A1
  • CActive::CActive 0x8019F189, SetActive 0x8019F197, Cancel 0x8019F0D9, Deque 0x8019F0FB, the destructor 0x8019F09B, RunError 0x8019F1C1 and IsAdded 0x801B7C58
  • the queue machinery: TDblQueBase's constructor 0x801A97CA, the priority insert 0x801A97FA, the unlink 0x801A96D8
panicraised bythe test, as the instruction writes it
40CActive destructor 0x8019F0A8flags bit 0 still set — destroying an object with a request outstanding
41CActiveScheduler::Add 0x8019F146IsAdded, i.e. iLink.iNext is not null — already on a queue
42CActive::SetActive 0x8019F1A0flags bit 0 already set — a second request without a completion
43CActiveScheduler::Install 0x8019F3F6a non-null argument while Current is already non-null
44Add 0x8019F12C, Stop 0x8019F530, the loop driver 0x8019F4AACurrent returned null — no scheduler installed
45CActiveScheduler::Stop 0x8019F54Cthe loop list ran out without a node whose token is 1
46the run loop's queue-exhausted branch, through the ARM-to-Thumb veneer 0x8019F088 to 0x8019F090the queue holds no runnable object — a stray signal
48CActiveScheduler::Add 0x8019F136a null CActive pointer
49SetActive 0x8019F1B0 and Deque 0x8019F106not IsAdded — the object is not on any queue

The layouts follow from the same reads. CActive is a vtable pointer, then the TRequestStatus at +4, then a flags word at +8, then a TPriQueLink at +0xC whose third word is the priority — which is why the constructor writes aPriority to +0x14. The scheduler is a vtable pointer, the loop stack at +4, and a TDblQueBase at +8 built with link offset 0xC; its destructor recovers each object by subtracting the offset it stored, so the offset is not a constant the code assumes but a field it reads back.

Three vtable slots are pinned by use rather than by convention: slot 3 at +0xC is DoCancel because Cancel loads it and calls it, slot 4 at +0x10 is RunL because both the ARM loop and RunIfReady call it, and slot 5 at +0x14 is RunError because the leave path calls it with the leave code. The base RunError, ordinal 1089, is movs r0,r1 ; bx lr — it hands the error straight back, which is what makes RunIfReady publish it.

Two details will save an implementation from a plausible-looking wrong answer. The priority insert walks while the queued link's priority is greater than OR EQUAL to the new one, so equal priorities run first-in-first-out rather than last-in-first-out. And Start does not allocate: it builds a two-word TLoop node on its own stack, pushes it onto the scheduler's +4, and Stop finds it by searching for the token 1 — so nesting depth is bounded by the C stack and by nothing else.

One thing this does not settle, and it is stated in the open list rather than filled in: the flags word at CActive+8 has a second bit. Three independent sites clear bits 0 and 1 together — Cancel and RunIfReady with lsrs #2 ; lsls #2, the ARM loop with bic ip,ip,#3 — but no instruction anywhere in EUser was found that sets bit 1.

Counts, with the method beside each
valuewhatmethod
9distinct E32USER-CBase panic numbers this pair raises, each tied to one instruction that tests one thingreading every movs r0,#imm that precedes a call to the E32USER-CBase panic helper 0x801A3098 inside 0x8019F088..0x8019F5F0
Sources
  • emulation/ngage/db/os/struct.tsv — cactive, cactivescheduler, tdblquebase, tpriquelink, tschedulerloop
  • emulation/ngage/db/os/euser-export.tsv — ordinals 422, 424 to 428, 430, 1087 to 1091, 1094

RArrayBase and RPointerArrayBase are two different layouts, and iAllocated is the field that moves

established

The array base this ROM ships is iCount, iEntries, iEntrySize, iKeyOffset, iAllocated, iGranularity — at +0x00, +0x04, +0x08, +0x0C, +0x10 and +0x14, with two more words the constructors zero and nothing reads. Its pointer-array sibling shares only the first two: it has no entry size at all, and it keeps iAllocated at +0x08 and iGranularity at +0x0C, which is where the other class keeps iKeyOffset. One set of accessors over both would write the wrong field and look right.

Check it at
  • the one-argument constructor, ordinal 55 = 0x801ACC8B: it zeroes six words, stores 8 at +0x14, checks the entry size against the literal 0x27F at 0x801ACD6C and stores it at +0x08 rounded up to four
  • Append, ordinal 48 = 0x8019CC10 and ARM: ldm r0,{r3,ip} takes iCount and iEntries as a pair, ldr r2,[r0,#0x10] is iAllocated, ldr r2,[r0,#8] is the stride and mla r0,r2,r3,ip is the slot
  • Remove, ordinal 50 = 0x801ACECD, and the free path, ordinals 46 and 47 at 0x801ACE24 and 0x801ACF28 — two byte-identical bodies
  • Count, ordinal 1510 = 0x801ACE39, is the entire accessor: ldr r0,[r0] ; bx lr
  • the three- and four-argument constructors 0x801ACCBD, 0x801ACD03 and 0x801ACDE7, which are what fix iGranularity and iKeyOffset
  • the pointer-array class: constructor ordinal 481 = 0x801AC97B, nine instructions and no argument; the free path ordinal 471 = 0x801ACAE3, which clears +0x08; Remove ordinal 474 = 0x801ACA89, whose lsls r1,r5,#2 is a hardcoded four-byte stride; Count ordinal 1593 = 0x801ACA07

The constructors are worth reading for what they enforce rather than what they store. USER 129 bounds the entry size to 1 through 640. USER 127 requires a positive granularity and that granularity times entry size stays within 0x10000000. USER 128 requires a key offset that is non-negative, four-byte aligned and strictly inside an entry. And the entry size is silently rounded up to a multiple of four before it is stored, so an array of three-byte entries has a stride of four — an emulator that takes the caller's number at face value produces an array that indexes correctly for the first element and wrongly after that.

Ordinals 46 and 47 are byte-identical bodies at different addresses: count to zero, User::Free on the entry block, then the pointer and the allocated count to zero. Close and Reset were compiled separately and behave identically here; which ordinal is which is not established by this binary, and neither is it needed, because the behaviour is the same.

offsetRArrayBaseRPointerArrayBase
0x00iCountiCount
0x04iEntriesiEntries
0x08iEntrySize, rounded up to 4iAllocated
0x0CiKeyOffsetiGranularity, 8 from the constructor
0x10iAllocatedwritten zero, read by nothing
0x14iGranularitywritten zero, read by nothing
0x18 and 0x1Cwritten zero, read by nothingnot written

The two are told apart by one instruction each. The pointer array's free path clears +0x08 where the value array's clears +0x10, and its Remove shifts the tail with lsls r1,r5,#2 — a stride baked into the code rather than loaded from the object, because a pointer entry is always four bytes. That is also why it has no entry size and why its constructor takes no argument at all.

This is the whole reason the HLE track was blocked on ordinal 55. Every operation above is a leaf over guest memory — no exec, no server, no allocation except the grow path — so with the offsets fixed the family is implementable outright. The offsets in this entry were reached twice independently, by this reference from the constructors and by the HLE track from the same addresses while implementing them, and the two readings agree; that is worth saying only because a layout that two readers agree on is the kind of thing nobody re-checks.

Counts, with the method beside each
valuewhatmethod
0reads of the two words at +0x18 and +0x1C anywhere in the array codea halfword scan of EUser's whole code section for the Thumb encodings of a load or store at rN+0x18 and rN+0x1C — 245 hits overall, of which the 10 inside 0x801AC800..0x801AD600 are all stores and all inside constructors
640the largest entry size a constructor accepts, from the literal 0x27F compared against aEntrySize minus onereading the literal at 0x801ACD6C that 0x801ACCA2 loads, and the bls that skips the USER 129 panic
Sources
  • emulation/ngage/db/os/struct.tsv — rarraybase and rpointerarraybase
  • emulation/ngage/db/os/euser-export.tsv — ordinals 46, 47, 48, 50, 55, 1510 and 2169 for the value array; 471, 474, 481 and 1593 for the pointer array
  • emulation/ngage/db/hle/import-impl.tsv — euser 481, the independent reading

The two panic category strings are in the ROM, and one adr instruction is what finds them

established

Every euser panic funnels into ordinal 650 at 0x801A599B through one of two helpers, and each helper carries its category as data. 0x801AB7C4 builds a descriptor over the bare UTF-16 text USER at 0x801AB7EC; 0x801A3098 loads a pointer to a literal descriptor whose 13 characters read E32USER-CBase. Reading a panic number out of a halted run therefore names a real category rather than a number.

Check it at
  • 0x801AB7C4: push {r2,r3,r4,lr} ; adr r1,#0x24 ; movs r4,r0 ; mov r0,sp ; bl 0x801AED70 ; movs r1,r4 ; bl 0x801A599A — the adr resolves to 0x801AB7EC, where the bytes are 55 00 53 00 45 00 52 00 00 00
  • 0x801A3098: movs r1,r0 ; ldr r0,[pc,#0x28] ; bl 0x801A599A — the literal at 0x801A30C8 is 0x801B92C4, whose word 0 is 0x0000000D and whose characters read E32USER-CBase
  • ordinal 650 = 0x801A599B is the shared entry, taking the category descriptor and the number

The pair splits cleanly along the library boundary. USER is the descriptor, array, heap and thread layer — the RArray constructors raise USER 127 through 130, the queue constructor USER 78. E32USER-CBase is the CBase-derived world, which in practice means the active-object protocol and the cleanup stack. A halt reporting a number without a category is therefore ambiguous by exactly this much, and it need not be.

The USER helper is the more interesting of the two, because its category is not a descriptor at all in the image — it is a plain null-terminated wide string, turned into a stack descriptor at call time by 0x801AED70. A tool that goes looking for a TLitC there will not find one.

Sources
  • emulation/ngage/db/os/euser-export.tsv — ordinal 650
  • emulation/ngage/db/os/struct.tsv — tdesc

struct stat is 96 bytes, and this libc writes exactly six places in it

established

The C library's stat() ends up in backend.dll, which zeroes 0x60 bytes of the caller's buffer and then writes the drive number twice, a 16-bit mode, a 16-bit link count, one timestamp into two words, a sign-extended 64-bit size and the constant 512. Everything else stays zero. That is the whole layout an implementation has to reproduce, and the parts it must NOT invent are as well defined as the parts it must fill.

Check it at
  • libc ordinal 344 = 0x82E70371 converts the path and calls the veneer 0x82E827A4, whose target word is 0x82E501DF — backend.dll, whose code runs 0x82E46158 to 0x82E55D1C
  • 0x82E501DE to 0x82E46871 to the worker 0x82E47851; the TEntry is built at sp+8 by 0x82E527F8, a veneer to EFSrv 0x801ED69D — the same constructor the TEntry entry of this reference already rests on
  • the size measurement: 0x82E478CA movs r1,#0x60 ; movs r0,r4 ; blx 0x82E52EA8 zeroes the buffer before anything is written
  • the field filler 0x82E4B091, which writes the mode at +8, the link count at +0xA, the timestamp at +0x18 and +0x20, and 512 at +0x40
  • the drive number: 0x82E478DA through the veneer 0x82E52858 to EFSrv 0x801ED603, which reads the first UTF-16 character of the path and converts it with 0x801ECE16
  • the timestamp: 0x82E4B0E6 through 0x82E522E8 to EUser 0x801AA9E5, with the 64-bit constant 0x00DCDDB3 0x0F2F8000 and the divisor literal 0x000F4240 at 0x801AAC30
offsetsizewhat is writtenthe instruction that writes it
0x004the path's drive number0x82E478E6 str r0,[r4] — from EFSrv 0x801ED603, first character of the path through the character-to-drive conversion
0x082mode: file-type bits then permission bits0x82E4B0B4 or 0x82E4B0B8 strh, then 0x82E4B0CE strh after ORing 0x100 or 0x180
0x0A2link count, 2 when the mode is 0xA000 and 1 otherwise0x82E4B0DA strh r1,[r4,#0xa]
0x144the same drive number again0x82E478E4 str r0,[r4,#0x14]
0x184seconds since 1970, or -1 when the conversion overflows0x82E4B0F8 str r0,[r4,#0x18]
0x204the same seconds value, byte for byte0x82E4B0F6 str r0,[r4,#0x20]
0x30 and 0x344 + 4the file size, sign-extended to 64 bits from TEntry.iSize0x82E478D4 asrs r1,r0,#0x1f ; str r1,[r4,#0x34] ; str r0,[r4,#0x30]
0x404512, unconditionally0x82E4B0FC movs r0,#1 ; lsls r0,r0,#9 ; str r0,[r4,#0x40]

The mode word is built from the TEntry's attribute bits and nothing else. The default is 0x8000; attribute bit 4, the directory bit, turns it into 0x4000 through an asrs r3,#1 of the 0x8000 that was already in the register; attribute bits 2 and 3 select 0xA000; the drive-root case forces the attribute word to 0x10 first. Then attribute bit 0, read-only, chooses 0x100 over 0x180. No group or other permission bit is ever set, so a mode read back from this producer is always owner-only.

The timestamp is worth spelling out because it is the one place a factor of a million can hide. EUser 0x801AA9E5 subtracts the epoch constant from the file's TTime as a 64-bit quantity, divides by the literal 1,000,000, bounds the result to the range of a TInt using 0x7FFFFFFF and 0x80000000 and returns KErrOverflow otherwise. So the two words are seconds, not microseconds — and the -1 the caller stores on failure is a real value the buffer can hold, not an error code the caller checks.

Before this was read, the HLE track's row for libc ordinal 344 said honestly that the layout was not measured and that it was safe only because the one caller in the game, at mgs_ngi.exe 0x249F4, tests the return value and never touches the buffer. That was the right way to leave it. It can now be filled, and the fields above are the whole of what a faithful implementation writes: filling more than this producer does would be as wrong as filling less.

Counts, with the method beside each
valuewhatmethod
96bytes of struct stat, as this producer defines itthe length argument of the zero-fill the worker performs on the caller's buffer at 0x82E478CA, before any field is written
62168256000000000the constant subtracted from the file's TTime before the divide, i.e. 1970-01-01 in TTime unitsthe two literals loaded at 0x82E4B0DE and 0x82E4B0E2, 0x0F2F8000 and 0x00DCDDB3, read as one 64-bit value
Sources
  • emulation/ngage/db/os/struct.tsv — libcstat
  • emulation/ngage/db/hle/import-impl.tsv — libc 344, the row that declined to guess
  • emulation/ngage/db/os/struct.tsv — tentry

Servers and the file system

User::QueryVersionSupported ignores iBuild entirely

established

Every Symbian client-server connect carries a TVersion and the server decides whether to accept it. TVersion has three fields — iMajor, iMinor, iBuild — and only the first two participate: the halfword at offset 2 is never read.

Check it at
  • euser 0x801A5B27 User::QueryVersionSupported — r0 is current (the server's), r1 is requested (the client's)
  • efile.exe CServerLoader::NewSessionL builds TVersion(1, 0, [0x801D2604] = 0x625 = 1573) and calls the same stub 0x801E538C the !FileServer connect uses
  • the client's connect arrives with args[0] = 0x063F0001 = 1.0.1599
      movs  r3, #0
      ldrsb r2, [r1, r3]      ; requested.iMajor   (byte 0)
      ldrsb r4, [r0, r3]      ; current.iMajor
      cmp   r2, r4
      blt   supported
      cmp   r2, r4
      bne   not_supported
      movs  r3, #1
      ldrsb r1, [r1, r3]      ; requested.iMinor   (byte 1)
      ldrsb r0, [r0, r3]      ; current.iMinor
      cmp   r1, r0
      bgt   not_supported
supported:
      movs  r0, #1

Offsets 0 and 1 only. Supported means: requested.iMajor is below current.iMajor, or the majors are equal and requested.iMinor is at most current.iMinor.

Why it matters. MGS's C runtime connects to !Loader asking for version 1.0.1599, while the loader server in this ROM is 1.0.1573. The client asks for a higher build than the server has and the connect still succeeds. An emulator that compares all three fields rejects it and stalls the game — behind a perfectly reasonable-looking KErrNotSupported. KErrNone is the device's result, not a chosen one, and web/src/kernel/version.js owns the one predicate both servers import, so the rule exists once.

Note the argument order, which is equally easy to get backwards: r0 is current, r1 is requested.

Sources
  • emulation/ngage/db/frontier.json — history: unimplemented !Loader op -1 (KConnectMessage)
  • emulation/ngage/db/os/fs-op.tsv — !FileServer −1, !Loader −1
  • emulation/ngage/web/src/kernel/version.js

Finding a server's NewSessionL without symbols

established

CServer2 subclasses are where a Symbian server decides who may connect. With no symbols the practical route is the vtable — and the slot index can be derived rather than guessed, once one server's NewSessionL is known. In this ROM it is slot 7, and a server's shareability is a constructor argument, not a property of its name.

Check it at
  • CServerFs constructor 0x801D2A5A: movs r2,#2 → EIpcSession_GlobalSharable; vtable literal [0x801D2BD4] = 0x801E9E8C; slot 7 = 0x801D2E4B = CServerFs::NewSessionL; name TLitC16 0x801E7EC0 '!FileServer'
  • CServerLoader constructor 0x801D0C30: movs r2,#0 → EIpcSession_Unsharable; vtable literal [0x801D0DD0] = 0x801E96E8; slot 7 = 0x801D2225 = CServerLoader::NewSessionL; name TLitC16 0x801E7630 '!Loader'
  • CServer2::CServer2 0x801A29CD stores aType at CServer2+0x1C (movs r5,r2 … str r5,[r4,#0x1c]); CServer2::StartL 0x801A29A1 reads it back (ldr r2,[r0,#0x1c] ; adds r0,#0x20 ; bl 0x801A6E96) as RServer2::CreateGlobal's mode, which issues svc #0x7d at 0x801A6EBC
!FileServer (CServerFs)!Loader (CServerLoader)
constructor0x801D2A5A0x801D0C30
TServerType passed to CServer2::CServer2movs r2,#2 → 2, EIpcSession_GlobalSharablemovs r2,#0 → 0, EIpcSession_Unsharable
vtable literal[0x801D2BD4][0x801D0DD0]
vtable0x801E9E8C0x801E96E8
slot 70x801D2E4B = CServerFs::NewSessionL0x801D2225 = CServerLoader::NewSessionL
name TLitC160x801E7EC0 '!FileServer'0x801E7630 '!Loader'

The index is pinned, not assumed: CServerFs::NewSessionL was already known from following CServer2::StartL, it sits in slot 7, and slot 7 of the loader's vtable disassembles to the same shape — build a TVersion, call QueryVersionSupported, User::Leave(KErrNotSupported) otherwise, then allocate the session object.

The shareability the constructor sets is what Exec::SessionShare later checks: the file server's 2 is what RSessionBase::ShareAuto needs, and the CRT shares its RFs session right after Connect. The loader's 0 means a loader session cannot be shared at all.

Sources
  • emulation/ngage/db/os/hle-server.tsv — !FileServer, !Loader
  • emulation/ngage/db/os/exec-impl.tsv — slow 0x27, slow 0x7e
  • emulation/ngage/db/frontier.json — history: unimplemented !Loader op -1 (KConnectMessage)

The file server computes a session's private path from the SecureId

established

CServerFs::NewSessionL builds the new session's path as 'C:' + '\Private\' + the message's SecureId as eight zero-padded hex digits + '\' and stores it in the session object. A Symbian app's private directory is its UID3 in hex not by tooling convention but because the file server computes it at connect time from the message's secure ID.

Check it at
  • efile.exe CServerFs::NewSessionL 0x801D2E4A; TBuf<30> built with AppendNumFixedWidth(aMessage.SecureId(), EHex, 8); stored at CSessionFs+0x30 by 0x801D2EA6 str r0,[r5,#0x30]
  • '\Private\' is the efile TLitC at 0x801E7F7C
  • mgs_ngi.exe SecureId 0x20010925 → C:\Private\20010925\ — the directory the game's assets install to

RFs::SetSessionToPrivate(aDrive) — function 85 — later rewrites only the drive letter, and RFs::PrivatePath (86) answers the same string without a drive. The path is what SessionPath (14) copies back into the caller's descriptor, which is the first thing the C runtime asks for after connecting.

Sources
  • emulation/ngage/db/os/fs-op.tsv — !FileServer −1, 14, 85, 86
  • emulation/ngage/web/src/fs/fileserver.js

!Loader function 2 is the library load, and a UID1 gate is what makes it so

established

The loader server's ServiceL dispatches functions 1..14; the handler behind function 2 accepts a request whose uid1 is 0 or 0x10000079 (DLL) and appends '.DLL' when the name carries no dot, while function 1 checks 0x1000007A (EXE) and appends '.EXE'. The flag word the client sends, 0x1FC, is a constant baked into euser's three-argument TIpcArgs constructor.

Check it at
  • euser ordinal 1135 at 0x801A78FE sends it: 0x801A7954..0x801A7958 ldr r0,[sp,#0x44] ; movs r1,#2 ; bl 0x801B7C66 — r1 = 2 is the function number; the TIpcArgs constructor 0x801B8B04 does movs r1,#0xff ; adds r1,#0xfd = 0x1FC
  • efile.exe CSessionLoader::ServiceL 0x801D1DCC: 0x801D1DD6 subs r0,r5,#1 ; cmp r0,#0xe ; blo — an unsigned-lower test on (fn − 1), so 1..14, not 1..15; __ARM_switch8 to 0x801D1F5A
  • handler 0x801D1480 reads tldrinfo.uid1 and accepts only 0 or 0x10000079 — the literal [0x801D16E8] = 0x1000007A minus one; '.DLL' is the TLitC8 at 0x801E7574
  • the 0x20-byte write-back is conditional in the handler (skipped when req+0x2C is null, 0x801D1552) and mandatory only because ServiceL always stores aMessage there; efile reads the block with RMessagePtr2::Read(0, TDes8&, 0) at 0x801D19E2 and writes it back at 0x801D13A4
  • tldrinfo, 0x20 bytes, built by euser 0x801A5B64 (zeroed through 0x8019E65C, then movs r0,#1 ; mvns r0,r0 ; str r0,[r4,#0x18]); uid1, uid2, uid3, a 1 at +0x0C, the handle at +0x10, a version; +0x14 and +0x1C are never written on the library path and are deliberately not declared
  • the drive search at 0x801D19FE, which resolves a library the caller did not path, is not disassembled

The client side has the unmistakable shape of RLibrary::Load — filename, TUidType, out-handle, a KMaxFileName check, the DLL lock taken with exec 0x9B before and released with 0x9C on failure — but a stripped ROM carries no names, so whether ordinal 1135 is that function is settled by shape, not by evidence.

Two corrections an adversarial pass produced were re-derived before adoption: the 1..14 range (a blo on fn − 1) and the conditional write-back. Both are the kind of detail an implementation copies from a note without checking, and both were wrong in the note.

Counts, with the method beside each
valuewhatmethod
41,904 → 42,112instructions the boot runs before and after answering function 2; the first library the game asks for is hal.dll, which the VFS resolves from samples/firmware/5320/sys/binrun-rom.mjs, which asserts the reply 'hal.dll -> handle …'
Sources
  • emulation/ngage/db/os/fs-op.tsv — !Loader 2
  • emulation/ngage/db/os/struct.tsv — tldrinfo
  • emulation/ngage/db/frontier.json — history: unimplemented !Loader op 2

TEntry is 552 bytes, and the layout is recoverable entirely from consumers

established

RFs::Entry sends a TPckg<TEntry> of 0x228 bytes. The layout is fixed by the code that reads it — which matters, because on this device the code that fills a TEntry lives in a filesystem plugin that is not in the firmware image at all. Eight bytes at 0x220..0x227 are read by nothing on this device.

Check it at
  • EFSrv.dll RFs::Entry wrapper 0x801ECCC7 builds the TPckg with movs r3,#0x45 ; lsls r3,r3,#3 = 0x228; efile.exe's function-22 handler 0x801D3530 reserves a 0x238 frame with the TEntry at sp+8 and a TPtrC16 immediately after it at sp+0x230; a third ROM module at 0x80BA9A80 heap-allocates 0x45 << 3 before calling the constructor 0x801ED69D
  • the constructor: adds r0,#0x10 (TUidType ctor, 12 bytes zeroed by EUser 0x801A53B5) then adds r0,#0xc (TBufC16 ctor 0x801AF70B, which stores a zero length word)
  • EFSrv EntrySize 0x801F28E1: ldr r0,[r0,#0x1c] ; lsls r0,r0,#4 ; lsrs r0,r0,#3 ; adds r0,#0x20 = 0x20 + 2·length; EFSrv 0x801F140E passes 256 as the capacity bound to TBufCBase16::Copy (EUser 0x8019DE44)
  • five one-line EFSrv attribute accessors, each ldr r0,[r0] plus one bit test (0x1, 0x2, 0x4, 0x10, 0x20); a caller clamps iSize with cmp r0,r5 ; bge — a signed compare; two entries' +8/+0xC loaded as register pairs into subs r0,r0,r2 ; sbcs r1,r1,r3
offsetfieldevidence
0x00iAttfive one-line EFSrv accessors, each ldr r0,[r0] plus one bit test (0x1, 0x2, 0x4, 0x10, 0x20)
0x04iSize, signeda caller clamps it with cmp r0,r5 ; bge — a signed compare
0x08iModified, one 64-bit valuetwo entries' +8/+0xC loaded as register pairs into subs r0,r0,r2 ; sbcs r1,r1,r3
0x10iType, three UIDsthe constructor's adds r0,#0x10, then a routine that zero-fills exactly 12 bytes
0x1CiName length wordEntrySize = ldr r0,[r0,#0x1c] ; lsls #4 ; lsrs #3 ; adds r0,#0x20 = 0x20 + 2·len
0x20iName characters, 256 unitsthe capacity bound 256 is passed to TBufCBase16::Copy, not inferred from KMaxFileName

0x1C + 0x204 = 0x220 = 544. sizeof is 552. The eight bytes at 0x220..0x227 cannot be tail padding — 544 is already 8-aligned and the struct's alignment is 8 because of iModified — and nothing on this device reads them; what they are is closed by cross-reference in the next entry.

Two details worth keeping. TEntry::operator= copies only 0x20 + 2·len bytes, so assignment does not preserve anything past the name. And the packed form used for directory listings advances by EntrySize rounded up to 4, not by the fixed 0x228 — which will matter to anyone implementing RDir::Read.

Counts, with the method beside each
valuewhatmethod
0loads or stores at rN + 0x220 or rN + 0x224 anywhere in efile.exe or EFSrv.dll, and literal 0x220/0x224 offsets anywhere in the ROMa sweep of all 68,759,552 ROM bytes for the literals, and of the two images' instructions for the addressing forms
Sources
  • emulation/ngage/db/os/struct.tsv — tentry
  • emulation/ngage/db/os/struct_field.tsv — tentry.*
  • emulation/ngage/db/os/fs-op.tsv — !FileServer 22

The eight unread bytes of TEntry are iSizeHigh and iReserved

cross-reference

Symbian's published TEntry reference documents FileSize() and SetFileSize(TInt64) with the note that the low word is stored in iSize and the high word in the private member iSizeHigh, and f32file.h ends the class with TUint32 iSizeHigh; TUint32 iReserved. iSize is at +0x04, so the private high word is the word after iName, +0x220, and +0x224 is the reserved one. That fits the geometry exactly.

Check it at
  • TEntry +0x220 iSizeHigh, +0x224 iReserved — per f32file.h (Symbian^3), against the +0x04 iSize and 0x20 + 0x204 name extent established from this ROM's EFSrv.dll

It is graded cross-reference, not established, and deliberately not committed as a struct field: this ROM is Symbian 9.3 while the reference is Symbian^3, nothing in the image reads either word, and the project's precedence rule makes published documentation a corroborator, never a source.

Worth knowing anyway — a file server that leaves +0x220 unwritten hands FileSize() a garbage high word, and the constructor provably does not zero it.

Sources
  • emulation/ngage/db/os/struct.tsv — tentry (the note that declines to declare the field)

The CPU

Thumb condition codes and flag-setting are where emulators quietly die

measured

Three defects in this project's own CPU each produced a plausible run for a long time: HI, LS, VS and VC missing from the condition evaluator; ADDS and SUBS forms that set only N and Z; and shift-by-register forms that never updated carry. None was found by a result-only test; all three fell out of a differential oracle.

Check it at
  • euser's DoPop compares iNext against a target and exits its loop with BHI — always-taken meant over-popping the cleanup stack, a panic nowhere near the missing condition code
  • the dedicated Thumb ADDS/SUBS forms (register, imm3, imm8 — for example 0x18C0 adds r0,r0,r3) and LSLS, LSRS, ASRS, RORS by register
  • 0x801D2225 is a function; 0x801D2224 is a fiction — Symbian application, euser and efile code is Thumb, and a VA disassembled without bit 0 set yields confident, well-formed ARM nonsense

The condition evaluator had a default that returned true — precisely what made the missing codes take every branch. A silent default is the fidelity scanner's whole reason to exist, and this is the case that justified it: every condition code is now explicit and an unknown one HALTs.

The carry class is the one that hides longest. Dropping C and V on an add or subtract breaks every 64-bit ADC/SBC carry-in and every BCS, BCC and BVS after it; the oracle's 64-bit tests were ARM-correct and Thumb-wrong until cpu.addF and subF existed. That bug was not on the first thousand instructions of the boot path — it would have bitten later, somewhere unrelated.

Counts, with the method beside each
valuewhatmethod
13,600state pairs on which the live ALU and an independently generated micro-op executor agree after the shift-carry fixweb/test/t16-diff.mjs, a differential test between the interpreter and an executor generated from db/cpu/microop.tsv and insn-t16alu.tsv
255encodings of decode debt retired when 0xDExx (UDF) stopped executing as an always-taken branch and HALTs insteadtools/cpu-decode-audit.mjs against db/cpu/decode-baseline.json, which may only decrease
Sources
  • emulation/ngage/db/frontier.json — history: Thumb cond() HI/LS/VS/VC missing; Thumb ADDS/SUBS dropped carry/overflow
  • emulation/ngage/web/test/thumb-cond.mjs
  • emulation/ngage/web/test/cpu-oracle.mjs
  • emulation/ngage/web/test/t16-diff.mjs

The target is ARMv6: Thumb-1 plus BL/BLX, and nothing from Thumb-2

measured

The Nokia 5320's ARM1136J executes ARMv6. The real executed instruction stream of the game and the firmware contains no 32-bit Thumb encoding other than the BL/BLX pair — no Thumb-2, no CBZ, no IT — so an interpreter that executes those is executing encodings the device cannot.

Check it at
  • tools/isa-evidence.mjs over the ROM-backed run — a verify.mjs stage
  • db/cpu/t32-decode.tsv: on ARMv6 only the BL/BLX pair is valid; db/cpu/t16-decode.tsv and arm-class-decode.tsv are generated from LLVM's armv6 decoder by tools/gen-decode-db.mjs
  • cpu.js null-page guard: executing at PC below 0x1000 HALTs

The oracle for what an instruction is comes from the machine, not from memory: db/cpu is enumerated from LLVM and the live interpreter is audited against it. The firmware does use the ARMv5TE signed DSP multiplies (SMUL, SMLA, SMLAW, SMULW, SMLAL with their halfword selectors), which RVCT emits, and those are implemented; word LDREX and STREX are too (single CPU: the monitor always succeeds), while the ARMv6K byte, halfword and doubleword exclusives stay unimplemented per the same audit.

Counts, with the method beside each
valuewhatmethod
0non-BL 32-bit Thumb encodings in the real executed streamtools/isa-evidence.mjs classifies every executed halfword pair of the ROM run
about 14,000encodings the interpreter once executed that LLVM calls invalid on this target — latent misdecodes waiting to fire, ratcheted down by the decode audittools/cpu-decode-audit.mjs, behavioural: every LLVM-invalid encoding must HALT
3.99 milliondistinct PCs the CPU visited after one null branch, streaming the zero page as NOPs for four million instructions before the guard existedthe unique-PC count of the run that motivated the null-page guard
Sources
  • emulation/ngage/tools/isa-evidence.mjs
  • emulation/ngage/tools/cpu-decode-audit.mjs
  • emulation/ngage/db/cpu/decode-baseline.json
  • emulation/ngage/db/frontier.json — history: exec 0x28 RThread::Resume + word LDREX/STREX

The N-Gage container

A SIS v9.x file is a 16-byte UID header followed by TLV fields

cross-reference

Symbian OS v9.1+ installation packages open with four UID words and continue as type-length-value fields, little-endian, each zero-padded to a 4-byte boundary; elements inside a SISArray omit their type word; a compressed field is raw DEFLATE. The field-type numbers are the specification's, and the ones this project reads are checked against the three real containers.

Check it at
  • file offset 0x00 UID1 = 0x10201A7A (fixed), 0x04 UID2 (reserved), 0x08 UID3 = the package UID, 0x0C the UID checksum
  • Symbian OS v9.X SIS File Format Specification, Version 1.1, June 2006 (Symbian Ltd.), mirrored at https://www.cryer.co.uk/file-types/s/sis/softwareinstallsis.pdf — the specification written for makesis and sisxlibrary; §2.12.2 is the padding rule
  • the readers: emulation/ngage/web/src/loader/sisx.js (the emulator loader, which is also the writer) and emulation/ngage/tools/sisx.py; EKA2L1 src/emu/loader/src/sis_fields.cpp as a second parser
partsizemeaning
Type4 bytesfield type identifier
Length4 bytes, or 8 with the top bit setvalue length; 8 bytes when the value is 2^31 or more
ValueLength bytesthe field's data
Padding0..3 zero bytesto the next 32-bit boundary: (4 − position mod 4) mod 4
typefieldtypefield
1SISString (UCS-2)22SISCertificateChain
2SISArray23SISLogo
3SISCompressed24SISFileDescription
4SISVersion25SISHash
5SISVersionRange26SISIf
6SISDate27SISElseIf
7SISTime28SISInstallBlock
8SISDateTime29SISExpression
9SISUid30SISData
11SISLanguage31SISDataUnit
12SISContents32SISFileData
13SISController33SISSupportedOption
14SISInfo34SISControllerChecksum
15SISSupportedLanguages35SISDataChecksum
16SISSupportedOptions36SISSignature
17SISPrerequisites37SISBlob
18SISDependency38SISSignatureAlgorithm
19SISProperties39SISSignatureCertificateChain
20SISProperty40SISDataIndex
41SISCapabilities
SISContents        { ControllerChecksum? DataChecksum? Controller: SISCompressed<SISController> Data: SISData }
SISController      { Info Options Languages Prerequisites Properties Logo? InstallBlock Signatures* DataIndex }
SISInfo            { UID VendorName Names[] VendorNames[] Version CreationTime InstallType(u8) InstallFlags(u8) }
SISInstallBlock    { Files: SISFileDescription[] EmbeddedSISFiles: SISController[] IfBlocks: SISIf[] }
SISFileDescription { Target MIMEType Capabilities? Hash Operation(u32) OperationOptions(u32) Length(u64) UncompressedLength(u64) FileIndex(u32) }
SISCompressed      { Algorithm(u32: 0 none, 1 deflate) UncompressedSize(u64) Data }
SISData            { DataUnits: SISDataUnit[] }   SISDataUnit { FileData: SISFileData[] }   SISFileData { Data: SISCompressed<bytes> }

Install types are 0 installation, 1 augmentation, 2 partial upgrade, 3 pre-installed application and 4 pre-installed patch; a file's operation is 1 install, 2 run, 4 text or 8 null. Controllers and individual files are both wrapped in SISCompressed, and the deflate inside is raw — no zlib header. For embedded SIS files the absolute data index is the sum of the parent controllers' indices, and the specification caps embedding at eight levels.

The pitfalls a parser meets, in the order this project met them: array elements carry no type word, string lengths are bytes not characters, file indices are relative to the current controller's data unit, and the padding belongs to the field — which is what the checksum finding below turns on.

Sources
  • archive/ngage — the 2025 prototype's format notes, consumed into this entry
  • http://justsolve.archiveteam.org/wiki/SIS
  • https://github.com/EKA2L1/EKA2L1/blob/master/src/emu/loader/src/sis_fields.cpp

An .n-gage file is a MIME multipart with two SIS parts, and its arrays do not nest

measured

Every N-Gage 2.0 package examined is multipart/mixed with boundary KBoundary: a text/xml retailer part, then two application/sis parts with Content-ID <Metadata> (a small installer) and <Game> (the asset pack with the E32 executables). Inside them SISData holds one flat SISArray<SISDataUnit> and a data unit one flat SISArray<SISFileData> — a single array level in all three titles, which corrects the two-level nesting this entry claimed until 2026-09-04 — and a controller may embed further controllers.

Check it at
  • samples/mgs/Metal.Gear.Solid.n-gage — 2,542,447 bytes, sha256 575afba9a431775edd30e20a54e23380d182560d7d51db90ae5edd5b810b2d81, byte-identical to tests/fixtures/ngage/mgs.n-gage; tests/fixtures/ngage/resident_evil.n-gage; tests/fixtures/ngage/tetris.n-gage
  • samples/mgs/extracted/RetailerInfo.xml, Metadata.sis, Game.sis — the unwrapped parts, produced by tools/extract_ngage.py and tools/sisx.py; Game.sis opens with 7A 1A 20 10 = 0x10201A7A
  • SISData → Array<DataUnit> → DataUnit → Array<FileData> → FileData → Compressed, read off the elemType word of each array by emulation/ngage/web/src/loader/sisx.js parseNode; emulation/ngage/web/test/loader.mjs serializes all three titles back through that tree byte-for-byte; SISController fields of type 13 inside a controller

Measured on 2026-09-04 from the array elemType words themselves: every SISData in the three titles is a flat SISArray<SISDataUnit> and every data unit a flat SISArray<SISFileData>. The earlier claim of two or three nesting levels came from a reader that unwrapped defensively and never checked what it unwrapped; a reader that expects the flat form re-serializes all three containers byte-for-byte. Where a structure does not match, the honest response is to stop with the offset, not to try an alternative reading.

The .n-gage file is neither encrypted nor a raw SIS. Symbian Seance's container pane lists the parts unprompted, which is why the older Sextant tool was superseded rather than revived.

Counts, with the method beside each
valuewhatmethod
2.2–2.4 KB and tens of KBthe <Metadata> part's controller and data across the three titlesthe TLV tree of emulation/ngage/web/src/loader/sisx.js
2.3–2.4 MBthe <Game> part of each titlethe same field boundaries
23–36files per <Game> controller, with 0 signatures and a package UID matching the sibling <Metadata> partparseSisx over each <Game> controller of the three titles
Sources
  • emulation/ngage/web/src/loader/sisx.js
  • emulation/ngage/tools/extract_ngage.py
  • emulation/ngage/tools/sisx.py
  • emulation/ngage/samples/mgs/PROVENANCE.md

SIS ControllerChecksum and DataChecksum are CRC-16/XMODEM over the padded field — for the Metadata part

measured

The two optional SISContents fields (types 34 and 35) verify as CRC-16/XMODEM — polynomial 0x1021, init 0, no reflection, xorout 0 — computed over the whole SISField including its trailing alignment pad, from the field's type word to the byte before the next sibling's type word. That reproduces all six stored values of the three <Metadata> parts and none of the six <Game> values under any of 280 CRC-16 parameterisations.

Check it at
  • SISContents: ControllerChecksum (type 34) over the whole SISCompressed<SISController> field, DataChecksum (type 35) over the whole SISData field — specification §4.2.1.1, §4.2.1.2, §4.2.2.1, §4.2.3.1; the specification fixes the byte range and never names the CRC-16 variant
  • emulation/ngage/web/src/loader/sisx.js parseSisx() records stored against computed per part; serializeSisx() after a patch recomputes a checksum only where the stored one verified and drops the optional field otherwise; emulation/ngage/web/test/loader.mjs asserts both outcomes on the MGS parts
containerstored ControllerChecksumrecomputedstored DataChecksumrecomputed
MGS <Metadata>0xA9790xA9790x45E80x45E8
Resident Evil <Metadata>0xD5070xD5070x76920x7692
Tetris <Metadata>0xA2D30xA2D30x6A430x6A43
MGS <Game>0xE251no match0x8BC6no match
Resident Evil <Game>0xD580no match0xB7A6no match
Tetris <Game>0x000Cno match0x786Fno match

Six independent 16-bit values with zero collisions against the wrong byte range or algorithm variant is not a coincidence match. The byte ranges are structurally sound before the algorithm is ruled out on the other part: the Data field ends exactly at the SIS part's last byte in every case, and every <Game> controller re-parses completely with the right name and UID. So the stored word genuinely does not verify against the bytes in the file. The likely explanation is that these are third-party-archived dumps repackaged by a tool that only cared about installability — the checksums are optional per §2.2 — but that cannot be verified either way without an authoritative, checksum-correct <Game>-sized sample, which this project does not have.

Hence the rule the writer follows since 2026-09-04: a patched part gets a recomputed checksum only when its stored checksum verified against the original bytes, so the algorithm is proven for that very file; otherwise the optional field is dropped. A patched Metadata part therefore carries a verifiable checksum and a patched Game part carries none — never a value computed by code that was never checked against that part. What would close it: an unmodified first-party N-Gage SIS or the makesis/sisxlibrary source, then the same comparison re-run.

Counts, with the method beside each
valuewhatmethod
6 of 6<Metadata> checksums reproduced exactly: MGS 0xA979 and 0x45E8, Resident Evil 0xD507 and 0x7692, Tetris 0xA2D3 and 0x6A43parse each container down to raw TLV boundaries, locate the exact byte range each specification paragraph names, recompute CRC-16/XMODEM over the padded whole field, compare with the stored word; the un-padded field, the value-only range and every other CRC-16 family tried all produce non-matching numbers
0 of 560 per field per titlematches for the <Game> parts (stored MGS 0xE251 and 0x8BC6, Resident Evil 0xD580 and 0xB7A6, Tetris 0x000C and 0x786F)280 parameterisations — polynomials 0x1021, 0x8005, 0x3D65, 0x8BB7, 0xA097, 0xC867, 0x0589; inits 0x0000, 0xFFFF, 0x1D0F, 0xB2AA, 0x89EC; both reflection settings; xorout 0x0000 and 0xFFFF — against both the padded whole field and the value-only range
Sources
  • emulation/ngage/web/src/loader/sisx.js
  • emulation/ngage/web/test/loader.mjs

This copy of Metal Gear Solid Mobile was a Nokia Shop download, not a Game Card

established

The retailer part shipped inside the .n-gage container says RetailerName Nokia Shop and the package installs as a partial upgrade; the physical N-Gage QD Game Card layout — E:\system\apps\<Game>\<Game>.aif beside <Game>.app — never applied to this title. Installing the <Game> SIS places 18 files: the executables under !:\sys\bin and the assets under \private\20010925, the game's SecureId in hex.

Check it at
  • samples/mgs/extracted/RetailerInfo.xml: <RetailerName>Nokia Shop</RetailerName>; samples/mgs/PROVENANCE.md records the install type partial_upgrade
  • E:\sys\bin\mgs_ngi.exe, E:\sys\bin\MetalGearSlibdl.dll, E:\private\20010925\{all.dz, mgs_ngi.s3e, warp.dz, …} — the 18 files EKA2L1's installer wrote from Game.sis
  • no .aif exists anywhere in the project's provenance: a byte search of Game.sis, Metadata.sis and the community launcher ngage-launcher-v1.40.1557.sis for '.aif' finds zero hits in all three

Why this matters beyond provenance: a reference emulator's Game Card route (EKA2L1's --mount and --runng, which read an AIF v2 registration blob — a compiled .rsc resource, registeration.cpp:238) cannot be used for this title without fabricating an artefact it never had. Unknown stays unknown; that is not a value to declare a model for, it is an input the title did not have. The launcher route — installing the N-Gage 2.0 launcher and selecting the game in its own Arena UI under \private\20003b78 — is a GUI interaction inside the guest and has no scriptable form.

Sources
  • emulation/ngage/samples/mgs/PROVENANCE.md
  • emulation/ngage/tools/eka2l1-capture.ps1 — -Route GameCard, the standing check that the route stays blocked for exactly these reasons

runtime.dll ships inside an embedded controller; the loader walks them and reproduces the installer

measured

The N-Gage launcher SIS carries 42 controllers. tools/sisx.py walks only the top-level one, so the 109 files it wrote are a subset of the 176 EKA2L1's installer produces from the same package — runtime.dll, the module mgs_ngi.exe imports ordinals 9 to 12 from, sits in the embedded package 0x102755AA. Since 2026-09-04 the loader walks every embedded controller: a controller's data unit is its own DataIndex added to its ancestors', and the install conditions (if and elseif blocks over exists, MachineUid, the comparison operators, and, or, not) are evaluated against the device. With EKA2L1's own device facts the loader installs the same e:\sys\bin set EKA2L1 did, file for file and byte for byte, bar the one file EKA2L1 writes itself.

Check it at
  • samples/launcher/ngage-launcher-v1.40.1557.sis, 11,083,484 bytes, sha256 92e671cd40f15e2ed1b6c1b8803f2ac1f089aa19d8e98e285474e06c75284354: 42 SIS controllers, with runtime.dll{2000107A} in the embedded controller of package 0x102755AA
  • samples/launcher/extracted/drive_!/Sys/Bin (108 files) and drive_c/Sys/Bin (1) — what tools/sisx.py wrote from unit 0; samples/launcher/installed/sys/bin (176) — what EKA2L1 0.0.8.2 wrote from an install of the same SIS plus MGS's Game.sis and Metadata.sis, copied out of C:\tools\eka2l1-capture\data\drives\e\sys\bin with SHA256SUMS beside it
  • runtime.dll: 10,471 bytes, uid3 0x2000107A, 41 exports; it imports euser 102, eikcore 34, avkon 29, CONE 22, WS32 18, centralrepository 5, AknCapServerClient 3, APGRFX 3, APPARC 1, ESTLIB 1, drtaeabi 43, scppnwdl 2, drtrvct2_2 1
  • emulation/ngage/web/src/loader/sisx.js: collectController adds the controller's DataIndex to the parent's unit; evaluate() is the condition table (operators 1 to 10, 15 and 16); DEVICE_5320 carries machine UID 0x2000DA5A (C:\tools\eka2l1-capture\data\devices.yml, RM-409 machine-uid 536926810) and ROM_5320 the 14 exists() facts the launcher asks about, each read off EKA2L1's ROFS dump of the same firmware, C:\tools\eka2l1-capture\data\drives\z\rm-409

The gap does not announce itself. The extraction succeeds, writes 109 files and reports nothing wrong; the missing module surfaces one hop away, in a different tool, as an import that resolves to nothing. That is precisely the shape the fail-loudly rule exists for — a reader that stops at the first data unit and reports what it wrote rather than what it skipped.

Graded measured, not established, on purpose: it is a comparison of two tools' outputs over one input, reproducible by re-running either, and not a fact read out of an instruction. What is established from the binaries is the consequence — mgs_ngi.exe imports ordinals 9 to 12 from runtime{2000107a}, so a run without that module cannot resolve them, whatever the file counts say.

The extraction tool still stops at the top-level controller, but the emulator no longer depends on it: the loader installs the launcher package itself, and samples/launcher/installed/sys/bin has become the oracle the loader is measured against rather than the thing it mounts. A device the loader holds no fact for — an exists() path outside ROM_5320, an attribute DEVICE_5320 does not declare — makes the parse fail with the path or attribute named, never a silently chosen branch. The launcher stays a community-patched build, a functional reference and not a pristine Nokia artefact.

Counts, with the method beside each
valuewhatmethod
109 and 176Sys/Bin files from the two routes over the same package: our extractor's, and EKA2L1's installcount of regular files under samples/launcher/extracted/drive_!/Sys/Bin (108) plus drive_c/Sys/Bin (1) against samples/launcher/installed/sys/bin; the install directory also holds MGS's six binaries and AS_20010925.exe, because Game.sis and Metadata.sis were installed onto the same drive
69module names present in the install and absent from the extraction, runtime.dll among themcase-insensitive comparison of the two sets of file names
4 and 4runtime.dll ordinals mgs_ngi.exe (9 to 12) and MGSfileaccess.dll import — every one of them unresolvable while the module was missingparseImports over both binaries, checked against the extraction's file list
494 of 494installable file descriptions across all 42 launcher controllers whose SISHash is the SHA-1 of the payload the ancestor-sum data unit rule selects — the rule is proven by the hashes, not assumedemulation/ngage/web/test/loader.mjs: sha1 over every extracted payload against the SISFileDescription hash; run on the launcher, Game.sis, Metadata.sis and the two fixture titles
175 of 176files in samples/launcher/installed/sys/bin the loader installs into e:\sys\bin with the same name and SHA-256; the 176th, activationengineserver.exe, matches no payload in any SIS because EKA2L1 replaces it with its own activation engine and keeps the SIS original as activationengineserver.exe.bak, which does matchemulation/ngage/web/test/loader.mjs: installSisx of the launcher, Game.sis and Metadata.sis into one VFS on drive e, then a name and sha256 comparison against SHA256SUMS in both directions; hiding two files from the VFS makes the assertion name them
14controllers the loader takes for the RM-409 device — the same fourteen, in the same order, that EKA2L1's install log reports registeringthe controller list parseSisx returns against the Package … registering lines of C:\tools\eka2l1-capture\install-launcher.log
Sources
  • emulation/ngage/web/src/loader/sisx.js
  • emulation/ngage/web/test/loader.mjs
  • emulation/ngage/samples/launcher/PROVENANCE.md
  • emulation/ngage/tools/sisx.py
  • emulation/ngage/tools/eka2l1-capture.ps1

MGSfileaccess.dll is not ngifileaccess.dll with one import renamed

established

The game's MGSfileaccess.dll carries the launcher's ngifileaccess.dll import table exactly — the same fourteen modules with the same ordinal counts — with caf.dll swapped for MetalGS_Crack.dll on the same nine ordinals, and MetalGS_Crack.dll is itself a caf.dll proxy. The code sections are not the same, though: they differ in length and in roughly four bytes out of five. The crack is more than a redirected import.

Check it at
  • samples/mgs/extracted/game/drive_!/sys/bin/MGSfileaccess.dll — 23,880 bytes, uid3 0x20003802, 1 export, stored uncompressed — against samples/launcher/installed/sys/bin/ngifileaccess.dll — 14,803 bytes deflated to 23,840, the same uid3 0x20003802, 1 export
  • both import fourteen modules with identical ordinal counts: euser 85, efsrv 41, runtime 4, DRMCommon 4, DrmRights 12, activationengineclient 4, PlatformEnv 5, apgrfx 4, apmime 2, bafl 2, drtaeabi 53, scppnwdl 2, drtrvct2_2 3 — and one name differs, caf{000a0000}.dll on 9 ordinals becoming MetalGS_Crack.dll on the same 9
  • MetalGS_Crack.dll, 4,547 bytes, uid3 0xF4F06529, 122 exports, imports caf{000a0000}.dll on those same 9 ordinals plus euser on 9: it stands between the game and the content-access framework

A renamed import would leave the code identical apart from the stubs, and it does not. Whatever else this module does, it was compiled, not patched at the import table — which is why the two numbers above are given as two: the whole-image figure of 18,687 and the code-section figure of 17,226 answer different questions, and quoting one as the other is the mistake this reference keeps a rule about.

The DRM shape survives in what it kept: DRMCommon, DrmRights, activationengineclient and caf, now reached through a 122-export proxy. What the changed code actually does has not been disassembled, and it is listed as open rather than guessed at.

Counts, with the method beside each
valuewhatmethod
18,687 of 23,840bytes that differ between the two inflated images over the length they share; the images are 23,880 and 23,840 bytesinflate both with web/src/loader/e32.js flatImage and byte-compare from offset 0
17,226 of 21,292differing bytes when only the code sections are compared — 21,332 against 21,292, so the game's copy is 40 bytes longer as well as differentthe same two inflated images, compared over each one's own [codeOffset, codeOffset + codeSize)
14 and 14imported modules on each side, with exactly one name changed between themparseImports over both inflated images
Sources
  • emulation/ngage/web/src/loader/e32.js — flatImage, parseImports
  • emulation/ngage/samples/mgs/PROVENANCE.md
  • emulation/ngage/samples/launcher/PROVENANCE.md

The S3E stage loader

From _E32Startup to blx r4: everything the stub does before the game exists

established

mgs_ngi.exe is a Marmalade S3E stage loader. It enters Thumb four instructions in, scans its own directory for exactly one file ending .s3e, sniffs that container's compression, decompresses it with an LZMA decoder statically linked into the stub itself, parses the XE3U image out of it, allocates code and data as two separate blocks, relocates against two different deltas, and enters the result with blx r4 on the main thread.

Check it at
  • entry ARM _E32Startup 0x8338; add ip,pc,#1 ; bx ip at 0x83F4 enters Thumb; import thunks are ldr pc,[pc,#-4] plus an ordinal slot — euser 1360's thunk 0xD1A8 is called at 0x841A (SetupThreadHeap), 641 at 0x842C, 585 in slot 0xD1FC
  • 0xE1C0 is the main-ish function, sole caller 0x22BAA at 0x22BF2; the exit flag is registry(2)+0xC4 through 0x1407C to 0x259E4; 0x25670 is the directory scan — opendir('.') 0x28538 with the string '.' at 0x25728, readdir 0x28600, the suffix '.s3e' at 0x2572C, a second match giving 'Multiple S3E files found in loader directory' at 0x2EAA8, none giving 'No executable to load' at 0x2EB30, and a failed listing 'Unable to list loader directory' at 0x25738
  • 0x257C4 opens it: fopen 0x19400 (mode 'rb' at 0x25840, error 'Can't open s3e file %s' at 0x25844), the reader slot 0x2356C (four 0x88-byte slots at registry(0x15)+0x20, vtable 0x187A5), the sniffer 0x1B8EC, the parser 0x1F540 ('Error reading s3e file %s' at 0x25860)
  • the sniffer 0x1B8EC reads two bytes: {0x01,0x20} is mode 3; a zlib CMF/FLG pair, the modulo-31 test through drtaeabi ordinal 155, is mode 2 and inflates through libz ordinals 33 to 36; an LZMA-alone header — properties at most 0xE1, dictionary at most 0x900000, bytes 9 to 13 zero — is mode 4; anything else is raw, mode 1
  • the LZMA decoder is inside the stub: 33 lsrs #11 ; muls range-coder pairs between 0xEE86 and 0xFE84, streaming through the reader vtable at 0x12820 blx r3
  • the parse 0x1F540: a 0x110-byte object from 0x2A688, the header copied to obj+0x88 (0x14F0C checks 'XE3U' 0x55334558 and rejects architecture 10 or above with 'Unknown processor architecture %d' at 0x254A0); 0x12724 allocates memSize minus dataOff through 0x2A6C0 into [obj+0xDC]; the code block comes from 0x2A724 (dataOff plus slack from 0x1D130) into [obj+0xD8] by way of registry(0x13)+0x64, a runtime.dll feature — LookupFeature is runtime ordinal 10, blx 0xD050 at 0x2A744 — vtable+0x14; fixups into [obj+0xD4]; 0x127B8 reads dataOff bytes into the code block and fileSize minus dataOff into the data block; the signature path 0x11D92 to 0x19F70 to the verifier 0x18428 ('Incorrect signature' at 0x1848C); ICF 0x16868; fixups 0x128CC (type 0 names, 'Symbol table error' at 0x129F4); the BSS memset at datablock plus (fileSize minus dataOff), 0x1F5FA to 0x1F612; 0x2ABF8(page-aligned code, length, 5) is movs r0,#1 ; bx lr; 0x25758 registers the code range through vtable+0x20
  • relocation 0x19ECC: ip = codeblock minus 0x4A000000, lr = datablock minus (0x4A000000 plus dataOff); the site is datablock + off − dataOff when off is at least dataOff and codeblock + off otherwise; each site gets += lr when the stored value is at least base + dataOff, and += ip when it is not
  • the jump: 0x255D4 computes 0x178A0 = [obj+0xD8] + [obj+0xB0]; at 0x25EB4 the byte at registry(0x17)+0x18 (0x2BE14) decides — 0 calls libc ordinal 313 and does blx r4 at 0x25ED6 on the main thread, non-zero goes to 0x27BB8 and a game thread ('Out of memory creating %dk stack for game thread' at 0x25F20) through an ARM trampoline blx #0x8000 that opens with mov sp,r0
  • mgs_ngi.s3e itself: SIS record 15 installed to !:\private\20010925\mgs_ngi.s3e, 481,006 bytes, LZMA-alone header 5d 00 00 01 00 d5 a5 10 00 00 00 00 00

The directory scan is why the stub needs its own private directory and why exactly one .s3e may be in it: two matches and it refuses by name, none and it refuses by name. There is no configured path anywhere — the container is found by suffix, in the current directory.

5d 00 00 01 00            properties 0x5D = lc3 lp0 pb2, dictionary 0x00010000 = 64 KiB
d5 a5 10 00 00 00 00 00   uncompressed size 0x0010A5D5 = 1,091,029 bytes
  • First two bytes 0x01 0x20 select mode 3.
  • A valid zlib CMF/FLG pair — the header word divisible by 31, tested through drtaeabi ordinal 155 — selects mode 2, which inflates through libz ordinals 33 to 36.
  • An LZMA-alone header — properties byte at most 0xE1, dictionary at most 0x900000, the top five bytes of the 8-byte size field zero — selects mode 4, the decoder linked into the stub.
  • Anything else is taken as raw, mode 1.

The relocation is the part a model gets wrong for free. Code and data are two allocations with two deltas, so an emulator that places the image at a single base and relocates everything against it produces addresses that are right for the code and wrong for the data — and the fault shows up far away, at whatever first dereferences a relocated global. The single load base in db/game/mgs/image.tsv is a convention of this project's HLE, not a property of the device.

The last call before the jump is worth not over-reading: 0x2ABF8, handed the page-aligned code range and a length, is movs r0,#1 ; bx lr. In this build the stub neither flushes the instruction cache nor changes protection on the code it just wrote.

Counts, with the method beside each
valuewhatmethod
1,091,029bytes the stub's own LZMA decoder produces from the 481,006-byte mgs_ngi.s3e — exactly the length of mgs_ngi.xe3u on diskthe 8-byte size field at offset 5 of the LZMA-alone header, read against the byte length of the extracted .xe3u
2separate allocations the loaded game occupies on the device: one code block, one data-and-bss block, relocated against two different deltasthe two allocation sites 0x2A724 and 0x2A6C0 and the two-delta relocation loop at 0x19ECC
Sources
  • emulation/ngage/db/game/mgs/image.tsv
  • emulation/ngage/tools/mgs_re.py
  • emulation/ngage/samples/mgs/PROVENANCE.md

The XE3U header, as the bytes actually fit it

established

mgs_ngi.xe3u opens with a fixed header whose every word checks against the file's own geometry: the ICF configuration text starts at 0x48 and its length carries you exactly to the fixup block, and the size the container's LZMA header promises is the file's length. The salvaged reader's section table at +0x10 and +0x40 does not describe these words.

Check it at
  • mgs_ngi.xe3u, 1,091,029 bytes: +0x00 'XE3U' 0x55334558, +0x04 version 0x00040E01 (S3E 4.14.1), +0x08 architecture 4, +0x0C fixup offset 0x58CB, +0x10 fixup size 0x12776, +0x14 code offset 0x18041, +0x18 file size 0xF244C, +0x1C memory size 0x11B700, +0x20 signature 0x10A48D with +0x24 0x8C, +0x28 entry 0, +0x2C ICF offset 0x48 with +0x30 ICF size 0x5883, +0x34 link base 0x4A000000, +0x40 extended header 8, +0x44 data offset 0xED98C
  • the header's own self-check: 0x48 + 0x5883 = 0x58CB, the fixup offset at +0x0C — the ICF text ends exactly where the fixup block begins
  • the ICF at file offset 0x48 opens [S3E] then StackSwitch=0 with the comment 'Temporary measure - disable stack switching whilst its tested'; its {OS=SYMBIAN} block sets DoubleBuffer=1 and, under [GL], EGL_BUFFER_SIZE=16 and EGL_DEPTH_SIZE=16, leaving EGL_SURFACE_TYPE commented out with the default named as WINDOW
  • the reader that got it wrong: the retired workbench lineage's xe3u.js, which looked for a section table at +0x10 and +0x40 — in this image the fixup size and the extended-header count. It was removed from the tree with the rest of that lineage on 2026-09-04, so the offsets are recorded here rather than cited
offsetwordwhat it is
+0x000x55334558the magic, 'XE3U' — the parser at 0x14F0C compares this word
+0x040x00040E01version, S3E 4.14.1
+0x084processor architecture; 10 or above is refused by name
+0x0C / +0x100x58CB / 0x12776fixup block offset and size
+0x140x18041code offset
+0x180xF244Cfile size — the end of .data
+0x1C0x11B700memory size — the end of .bss
+0x20 / +0x240x10A48D / 0x8Csignature
+0x280entry
+0x2C / +0x300x48 / 0x5883ICF offset and size
+0x340x4A000000link base, the value the relocation subtracts
+0x408extended header
+0x440xED98Cdata offset — the end of code and rodata

Those last three words are the whole section map, and they are what db/game/mgs/image.tsv already carries in HLE form: the load base 0x100000 plus 0xED98C, 0xF244C and 0x11B700 gives 0x1ED98C, 0x1F244C and 0x21B700, the three boundaries that table records. There is no section table to read; the file states four sizes and the loader derives the rest.

Which is where the salvaged reader went wrong. It looked for a section table at +0x10 and +0x40, and those offsets hold a fixup size and an extended-header count in this image — a table read there yields plausible small numbers and describes nothing. Reading a value that exists is not the same as reading the right value, and only the geometry check settles it: any layout that does not put the ICF's end at the fixup block's start is wrong about this file.

The ICF matters twice over. It is the game's own configuration, so it says what the game asks the platform for — a 16-bit colour buffer, a 16-bit depth buffer, double buffering, a window surface — and it is text, so it is the one part of the image that reads back without any disassembly at all.

Counts, with the method beside each
valuewhatmethod
0x10A5D5 = 1,091,029the file's length and the uncompressed size mgs_ngi.s3e's LZMA header declares — one number, two sourcesthe 8-byte size field at offset 5 of mgs_ngi.s3e against the byte length of mgs_ngi.xe3u
0x58CBboth the fixup offset at +0x0C and the sum of the ICF offset and size at +0x2C and +0x30read the three header words and add two of them
Sources
  • emulation/ngage/db/game/mgs/image.tsv
  • emulation/ngage/samples/mgs/extracted/game/drive_!/private/20010925/mgs_ngi.xe3u

MetalGearSlibdl's four ordinals are libdl's, and the salvaged table has them in the wrong order

established

MetalGearSlibdl.dll re-exports the ROM's libdl: ordinal 1 is dlclose, 2 is dlerror, 3 is dlopen and 4 is dlsym. The table this project salvaged from the older workbench says 1 dlopen, 2 dlerror, 3 dlsym, 4 dlclose — so a call the game makes as dlopen would be answered as dlsym, and the 0x101 the salvaged note reads as an ordinal handed to dlsym is dlopen's mode argument.

Check it at
  • the four stubs inside MetalGearSlibdl.dll: 0x2A158 dlclose, 0x2A16A dlerror, 0x2A19C dlopen with mode 0x101, 0x2A190 dlsym (which maps handle 0 to −2 and −1 to −1)
  • the ROM's libdl export directory 0x82E99A84: ordinal 3's body at 0x82E9952A, a strlen bounded at 0x100; ordinal 4's at 0x82E9967E, which rejects −1, −2 and −3; ordinal 1's at 0x82E99886
  • mgs_ngi.exe imports MetalGearSlibdl ordinals 1, 2, 3, 4 and 1308; the name 'hal.dll' is the string at 0x13FAC, passed at 0x13F72 through 0x26880
  • the salvaged table is quoted below rather than cited: it lived in the retired workbench lineage's symbian.js, removed from the tree on 2026-09-04, and this is its text as it stood
//   ord 1  dlopen(const char* name)        -> handle
//   ord 2  dlerror()                       -> const char*
//   ord 3  dlsym(handle, int ordinal)      -> void*    <- the one MGS calls
//   ord 4  dlclose(handle)                 -> int
export const METALGEARSLIBDL = { dlopen: 1, dlerror: 2, dlsym: 3, dlclose: 4 };

The boot corroborates the real mapping from the other side. The frontier's history records the run halting at metalgearslibdl ordinal 3, and what that import turns into once it is answered is a library load — hal.dll, through !Loader function 2. That is dlopen's job. Under the salvaged table, ordinal 3 is dlsym, and a symbol lookup does not load anything.

The salvaged comment then reads the 0x101 at that site as an ordinal passed to dlsym. It is the mode argument the dlopen stub at 0x2A19C passes. A wrong table with a plausible comment beside it is worse than no table at all: it is the exact shape of a value that survives review because it reads as though someone checked.

Counts, with the method beside each
valuewhatmethod
1 of 4ordinals the salvaged table gets right — only 2, dlerror; dlopen and dlsym are transposed and dlclose is at the other endthe four stub bodies in MetalGearSlibdl.dll against libdl's export directory at 0x82E99A84, compared with the constant the retired lineage carried
Sources
  • emulation/ngage/db/frontier.json — history: unimplemented import metalgearslibdl@3
  • emulation/ngage/web/src/loader/e32.js — parseImports
  • emulation/ngage/db/os/fs-op.tsv — !Loader 2

What the old HLE lineage actually reached: three pixels in a thousand, and no GL at all

measured

Seance's HLE mode runs the XE3U against a faked S3E API. Measured on 2026-09-04 it ran 120 million steps without halting, presented 797 surfaces of 176 by 208 in RGB565, and had 0.3 percent of the pixels non-zero at 60 million steps. GLES was never instantiated: zero GL calls. This is the record of why the lineage was retired, and it is kept because the alternative is repeating it.

Check it at
  • web/public/js/lib/symbian_seance/s3e.js and symbian_hle.js — the faked S3E API and the SVC layer under it
  • symbian_hle.js lines 141 to 145: the EUSER ordinal table mapping 1 to 5 onto User_Alloc, User_AllocL, User_AllocLC, User_AllocZ and User_AllocZL — assigned by inference, never read out of EUser.dll's export directory
  • symbian_hle.js line 219 with its siblings at 253 and 330: the unknown-call default hands out a fresh handle from the counter at line 378, so a call nobody implemented still succeeds
  • the retired workbench lineage's mgs-patches.js, removed from the tree on 2026-09-04, opened with a note that MGS reaches a clean User::Exit(0) at about 20,000 steps — that number is the STUB exiting, not the game's own code running, and it is the kind of figure that gets quoted forward as a frontier

Two of those numbers are the finding and the rest are context. 120 million steps without a halt reads like progress and is its opposite: when the SVC layer answers an unknown call with a fresh handle, nothing can fail, and the run walks forever through code whose preconditions were never met. The pixel count is what says so out loud — after 60 million steps the surface is essentially empty, and the 797 presents are presents of nothing.

      1: () => this.User_Alloc,
      2: () => this.User_AllocL,
      3: () => this.User_AllocLC,
      4: () => this.User_AllocZ,
      5: () => this.User_AllocZL,

        // For unknown exec calls, return a unique handle instead of 0

The ordinal table is the other half of it, and the two fragments above are the whole disease in seven lines. Ordinals were assigned to allocator entry points by inference rather than resolved through the real export directory, and an unrecognised call was answered with a fresh handle rather than refused. Compare the LLE path: the same ordinals resolve through EUser.dll's own directory, and one outside the table is refused rather than answered.

The lineage is not deleted for the same reason this entry exists. A faked platform API produces a run that looks alive and proves nothing; the frontier only started meaning something once a halt became the honest answer.

Counts, with the method beside each
valuewhatmethod
120,000,000steps run without a haltthe Seance HLE run of 2026-09-04 over mgs_ngi.xe3u with the faked S3E API
797s3eSurfaceShow presents of a 176 by 208 RGB565 surfacethe same run, counting the presents the faked API served
0.3 percentnon-zero pixels of the presented surface at 60 million stepsthe same run, sampled at 60 million steps
0GL calls: the GLES context was never createdthe same run
Sources
  • web/public/js/lib/symbian_seance/symbian_hle.js
  • web/public/js/lib/symbian_seance/s3e.js
  • emulation/ngage/db/frontier.json

The graphics path

libgles_cm is a software GL that presents through the window server

established

The 5320's libgles_cm.dll issues no supervisor call of its own: it reaches the device through 69 import thunks, thirty of them into EUser and thirteen into Ws32. eglCreateWindowSurface builds an RWindow, a CWindowGc and a CWsBitmap; eglSwapBuffers BitBlts that bitmap into the window between three window-server ops and ends at op 0x13. A frame becomes visible when the window server says so — so the first thing an emulator needs here is a window server, not a GPU.

Check it at
  • libgles_cm.dll: TRomImageHeader 0x8062D660, uid3 0x101FCABD, code 0x8062D6D8 size 0x30B50, 175 exports at 0x8065A140, version 2.1.576, zero SVC instructions
  • the import thunks: 69 of them, eight bytes each, from 0x8065A3FC through 0x8065A61C, every one ldr pc,[pc,#-4] followed by its target word
  • Ws32 ordinals 224, 264, 266, 275, 276, 278, 351, 358, 359, 361, 384, 387 and 490; FbsCli 14, 25, 27, 107, 108, 109, 117 and 133; Cone 136, 182 and 191 = 0x805721AD, 0x80571EA9, 0x80571EB1
  • export VAs: 1 eglChooseConfig 0x8062ED20, 3 eglCreateContext 0x8062FC4C, 6 eglCreateWindowSurface 0x8062F348, 14 eglGetDisplay 0x8062FDA0 (cmp r4,#0 ; cmpne r4,#1 ; moveq r0,#1 ; movne r0,#0), 17 eglInitialize 0x8062EA5C, 18 eglMakeCurrent 0x8062FF10, 22 eglSwapBuffers 0x806304D0, 31 glClear 0x806344AC
  • eglCreateWindowSurface allocates 0x44 at 0x8062F3F4, stores the native window at surface+0x34 (0x8062F42C) and the config at surface+0x1C; 0x8062F058 takes Cone 0x80571EA9 as its environment — an RWsSession at env+0x2C and a CWsScreenDevice at env+0x48, those two names inferred rather than read — and builds: new 0x18 with Ws32 278 and Ws32 275 (an RWindow), new 0x14 with Ws32 351 on [env+0x48] and vtable+0x10C (a CWindowGc), new 0x20 with Ws32 361 on env+0x2C (a CWsBitmap), Ws32 387 for the size and Ws32 359 Create(size, mode) whose mode comes from TLS-state+0x1C and not from surface+0x1C; on success the bitmap goes to surface+0x3C and the gc to surface+0x40, at 0x8062F160 and 0x8062F164
  • eglSwapBuffers 0x806304D0 reaches the TLS state at 0x8062D760 and then 0x806399D0; when [surface+0x30] is zero it runs 0x80630408 — Ws32 490 (a size copy), EUser 1652, Ws32 264 = window op 0x10, gc vtable+0x110 Activate, Ws32 266 = op 0x12, gc vtable+0xE4 BitBlt(TPoint(0,0), the CWsBitmap at surface+0x3C), Ws32 276 = op 0x13, gc vtable+0x114 Deactivate
  • the three ops are literally movs r1,#0x10, movs r1,#0x12 and movs r1,#0x13 followed by bl 0x804AFD8E, at 0x804B2DAA, 0x804B2D56 and 0x804B2D8E
  • '!Windowserver': the TLitC16 length word at 0x804B6618 in Ws32.dll (header 0x804AFC60) and at 0x804AA60C in EwSrv.exe (ROM header 0x80489720, uid3 0x10003B20, code 0x80489798 size 0x262B4, TRomEntry 0x80049F4C, size 156,872) — EwSrv.exe is not among the extracted sys/bin files. '!Fontbitmapserver' is at 0x8046B40C, and FbServ.exe is extracted
  • the game's own side: the XE3U imports its EGL and s3e entry points from the stub by name — eglSwapBuffers at 0x624C, eglCreateWindowSurface at 0x61B2, eglGetDisplay at 0x6200, s3eGLGetNativeWindow at 0x5B08, s3eSurfaceShow at 0x5DFF, s3eGLGetInt at 0x5D53 — and its ICF asks, under {OS=SYMBIAN}, for EGL_BUFFER_SIZE 16 and EGL_DEPTH_SIZE 16 with the surface type left at its default, WINDOW
  • ngiOpenGLControl.dll in the launcher (uid3 0x2000F7CB) dlopens 'libGLES_CM.dll' with mode 0x100 and then looks up 42 ordinals by name string — 12, 13, 11, 18, 132, 90, 82, 98, 52, 56, 67, 29, 57, 53, 141, 121, 30, 47, 140, 7, 1, 133, 126, 15, 134, 135, 131, 125, 128, 120, 130, 38, 55, 21, 3, 42, 22, 8, 70, 68, 17, 23 — never 6 and never 14, and it carries the strings /hardware/opengl, version1x, texturecompressionformat and GL_IMG_texture_compression_pvrtc

So the shape of the first slice is fixed by the binary rather than chosen: HLE !Windowserver and !Fontbitmapserver at the session boundary the way !FileServer and !Loader already are, and copy the CWsBitmap's pixels to the canvas at op 0x13. Nothing above that seam needs modelling — the GL itself is ordinary ARM code in the ROM and can simply run.

One name in the thunk table is worth stating with its method, because it arrived as a guess. The three thunks into uid3 0x10003B0B land in the module whose ROM header is 0x80534D80, and two launcher modules, runtime.dll and ngiutil.dll, decorate that same uid3 in their own import tables as ESTLIB. Two independent places, so it is established rather than assumed. The remaining ten thunks are the RVCT runtime, where uid3 is 0 for every module: a uid3-to-name map merges them and will confidently name the wrong DLL, so those are attributed by header VA.

EwSrv.exe, the window server itself, is XIP-only — it is not among the extracted sys/bin files, which is exactly why the session boundary is the right seam. And the two server names are read out of the ROM the same way !FileServer was: the literal, at the address, in the image that owns it.

The launcher's ngiOpenGLControl.dll is a capability probe and not the render path. It asks for 42 ordinals and never for 6 or 14 — never for a display and never for a window surface — while carrying strings about hardware OpenGL and PVRTC texture compression. It is what the platform asks the device about itself; the game's own frames go through the stub.

The op names InvalidateFull, BeginRedrawFull and EndRedraw come from EKA2L1's enum order and are cross-reference only. The op numbers are read from the instructions; the names are a second opinion and nothing here depends on them.

Counts, with the method beside each
valuewhatmethod
69import thunks, and where they land: EUser 30, Ws32 13, FbsCli 8, Cone 3, ESTLIB 3, Hal 1, Gdi 1, and 10 into the uid3-0 RVCT runtime between 0x8018E048 and 0x80198F73read the 69 eight-byte thunks from 0x8065A3FC, assert each is ldr pc,[pc,#-4] (69 of 69 are), and attribute every literal to the ROM module whose [code_address, code_address + code_size) contains it, the module set coming from the uid1 plus code_address = headerVA + 0x78 sweep of 5320.rom
0supervisor calls in libgles_cm's 0x30B50-byte code section: it reaches the system only through those thunksa disassembly sweep of the code section at 0x8062D6D8 for supervisor-call encodings, corroborated structurally by the thunk table — a DLL reaches the kernel through EUser's wrappers, so the 30 EUser thunks are where its system calls are
3window-server ops one presented frame costs — 0x10, 0x12 and 0x13, around a single BitBltthe disassembly of 0x80630408, each op number read from the movs immediate inside the Ws32 wrapper it calls
Sources
  • emulation/ngage/db/os/hle-server.tsv — the servers answered at the session boundary
  • emulation/ngage/samples/firmware/5320/5320.rom
  • emulation/ngage/samples/launcher/installed/sys/bin/ngiOpenGLControl.dll

Cross-references: EKA2L1

EKA2L1, the reference emulator: what it is and what it may be used for

cross-reference

EKA2L1 0.0.8.2 (build master-e67f84dc6, GPL-3.0) runs the owner's own Nokia 5320 firmware as an installed RM-409 device on platform epoc93fp2 — on the byte-identical EUser.dll this project boots. It is a second, independent implementation of the same kernel seam, and that is the only thing it is used for: a cross-reference can corroborate or contradict a value here; it is never the source of one.

Check it at
  • C:\tools\eka2l1\eka2l1_qt.exe, config.yml and data\devices.yml (RM-409: platver epoc93fp2, model Nokia 5320d-1 (01.01), machine-uid 536926810); source pinned at commit 46ef1ebdcfbd7454c080d0f1efb5fb54cfa7f622 — every file:line in this reference is against it
  • EUser.dll sha256 4d12cbb4…, 206,216 bytes — identical between samples/firmware/5320/sys/bin/EUser.dll and EKA2L1's Z: drive
  • device install: 5320.RPKG (118,133,487 bytes, sha256 74ea44213f7591309ed8c6267f1bad2b724e037488e81f0875d45cbc09be9bf5) plus 5320.rom (68,759,552 bytes), kept at C:\tools\eka2l1-ref outside any repo; the RPKG is required because should_install_requires_additional_rpkg() (system/src/installation/rpkg.cpp:43-66) returns true whenever rom_base differs from EKA1_ROM_BASE 0x50000000 (loader/include/loader/rom.h:38) — on EKA2 the Z: drive lives largely in ROFS, outside the .rom
  • z:\resource\versions\sw.txt = 05.16 / 29-April-2009 / RM-409 / (c)Nokia; model.txt = Nokia 5320d-1 (01.01) — both UTF-16LE with a byte-order mark

Where the two disagree, the firmware wins — and where they agree, the agreement is worth exactly one second opinion. EKA2L1 reimplements the Symbian kernel in C++ and traps SVCs; it never executes ekern's ARM code. So does this project. That makes the executive-call boundary the one place two independent implementations are definitionally comparable, and it is where the differential oracle diffs them.

Two structural differences keep the roles straight. EKA2L1's exec tables are hand-maintained C++ literals with no provenance, which is how a v94-shaped number can sit under a v93 device unnoticed; every value here is a table row with an evidence column and a gate that fails when generated code drifts from it. And EKA2L1 logs an unimplemented system call and carries on, while this dispatcher HALTs with the handler VA and the disassembly command — it refuses to fabricate. EKA2L1 is far ahead in coverage: it boots the whole device, a full service layer, graphics and audio, 148 slow execs for v93. It has no savestate, no instruction trace and no reproducible run; this project can diff two of its own runs to the instruction, and EKA2L1 cannot.

Counts, with the method beside each
valuewhatmethod
8,495 files, 117,304,613 bytesthe Z: drive EKA2L1 extracts from the RPKG, paths lowercased with the z:\ prefix droppedthe install script's extraction count; EKA2L1's own --listapp then enumerates 119 ROM applications
Sources
  • emulation/ngage/tools/eka2l1-capture.ps1
  • emulation/ngage/tools/oracle-diff.mjs
  • the source-of-truth precedence in CLAUDE.md (formerly db/SCHEMA.md)

EKA2L1's SVC trace: the line shape, what in it is ground truth, and where the traces live

cross-reference

With log-svc true and log-filter *:trace, EKA2L1 writes one line per executive call in the shape T <file>:<line> [Kernel]: Calling SVC 0x75 dll_set_tls. The number is ground truth — the raw 24-bit immediate the guest instruction encoded, bit 23 marking a fast exec; the name is EKA2L1's interpretation. Arguments and return values are not logged.

Check it at
  • kernel/src/libmanager.cpp:1052-1083 passes the raw immediate through unmasked as the map key; the log line is libmanager.cpp:1076
  • config.yml beside the exe (keys from config/include/config/options.inl): log-svc, log-ipc, log-filter; the shipped default filter contains Kernel:Warn, which silences trace, so log-svc alone produces nothing
  • EKA2L1-only traffic to normalise away before diffing: 0xFF (its ROM-export trampoline) and 0xC10000 / 0xC10001 (its HLE dispatcher escapes)
  • traces, frozen outside any working tree: C:\tools\eka2l1\traces\boot-2026-08-07.log — the device boot, 148,842 lines, sha256 a5007e8532b85e5847f1c2d35c2fbd2cd80b10e00ccd580fc87fd729cf8709b6; mgs-2026-09-02.log — mgs_ngi launched through --run e:\sys\bin\mgs_ngi.exe, 410 lines, sha256 7f1a777fc4cbaf323e0fdf28aa2568228ecf102a2d111857eb78dac4d1c72f4e; mgs-runng-blocked-2026-09-02.log — the Game Card route, exit −1 with the message Can't find the game mounted in E drive!
  • the capture rule: EKA2L1 truncates EKA2L1.log on every start, so nothing is ever launched from C:\tools\eka2l1 itself — runs go from the robocopy twin C:\tools\eka2l1-capture, and the reference log's hash is checked before and after (tools/eka2l1-capture.ps1)

Every process in the trace begins with the same deterministic prefix, which is _E32Startup and is the same code in both emulators because it is the same EUser.dll: heap_switch 0x800002 → handle_duplicate 0x6D → handle_close 0x69 → duplicate → close → dll_set_tls 0x75 → static_call_list 0x9F → heap 0x800001 three times → set_trap_handler 0x800009 → trap_handler 0x800008. The oracle aligns on it, diffs the number streams as sequences with a longest-common-subsequence, and prints the first divergence classified: an insertion is timing noise, a substitution or a missing call at the same landmark is a defect.

The log carries no per-line process or thread id; the tool reconstructs scope from the two boundary markers the stream does carry (Spawned process: X and the thread-rename line), so --process is a best-effort replay of the log's own order. And the trace has a known limit for this game: EKA2L1's raw-path launch spawns mgs_ngi with entry point 0 and dies on an access violation at address 0 in thread Main (kernel.cpp:914, kernel.cpp:363). That is EKA2L1's real behaviour on this input, reported as data; 150 calls is what the reference covers.

Counts, with the method beside each
valuewhatmethod
143,069executive calls in the device-boot trace, from a 75-second runcount of Calling SVC lines in boot-2026-08-07.log
150 and 50mgs_ngi-scoped calls on EKA2L1's side and ours in the game trace, neither containing the literal _E32Startup landmark (landmark index −1 on both)tools/oracle-diff.mjs --process=mgs_ngi, confirmed by calling its run() directly
4harness-only calls that precede the shared body on this project's side — three 0x800001 heap creates and one 0x800009 set_trap_handler from the bootstrap allocator installer — after which both traces converge on hal_function, chunk_new, hal_function, chunk_max_size, chunk_adjust, chunk_base, hal_function, semaphore_createthe first divergence oracle-diff.mjs reports, seen identically against the boot trace and the game trace
Sources
  • emulation/ngage/tools/oracle-diff.mjs
  • emulation/ngage/web/test/oracle-diff.mjs
  • emulation/ngage/tools/eka2l1-capture.ps1
  • emulation/ngage/web/test/run-rom.mjs — NGAGE_EXEC_LOG

Where EKA2L1's model and the firmware part ways

cross-reference

EKA2L1's tables and semantics were checked against the disassembly wherever they touched this project's frontier. Its exec numbering and its call-list order were confirmed by the binary, its reading of this project's table was corrected by it, and one difference is a genuine divergence between its HLE and ekern.

Check it at
  • the exec table: libmanager.cpp:1268-1270 maps epoc93fp2 to svc_register_funcs_v93 (svc.cpp:5996), where 0x9F = static_call_list, 0xA0 = library_detach, 0xA1 = library_detached; the v94 table is shifted one higher and has 0xA0 = static_call_list — the likely origin of this project's old number
  • EUser thunks, read from the binary: svc 0x9F at 0x8019C310, svc 0xA0 at 0x8019C318, svc 0xA1 at 0x8019C320 — an 8-byte-stride ARM table of svc #n ; bx lr wrappers, image base 0x80198FF8 (8 of 8 votes from the anchored offsets)
  • the call list: kernel/src/codeseg.cpp:656-694 queries_call_list — one E32 entry point per codeseg, dependencies depth-first then self last, each codeseg once per process ever (FLAG_EP_QUERIED, codeseg.h:112-116); entry points pushed as-is for ROM/XIP (codeseg.cpp:686-690); invoked in returned order with the DLL reason in r0 (libmanager.cpp:1085-1214)
  • the return value: svc.cpp:1951-1975 static_call_list has exactly one return statement and it is error_none — an empty list is *total = 0, never an error; utils/include/utils/err.h: error_not_found = −1 (line 10), error_eof = −25 (line 34)

Confirmed by ekern: 0x9F is the process static-call list (handler 0x80089994, walking DProcess::iCodeSeg deps-first, self-last through DCodeSeg::TraverseDeps 0x800874D8 — the order codeseg.cpp describes); entry points are the veneers exactly as they stand in the header, never +8; and 0xA0 is not the constructor path (handler 0x80089A84 walks the pending-library-detach queue at DThread+0x138). Corrected by ekern: the suggestion that this project's exec-number-to-handler extraction was off by one in that region. It was not — the slow numbers were right and the naming was wrong; the table that had been shifted was the fast column, for a different reason.

The genuine divergence: ekern returns −25, KErrEof, from 0xA0 when the detach queue is empty, and −21 from 0x9F once its one-shot bit in DThread+0x2C has been consumed; EKA2L1's static_call_list cannot return an error at all. Both mean nothing more to run, but an emulator diffed against EKA2L1 at that seam will see a substitution there and must know it is the reference that simplifies. The ledger's old gloss of −25 as KErrNotFound was wrong in name only; the value the handler loads with mvn r4,#0x18 is −25.

Also read from the trace and worth keeping: EKA2L1's own EKA1 bootstrap allocates 100 entries for the call-list buffer, a calibration figure for capacity; this project's boot asks with capacity 256 and receives 8.

Counts, with the method beside each
valuewhatmethod
5 and 26times the firmware issues exec 0x9F and exec 0xA0 in the boot trace — distinct calls, both real; 0x9F once per process, always in the _E32Startup shapecount per number over the 143,069-call boot trace
6 and 6occurrences of 0x9D library_attach and 0x9E library_attached, at identical positions — a textbook attach pair with 0xA0 interleaved among themthe same count
Sources
  • emulation/ngage/db/os/exec-impl.tsv — slow 0x9f, slow 0xa0
  • emulation/ngage/db/frontier.json — history: exec 0xa0 mis-identified as the C++ constructor path

The capture route that works, and what an SVC trace cannot attribute

cross-reference

With the launcher's runtime installed, EKA2L1 gets Metal Gear Solid Mobile far enough to create screen devices and set the display orientation: 271,617 SVC lines on 2026-09-04, against 410 for the raw-path launch of the same binary. What the trace still cannot do is attribute — it carries no per-line process or thread id — and not one of those lines is a gles call, because EKA2L1 answers EGL and GL above the executive boundary.

Check it at
  • the route: install the launcher SIS, and MGS's Game.sis and Metadata.sis, into EKA2L1's E: drive so that runtime.dll is present, then launch; the earlier raw-path launch of mgs_ngi.exe died at entry point 0 after 410 lines
  • the observed behaviour on that route: screen devices created, display orientation set to 3 and then to 0
  • the capture rule is unchanged and is the reason the reference logs survive: EKA2L1 truncates EKA2L1.log at every start, so runs go from the robocopy twin C:\tools\eka2l1-capture and the reference log's hash is checked before and after (tools/eka2l1-capture.ps1)

The zero is the informative number, and it is a fact about EKA2L1 rather than about the device. EKA2L1 implements EGL and GLES as host calls above the executive boundary, so its trace is silent exactly where the graphics path needs a reference: the ROM's own libgles_cm never executes there. The oracle can therefore corroborate the kernel seam underneath the graphics path — the window-server session, the bitmap handles — and nothing above it.

The attribution limit from the trace entry above still stands and matters more at this size. The log carries no per-line process or thread id, so scope is reconstructed from the two boundary markers the stream contains; a 271,617-line trace of a device running the launcher is mostly not the game, and no count taken from it may be quoted as the game's without saying how the scope was cut.

The route is worth having for one thing in particular. It shows the title starting under an independent implementation once runtime.dll is present, which is what turned a missing module from a hypothesis into a measured fact.

Counts, with the method beside each
valuewhatmethod
271,617SVC lines the launcher-runtime route produced on 2026-09-04, against 410 for the raw-path launch of the same binarycount of the trace's executive-call lines in the captured log
0gles lines anywhere in that tracethe same log, searched for the graphics calls
Sources
  • emulation/ngage/tools/eka2l1-capture.ps1
  • emulation/ngage/tools/oracle-diff.mjs
  • emulation/ngage/samples/launcher/PROVENANCE.md

What each exec really does

Projected from db/os/exec-impl.tsv joined with the dispatch tables read out of ekern.exe. An exec appears in the first table only when its behaviour is fixed by evidence from the firmware; every other number the kernel dispatches HALTs the emulator with its handler address and the disassembly command to read it.

42 execs established, with their handler address

execekern handlerflagsimplementationwhat it really doesevidence
fast 0x000x80068708wait_for_any_requestdispatched (count--; blocks the thread and switches to the ready-queue head; with nothing ready the model HALTs as scheduler-idle, naming what is waited on)
evidence

Exec::WaitForAnyRequest, dispatched by the SVC vector BEFORE the fast exec table: 0x80068130 `lsls ip,ip,#9` (C = bit 23, Z = number 0), 0x80068134 `blo` slow, 0x80068138 `ldr r1,[fp,#0x130]` = TScheduler.iCurrentThread, 0x8006813c `beq 0x80068708`. So docs/data/ekern-exec-table.tsv's 'fast 0x0 = 0x8006a674' is wrong for the same reason its whole fast column is: 0x80069e1c holds the count and word n the handler of exec n, so 0x8006a674 is fast exec 1's. The body 0x80068708: `ldr r2,[r1,#0x20] ; subs r2,r2,#1 ; str r2,[r1,#0x20] ; bxge lr` decrements NThread::iRequestSemaphore.iCount and returns at once (to `ldm sp!,{fp,pc}^`, r0 untouched) if the result is still >= 0; otherwise `str #1,[TheScheduler+0x10c]` (iKernCSLocked), `strb #1,[+0x108]` (iRescheduleNeededFlag), `str &sem,[r1,#0x28]` (iWaitObj), `strb #2,[r1,#9]` (iNState = EWaitFastSemaphore), `bl 0x800687ac` (unlink from the ready queue) and `b 0x80068c40` (the reschedule, which saves SP at NThread+0x68 and runs the highest-priority ready queue's head). The semaphore is signalled by NFastSemaphore::SignalN 0x80068750 / Signal 0x8006880c, reached from NKern::ThreadRequestSignal 0x8006ea50 (= slow exec 0x3b, and the tail of every request completion); nothing else can wake the thread. Caller in this boot: User::WaitForRequest 0x801a4594 `mvns r4,#0 ; loop: adds r4,r4,#1 ; blx 0x8019bd58 ; ldr r0,[r5] ; cmp r0,r6 ; beq loop` on the KConnectMessage's TRequestStatus.

fast 0x010x8006a674allocator_getdispatched (the thread allocator: the bootstrap allocator until the guest installs its RHeap via fast 2)
evidence

ekern 0x8006a674 `ldr r0,[r1,#-0x278] ; bx lr` with r1 = TScheduler.iCurrentThread (NThread = DThread+0x2c0), i.e. DThread+0x48 = iAllocator — the word HeapSwitch (fast 2) writes. It is fast exec 1, not 0: the fast table's word 0 at 0x80069e1c is the count (see fast 0x02's evidence for the vector's indexing).

fast 0x020x80082140heap_switchdispatched (installs the guest RHeap as the thread allocator; returns the previous one)
evidence

CORRECTS a fabricated row (the old evidence pointed at 0x8006a67c, which is a `b 0x8006a6b8` trampoline, and its `ldr r0,[r1,#-0x248]` text is the bytes at 0x8006a684 read through the 128-byte-header tool). The fast exec table is indexed count-first: the SVC vector 0x80068140..0x80068154 does `ldr r2,[r1,#0x60] ; ldr r3,[r2],ip,lsr #7 ; ldr r2,[r2] ; cmp r3,ip,lsr #9 ; bxhi r2`, i.e. word 0 of the table is the count and word n the handler of fast exec n; the table NKern::ThreadCreate installs (0x8006f38c literal) is 0x80069e1c = 0x1c, and its word 2 (0x80069e24) is 0x80082140. THAT handler is `ldr r1,[TheScheduler+0x130] ; sub r1,r1,#0x2c0 ; ldr r2,[r1,#0x48] ; str r0,[r1,#0x48] ; ldr r3,[r1,#0x4c] ; cmp r3,#0 ; streq r0,[r1,#0x4c] ; mov r0,r2 ; bx lr` = Exec::HeapSwitch: install r0 as DThread::iAllocator (+0x48, the word fast exec 1 returns), record it in iCreatedAllocator (+0x4c) if that is still null, return the previous allocator. euser corroborates: UserHeap::SetupThreadHeap 0x801b2d2c calls 0x801a4722 = `blx 0x8019bd68` (= `svc #0x800002`) with r0 = the RAllocator it just created or was handed in SStdEpocThreadCreateInfo.iAllocator. EKA2L1's own trace names the same exec heap_switch. Live in the boot: called with r0 = the RHeap built inside the chunk exec 0x6a created — the previous model ignored the switch and every later allocation went through the host bootstrap allocator instead of the guest RHeap.

fast 0x080x8006a694trap_handler_get[TLD+4]
evidence

euser GetActiveCleanup 0x801a19ba: svc8 ; ldr r0,[r0,#4]

fast 0x090x8006a69cset_trap_handlerstores [TLD+4]
evidence

euser CTrapCleanup::New 0x801a18ca -> SetTrapHandler 0x801a4552

fast 0x130x80072b0crom_header_addressUserSvr::RomHeaderAddress — returns TheRomHeader = the mapped ROM base
evidence

ekern fast table word 0x13 at 0x80069e68 (count-first table at 0x80069e1c, see tools/gen-exec-table.mjs) = 0x80072b0c: `ldr r0,[pc,#-0xe8c] ; ldr r0,[r0] ; bx lr` over the literal pool word 0x80071c88 = 0xc800004c, a kernel global in ekern's writable data (kern_data_address 0xC8000000). It is NOT a tick counter (the earlier frontier note was wrong — the same literal serves 18 readers because it is the ROM header pointer, not because they are timer code): the ONLY writer is 0x80072a84 `str r0,[r2]` (r2 = the literal) inside 0x80072a74, the first call of the kernel entry sequence 0x80068110 `tst pc,#0 ; b 0x8006e430` -> 0x8006e434 `bl 0x80072a74`, which stores r0 there, r1 in the neighbour 0xc8000050, and copies [r0+0x100+4i] -> [r1+0x108+4i] for i<8 (0x80072a8c..0x80072aa4 = TRomHeader.iTraceMask -> the debug-mask words fast execs 0xc/0xd/0xe read). Every reader dereferences it as a TRomHeader*: 0x800719cc..0x800719d4 `ldr r0,[r0] ; ldr r5,[r0,#0x94]` = iRomRootDirectoryList (struct tromheader +0x94, already decoded live), 0x80071a58/0x80071a5c +0xd0/+0xcc = iExtensionFile/iVariantFile, 0x80072850..0x8007285c +0x80/+0x84 = iTime, 0x80072970/0x80072980 +0x148/+0x14c = iPageableRomStart/Size, 0x80078300..0x8007831c +0xf4 = iUncompressedSize page-rounded, 0x80071b9c..0x80071ba8 `add r1,r1,#0x7c ; bx r1` = iRestartVector. The VALUE on this ROM is the ROM base: the header's own iRomBase (+0x8c) reads 0x80000000, iUncompressedSize (+0xf4) reads the exact byte length of 5320.rom, and that is where memory.js maps the ROM. The impl returns mem.romBase and HALTs unless those two header words agree with the mapping. Caller on the efile lane: efile.exe 0x801e3cb8 `push {r4,lr} ; blx 0x801e4b5c` (import veneer `ldr pc,[pc,#-4]` -> 0x8019bdf0 = EUser ordinal 1223, the ARM wrapper `svc #0x800013 ; bx lr`) then `ldr r1,[pc,#0xa4] ; str r0,[r1]` into efile's global at 0x0040010c. EKA2L1 (cross-reference only) names SVC 0x800013 user_svr_rom_header_address.

slow 0x010x800822980xe0000003rchunk_basedispatched (RChunk::Base = DChunk+0x2c)
evidence

CORRECTS a fabricated row (the old one took 'RChunk::Size' from the defective euser-svc-map and returned the committed size). Handler 0x80082298 = `ldr r0,[r0,#0x2c] ; bx lr` = DChunk+0x2c, and its use fixes the field: UserHeap::ChunkHeap calls the exec through 0x801a4278 right after the Adjust (0x801b04d8) and does `adds r1,r0,r7 ; str r1,[sp,#0x24]` = base + offset, the address it then constructs the RHeap at (0x801b04dc..0x801b0502). Live proof from the boot: with the old return value 0x40000 (the committed size) the very next exec was HAL::Get writing the page size to 0x40048 = that value + RHeap's +0x48 field, and HeapSwitch was then called with 0x40000 as the allocator — the heap had been built in low memory at the size, not at the chunk. +0x2c is RChunk::Base().

slow 0x030x800822a80xe0000003rchunk_max_sizedispatched (RChunk max size = DChunk+0x28; HALTs on an unknown handle)
evidence

CORRECTS a fabricated row. The handler for slow 0x3 is 0x800822a8 (ROM word at slow-exec dispatch-table entry 0x80069e9c + 3*8; the table base is fixed by 0x8006a394 = 0x80089994 for 0x9f, and all 241 slow rows of docs/data/ekern-exec-table.tsv re-read from the ROM match it). The bytes THERE are `e5900028 e12fff1e` = `ldr r0,[r0,#0x28] ; bx lr` — a DChunk field read, not a constant. `mov r0,#0xf ; bx lr` (e3a0000f e12fff1e) lives at 0x800822b0, which is the handler of exec 0x4: the old evidence was collected through tools/disasm.py with the 128-byte header, which reads a standalone image 8 bytes = one entry late. Which field +0x28 is, is fixed behaviourally: euser's UserHeap::ChunkHeap calls this exec through the wrapper at 0x801a42a0 (ldr r0,[r0] = the RChunk handle, then svc 3) at 0x801b0464, and at 0x801b04a6 compares the result against the requested minimum heap length, branching to `movs r0,#0x29 ; bl 0x801ab7c4` = User::Panic(USER 41) when it is smaller. Live in the mgs_ngi.exe boot: the requested minimum is 0x40000 (the game's E32 heapMin) and the chunk was created with tchunkcreate.max_size 0x1000000 (its E32 heapMax) — so +0x28 is the chunk's maximum size. Neighbours corroborate a DChunk accessor block: 0x80082298 exec 0x1 = +0x2c, 0x800822a0 exec 0x2 = +0x24, 0x8008399c exec 0x64 = +0x34 and 0x800839a4 exec 0x65 = [+0x34]+[+0x24].

slow 0x0e0x800823480x00000000library_lookupreturns the export VA at a 1-based ordinal (0 out of range); resolves object type 3, reads the codeseg export directory. A non-ROM (RAM) codeseg HALTs.
evidence

ekern 0x80082348 (ARM), disassembled from 5320.rom = Exec::LibraryLookup(TInt aHandle, TInt aOrdinal) = RLibrary::Lookup. euser-svc-map's 'CBufBase::Read' is REFUTED (that column is the defective cross-reference); EKA2L1 (cross-reference) names SVC 0xe library_lookup, and the euser wrapper proves the shape. 0x80082378 `mov r1,#3 ; mov r0,aHandle ; bl 0x8006b410` resolves the handle as object TYPE 3 = ELibrary (0x8006b410 takes the handle in r0, the type in r1, against the current thread); 0x8008237c `ldr r0,[obj,#0x30]` = DLibrary+0x30 = its DCodeSeg (the same field exec 0x9d reads), and 0x80082384 `cmp r0,#0 ; beq` returns r4=0 when it is null; else 0x80082388 `ldr r1,[r0]` the DCodeSeg vtable, 0x8008238c `ldr r2,[r1,#0x14]` the +0x14 virtual (address-of-export-by-ordinal), 0x80082390 `mov r1,aOrdinal ; blx r2` calls it and 0x80082398 `mov r4,r0` returns its result — the export VA, bit 0 = Thumb, directly callable. euser RLibrary::Lookup 0x801a414a: `movs r4,r0` this, 0x801a414e `subs r5,r1,#0 ; bge` then `movs r0,#0x74 ; bl 0x801ab7c4` panics USER 116 for aOrdinal < 0, 0x801a4158 `ldr r0,[r4]` reads the RLibrary's iHandle at +0, 0x801a415a `movs r1,r5` the ordinal, 0x801a415c `blx 0x8019bea8` (svc 0xe), returns the VA. Live in the boot: MetalGearSlibdl's dlopen('hal.dll') chain looks up hal.dll ordinal 1 = 0x80683ae3 (hal has 5 exports at export_dir_address 0x80684494). The model reads the ordinal's VA from the ROM export directory (romimageheader.export_dir_address/count via codeseg.exportVA), which is what the ROM codeseg's vtable+0x14 does; a RAM library's relocated export table is not modeled and HALTs.

slow 0x110x800824380xe0000006mutex_waitRMutex::Wait (EKA2L1 mutex_wait, cross-reference)
evidence

ekern 0x80082438 `b 0x800865a8` = DMutex::Wait on the preprocessed mutex (flags 0xe0000006 = TObjectType 5 EMutex): 0x800865b0..0x800865bc r5 = the current DThread; 0x8008664c `ldrb r0,[r4,#0x28] ; cmp r0,#0 ; mvnne r0,#1` = -2 KErrGeneral when the mutex is resetting; 0x8008665c `ldr r0,[r4,#0x3c] ; cmp r0,#0 ; bne 0x800865c4` reads the holder (DMutex+0x3c = iCleanup.iThread, which 0x80086684 `add r1,r4,#0x2c ; bl 0x800935a4` = DThread::AddCleanup sets when the mutex is taken); free: 0x80086668 `mov r0,#1 ; str r0,[r4,#0x20]` iHoldCount = 1, 0x80086674 `bl 0x800862a4` + 0x80086678 `strb r0,[r4,#0x38]`, AddCleanup, return 0; held by the current thread: 0x800865c4..0x800865d4 `cmp r0,r5 ; ldreq r0,[r4,#0x20] ; addeq r0,r0,#1 ; streq r0,[r4,#0x20] ; beq 0x80086688` increments iHoldCount and returns 0; held by another thread: 0x800865d8.. sets iMState 5 EWaitMutex (0x800865e8), queues on DMutex+0x54 and blocks, NOT modelled (HALT); no second thread contends for a mutex in this run. The euser wrapper RMutex::Wait is void, so no value is returned (r0 left as it was).

slow 0x120x8008243c0xa0000006mutex_signalRMutex::Signal (EKA2L1 mutex_signal, cross-reference)
evidence

ekern 0x8008243c = Exec::MutexSignal on the preprocessed mutex: 0x80082440..0x80082454 `ldr r2,[r0,#0x3c] ; ... cmp r1,r2 ; movne r0,#1 ; bne 0x8008d468` panics the thread with 1 (KERN-EXEC 1) unless DMutex+0x3c (the holder) is the current DThread; 0x80082458 `beq 0x80085bf4` = DMutex::Signal: 0x80085c08..0x80085c1c when not resetting `ldr r0,[r4,#0x20] ; sub r0,r0,#1 ; str r0,[r4,#0x20] ; cmp r0,#0 ; bne 0x80085c60` decrements iHoldCount and returns while it stays non-zero; at zero 0x80085c24..0x80085c34 `ldr r0,[r4,#0x54] ; ldr r1,[r4,#0x58] ; orrs r0,r0,r1 ; blne 0x80085b78` wakes a waiter when the wait queue at +0x54/+0x58 is non-empty (NOT modelled: HALT), 0x80085c38..0x80085c3c `mov r0,#0 ; str r0,[r4,#0x3c]` clears the holder and 0x80085c40..0x80085c48 unlinks the cleanup from the thread's list (0x80068804). Void in euser (RMutex::Signal); r0 is left as it was.

slow 0x160x800825100xa0000002process_filenameCORRECTS a 16-bit answer that cost the boot 60,490 instructions. Writing the name with writeDes16 made euser widen our UTF-16 bytes a second time, so EFSrv TParseBase::DoParse 0x801f0cfa saw "e\0:\0..." — no drive at 0x801f0e68 `cmp r0,#0x3a`, no path at 0x801f0ee4 `cmp r0,#0x5c`, then KErrBadName -28 from the name component 0x801f0f48 and FSClientPanic(24) out of the TParsePtrC ctor 0x801f1162. Written 8-bit, RFs::SetSessionToPrivate(EDriveE) follows, which is what the CRT wanted the name for.
evidence

ekern 0x80082510 = Exec::ProcessFileName(DProcess* aProcess, TDes8* aName) (flags 0xa0000002 = preprocessed PROCESS handle; KCurrentProcessHandle 0xffff8000 -> the current DProcess at 0x8006a75c): 0x80082520..0x80082528 `mov r1,#0x100 ; add r0,sp,#4 ; bl 0x8009976c` initialises a TBuf8 scratch — 0x8009976c is `mov r2,#0x30000000 ; str r1,[r0,#4] ; str r2,[r0]`, i.e. an EBuf head with iMaxLength 0x100 and no character-width doubling; 0x8008252c `ldr r4,[r4,#0x108]` = DProcess::iCodeSeg (the field exec 0x9f walks); when non-null 0x8008253c `bl 0x80088a20` = DCodeSeg::AppendFullFileName, which reads iFileName at [codeseg+0x2c] and appends 8-bit (its null leg 0x80088a38 `mov r1,#0x40 ; bl 0x8006d0c8` appends the single character '@' through TDes8::Append(TChar)); 0x8008254c `bl 0x80072b70` is `bx lr`; 0x80082558 `bl 0x80098088`. Kern::KUDesPut 0x80098088 is the ONE way an exec hands a name back and it is EIGHT-BIT: 0x80098098 `bic r4,r1,#0xf0000000` takes the SOURCE descriptor length, 0x800980c0 `bl 0x8006b9a4` = K::USetLength stores that same number in the caller's descriptor, and 0x800980d0 `bl 0x8006bec4` copies that many BYTES (tail 0x8006beec `ldrbpl r3,[r1],#1 ; strbtpl r3,[r0],#1`). THE CALLER PROVES THE WIDTH TWICE OVER: euser RProcess::FileName 0x801a43ba constructs a TFileName at sp+0xc (0x801a43c6), takes its character pointer (0x801a43cc `blx 0x8019ddf4`), makes a TPtr8 over ptr+0x100 with aMaxLength 0x100 (0x801a43d0 `adds r1,r0,#7 ; adds r1,#0xf9`, 0x801a43d4 `movs r2,#0xff ; adds r2,r2,#1`, 0x801a43da `bl 0x801ade9e`), passes THAT to svc 0x16 (0x801a43e2, stub 0x8019bee8 `svc #0x16 ; bx lr`), and then widens it into the TFileName with TDes16::Copy(const TDesC8&) 0x801aeda8 whose loop is 0x801aedce `ldrb r1,[r4] ; adds r4,r4,#1 ; strh r1,[r0] ; adds r0,r0,#2`, before returning the 0x208-byte TFileName by value (0x801a43ee `movs r2,#0x41 ; lsls r2,r2,#3`). The file name is the VFS path the image was installed to and loaded from (KState.attachProcess), e:\sys\bin\mgs_ngi.exe for the game; no return value is produced (r0 is left as it was).

slow 0x170x800825640xa0000002process_command_linenothing in this KState sets DProcess::iCommandLine, so this answers the evidenced null leg: an empty 8-bit descriptor, exactly what 0x800825b4 `ldreq r1,[pc,#0x1ac]` puts. A non-empty command line HALTs — its byte encoding has no evidence here. CORRECTED with 0x16: the old writeDes16 answer would have written character counts into a byte-counted descriptor.
evidence

ekern 0x80082564 = Exec::ProcessCommandLine(DProcess*, TDes8*): 0x80082574..0x8008258c the same owning-process check as 0x51; 0x80082594 `bl 0x8008b5cc` CheckedOpen and 0x800825a0 `str r4,[r0,#-0x1f0]` parks the process in the current DThread's iTempObj; 0x800825a8..0x800825b8 `ldr r1,[r4,#0x64] ; cmp r1,#0 ; ldreq r1,[pc,#0x1ac] ; bl 0x80098088` = KUDesPut of DProcess::iCommandLine, or of the KNullDesC literal when it is null; 0x800825c4..0x800825d8 clears iTempObj and Close()s the process. Kern::KUDesPut 0x80098088 is the ONE way an exec hands a name back and it is EIGHT-BIT: 0x80098098 `bic r4,r1,#0xf0000000` takes the SOURCE descriptor length, 0x800980c0 `bl 0x8006b9a4` = K::USetLength stores that same number in the caller's descriptor, and 0x800980d0 `bl 0x8006bec4` copies that many BYTES (tail 0x8006beec `ldrbpl r3,[r1],#1 ; strbtpl r3,[r0],#1`). iCommandLine is an HBufC8 whose BYTES ARE the 16-bit text, which is why euser RProcess::CommandLine 0x801a491e aliases a TPtr8 onto the caller's own 16-bit buffer with aMaxLength = 2*iMaxLength (0x801a4922 `ldr r0,[r0,#4] ; lsls r5,r0,#1`, 0x801a4932 `bl 0x801ade9e`) and afterwards HALVES the returned length to get characters (0x801a493e `ldr r0,[sp] ; lsls r0,r0,#4 ; lsrs r1,r0,#5 ; blx 0x8019de7c` = TDes16::SetLength) — the same halving Exec::ProcessCommandLineLength does kernel-side (0x800832cc `ldrne r0,[r0] ; lslne r0,r0,#4 ; lsrne r0,r0,#5`), which is why row 0x51 counts characters and this one counts bytes.

slow 0x260x800828a40xe0000001thread_idRThread::Id (EKA2L1 thread_id, cross-reference)
evidence

CORRECTS a fabricated row ('rchunk_bottom' came from the RChunk 'family' guess, not from this handler). ekern 0x800828a4 (slow table entry 0x26, flags 0xe0000001 = preprocessed THREAD handle): `ldr r0,[r0,#0x44] ; bx lr` = DThread+0x44 = iId (struct_field dthread.id: 0x80092b4c/0x80092b50 `bl 0x80091198 ; str r0,[r4,#0x44]` at creation). KCurrentThreadHandle 0xffff8001 maps to the current DThread in the preprocessor 0x8006a748 `cmp ip,#2 ; subeq r0,sb,#0x2c0`; a real thread handle resolves through the handle table. The numeric ids here are this model's (1 for the initial thread, then increasing); 0x80091198's global counter is not reproduced, and the value is only ever used as an opaque key by the callers seen (libpthread).

slow 0x270x8008a6640x00000000session_shareRSessionBase::ShareAuto/ShareProtected (EKA2L1 session_share, cross-reference); the CRT calls it right after RFs::Connect
evidence

ekern 0x8008a664 = Exec::SessionShare(TInt* aHandle, TInt aMode): 0x8008a668..0x8008a684 `cmp r1,#2 ; bhi -> mvn r0,#5` and `cmp r5,#0 ; bne` = -6 KErrArgument unless aMode is 1 or 2; 0x8008a69c `bl 0x8006baac` kumemget's the 4-byte handle from *aHandle; 0x8008a6a8..0x8008a6ac `mov r1,#8 ; bl 0x8006b410` looks it up as TObjectType 8 ESession (r6); 0x8008a6b4 `bl 0x8008b5cc` = DObject::CheckedOpen; 0x8008a6b8 `ldrb r1,[r6,#0x33]` = the server's TIpcSessionType copied at attach (0x8008a37c): 0x8008a6c8 `moveq r4,#0x17 ; beq 0x8008a744` panics the thread with 0x17 via 0x8008d484 when it is 0 (unsharable server), 0x8008a6d0 `cmp r1,r5 ; mvnlo r4,#0x2d` = -46 KErrPermissionDenied when it is below aMode; 0x8008a6dc `cmp r5,#2 ; moveq r1,#1 ; strbeq r1,[r6,#0x11]` sets the auto-attach byte for mode 2; 0x8008a6e8 `ldrb r0,[r6,#0x32] ; cmp r0,r5 ; strblo r5,[r6,#0x32]` raises the session's own share type to aMode and `cmplo r0,#1 ; bhs 0x8008a744` finishes with 0 when it was already >= aMode or already shared (>= 1); otherwise 0x8008a6fc..0x8008a704 `mov r1,r6 ; mov r0,#0 ; bl 0x80090de0` makes a NEW process-owned handle onto the same DSession, 0x8008a714..0x8008a71c `ldr r0,[sp,#4] ; bl 0x80090e30` closes the old handle (0x80090e30 is the same K::HandleClose that exec 0x69's handler calls at 0x80083ba8), 0x8008a720..0x8008a72c `mov r2,#4 ; bl 0x8006aff0` kumemput's the new handle to *aHandle (a fault there is panic 3), and 0x8008a76c returns r4 = 0. The server's type comes from db/os/hle-server.tsv (!FileServer = 2, RE'd from efile.exe's CServer2 construction).

slow 0x280x800828ac0xe0000001thread_resumedispatched (readies a suspended thread; the switch happens when the current thread blocks)
evidence

ekern 0x800828ac: r4 = the DThread (handle preprocessed); 0x800828b8..0x800828d4 compares [thread+0x30 (owning process)+0xb8] with the caller's and calls 0x800820d8 when they differ (cross-process, unmodelled -> HALT), then `b 0x80093828` = DThread::Resume: `add r0,r0,#0x2c0 ; bl 0x8006e678` = NKern::ThreadResume on the embedded NThread, `cmp r0,#0 ; popeq` when nothing was resumed, else `mov r0,#0` and a jump table on iMState (+0x149) whose EReady arm (2) is a plain return — so the exec returns 0 in every modelled case. NKern::ThreadResume 0x8006e5d4: no-op for nstate 5 (dead) or a non-negative iSuspendCount; otherwise ++iSuspendCount and, at 0, readies the thread through the ESuspended arm 0x8006e668 -> 0x80068828 (CheckSuspendThenReady, which appends to the tail of its priority queue). No context switch happens here — the current thread keeps running until it blocks.

slow 0x3b0x80082ba80x00000000request_signalimplemented (adds aCount to the current thread request semaphore; no return value is consumed)
evidence

ekern 0x80082ba8 `mov r1,r0 ; mov r0,#0 ; b 0x8006ea50` = NKern::ThreadRequestSignal(NULL, aCount): 0x8006ea54..0x8006ea60 `movs r4,r0 ; ldreq r4,[TheScheduler+0x130]` resolves the null thread to the current one, 0x8006ea6c `add r0,r4,#0x20 ; bl 0x80068750` = NFastSemaphore::SignalN on its request semaphore (count += aCount; the owner is readied only if the old count was negative, `bxlo lr` otherwise), under the scheduler lock 0x80068f84/0x80068f4c. euser's User::WaitForRequest 0x801a45aa..0x801a45b0 hands back r4 surplus wake-ups through it (`cmp r4,#0 ; beq ; movs r0,r4 ; blx 0x8019c000`), and 0x8019c000 is `svc #0x3b`.

slow 0x4c0x8006ac800xb0000009session_send_syncdispatched (queues the message on the server and returns KErrNone; -6 for aFunction -2, -15 for a terminated server). HALTs instead of guessing on: a second outstanding sync message, a repeat/late connect (both are kernel panics), and a server with a blocked receiver. *aStatus is deliberately NOT completed — no server thread exists to complete it, so the boot now stops in User::WaitForRequest at fast exec 0.
evidence

ekern 0x8006ac80 = Exec::SessionSendSync(TInt aHandle, TInt aFunction, const TIpcArgs* aArgs, TRequestStatus* aStatus), disassembled from the standalone ekern.exe. The handler is entered with a DSession* (docs/data/ekern-exec-table.tsv gives slow 0x4c flags 0xb0000009, the handle-preprocessing form): its first instruction 0x8006ac80 `ldr r4,[r0,#0x20]` reads the field DSession attach writes at 0x8008a3ac (`str r5,[r4,#0x20]`, r5 = the DServer whose +8 iAccessCount attach had just tested at 0x8008a33c the same way), so r0 is a DSession and +0x20 is iServer. VALIDATION, in the handler's own order: 0x8006ac84 `cmn r1,#2 ; beq 0x8006acc8` returns `mvn r0,#5` = -6 KErrArgument for aFunction == -2; 0x8006ac8c-0x8006aca0 `cmp r4,#0 ; ldrne r6,[r4,#8] ; cmpne r6,#0 ; beq 0x8006acd0` returns `mvn r0,#0xe` = -15 KErrServerTerminated for a null server or one with iAccessCount 0. MESSAGE: 0x8006ac94 `sub ip,sb,#0x1d4` against the same sb that 0x8006ad54 `sub sb,sb,#0x2c0` resolves to the current DThread makes ip = DThread+0xEC, the RMessageK embedded in the thread for synchronous sends; 0x8006ac98 `ldr r7,[ip] ; cmp r7,#0` tests its first word — which is the queue link the delivery routine writes at 0x8006ae60 `stm r1,{r2,r3}` — and 0x8006acb8 `bne 0x80089e50` = `mov r0,#0x1e ; b 0x8008d468` PANICS the thread with 30 when it is still outstanding (0x8008d468 is the panic helper: `mov r2,r0` code, `sub r0,r0,#0x2c0` current thread, `mov r1,#2` category, `b 0x80092638`). On the free path 0x8006aca8-0x8006acb4 store aSession/aFunction/aStatus into msg +0x20/+8/+0x30 — aStatus is only RECORDED, never dereferenced. 0x8006acbc `cmn r1,#1 ; bne 0x8006ad50 ; b 0x8006adc8` splits KConnectMessage (-1) to 0x8006adc8, which requires DSession+0x4c and +0x2c to both be null (`teq`/`bne 0x80089de0` = `mov r0,#9` and `bne 0x80089de8` = `mov r0,#0x39`, both kernel panics via 0x8008d468, NOT error returns), stores the message in +0x4c at 0x8006ade0 and rejoins the common tail. TAIL 0x8006ad50-0x8006adc4: iMsgCount (+0x34) +1, a reference taken on the client DThread (`ldr r1,[sb,#0x20] ; add r1,r1,#1 ; str r1,[sb,#0x20]`), the client stamped into msg+0x2c, the message appended to the session's SDblQue at DSession+0x3c/+0x40, then 0x8006ad84 `cmp r2,#0 ; ldmne r2,{r1,r3,r6,r7,r8}` reads the caller's TIpcArgs as five words (a null aArgs writes five zeroes) and 0x8006ada4 `stm r2!,{r1,r3,r6,r7}` + `strh r8,[r2]` copies four argument slots to msg+0xc..+0x18 and a HALFWORD of the type-flags word to msg+0x1c — euser's wrapper corroborates the same five words at 0x8019c590 `movs r8,r2 ; ldmne r2,{r4,r5,r6,r7,r8}` off the identical pointer; finally 0x8006adb8 `bl 0x8006ae44` delivers and 0x8006adc0 `mov r0,#0 ; bx lr` returns KErrNone. DELIVERY 0x8006ae44 is four instructions of queue work when DServer+0x28 (a server thread blocked in Receive) is null: `ldr r3,[r0,#0x38] ; add r2,r0,#0x34 ; str r1,[r2,#4] ; str r1,[r3] ; stm r1,{r2,r3}` appends to the server's message SDblQue at DServer+0x34/+0x38; the non-null case branches to 0x8006dfac, which MMU-switches to the receiver (mcr p15 c2/c13) and strt's the message out — NOT RE'd, so it HALTs. THE RETURN IS NOT THE SERVER'S ANSWER: euser's send wrapper 0x801a6c58 stores KRequestPending (literal 0x801a6f0c = 0x80000001) and iFlags=2 into a stack TRequestStatus, passes &it in r3, and on `cmp r0,#0 ; bne` returns the exec's error — but on ZERO it calls User::WaitForRequest 0x801a4594, whose loop `blx 0x8019bd58` (= `svc #0x800000`, fast exec 0) repeats while [status] == KRequestPending, and only then `ldr r0,[sp]` returns the STATUS as RSessionBase::CreateSession's result. So *aStatus must be left pending here; the server completes it.

slow 0x4d0x800831280x00000000dll_tlsimplemented (per-thread TLS lookup; 0 when unset or the uid differs)
evidence

CORRECTS a fabricated row (the old 'RomRootDirectoryAddress' came from the defective euser-svc-map plus EKA2L1's rom.h; the value it returned was never checked against ekern). Handler 0x80083128: `mov r3,r0 ; ldr r0,[TheScheduler+0x130] ; mov r2,r1 ; ldr r0,[r0,#0x130] ; mov r1,r3 ; sub r0,r0,#0x2c0 ; b 0x8009320c` = DThread::Tls(handle, uid) on the current thread: 0x80093224 finds the STls entry by iHandle (signed key order, 0x8006cc10), returns 0 when absent (0x8009325c), and 0x8009323c..0x80093254 returns iPtr only when the entry's iDllUid equals aDllUid, else 0. So the exec is Exec::DllTls(TInt aHandle, TInt aDllUid).

slow 0x4e0x800831440x00000000hal_getdispatched (fn7 only)
evidence

ekern @0x80083144; EHalGroupKernel.fn7 PageSizeInBytes=4096

slow 0x510x8008329c0xe0000002process_command_line_lengthRProcess::CommandLineLength (EKA2L1 process_command_line_length, cross-reference)
evidence

CORRECTS a fabricated row (the old 'allocator_get' evidence was a remembered API name, not a handler read). ekern 0x8008329c (docs/data/ekern-exec-table.tsv slow 0x51, flags 0xe0000002 = preprocessed PROCESS handle): 0x800832a8..0x800832c0 compares the target's owning-process id [r4,#0xb8] with the current thread's and calls 0x800820d8 when they differ (cross-process, unmodelled -> the impl accepts KCurrentProcessHandle 0xffff8000 only; the SVC preprocessor 0x8006a75c `cmp ip,#0 ; ldreq r0,[sb,#-0x290]` maps that pseudo-handle to the current DProcess); 0x800832c4 `ldr r0,[r4,#0x64] ; cmp r0,#0` = DProcess+0x64 iCommandLine (an HBufC16*), and when non-null 0x800832cc..0x800832d4 `ldr r0,[r0] ; lsl r0,r0,#4 ; lsr r0,r0,#5` = its length word masked to 28 bits and halved (bytes -> 16-bit characters); a null command line returns 0. This KState's process was started with no command line (KState.process.commandLine ''), the value the exec reports for a process whose DProcess+0x64 is null.

slow 0x640x8008399c0xe0000003rchunk_top-
evidence

ekern @0x80083d48 family; RChunk::Top()=base+committed

slow 0x670x800839fc0x00000000thread_createdispatched (handle of a suspended thread whose first user context follows EpocThreadFunction; -6 on a bad size)
evidence

ekern 0x800839fc = Exec::ThreadCreate(const TDesC& aName, TOwnerType aType, SThreadCreateInfo& aInfo): 0x80083a1c copies the 0x30-byte head from r2, validates iTotalSize ((total-0x30) < 0xd1, total&7 == 0 -> else -6 at 0x80083a44) and iUserStackSize >= 0x200 (0x80083a48..0x80083a54, else -6), copies the remaining total-0x30 bytes (0x80083a60..0x80083a78), then overwrites iType with 3 = EThreadUser (0x80083a7c), iInitialThreadPriority with -5 = EThrdPriorityNormal (0x80083a84), iSupervisorStack/Size with 0 (0x80083a98/0x80083a9c), takes the name via KUDesGet 0x80098040 into a KMaxKernelName scratch and calls DProcess::NewThread 0x80091570 on the current thread's process with the owner type in r1; `movs r4,r0 ; ldreq r4,[sp,#8]` returns the NEW HANDLE on success. Down that call: DThread::Create 0x80092ae8 stores the priority at +0x3c and the type at +0x165, AllocateUserStack 0x8007a920 page-rounds the size (0x80074a00) and places the stack above a guard, DoCreate 0x800929ac builds the NThreadCreateInfo (iFunction = EpocThreadFunction 0x8006a868, iParameterBlock = the kernel's edited copy, iParameterBlockSize = iTotalSize) and NKern::ThreadCreate 0x8006f320 leaves the thread ESuspended with iSuspendCount -1 (0x8006f3b4..0x8006f3c0). Its first user context is fixed by EpocThreadFunction 0x8006a89c..0x8006a920: stack filled with 0x29, the info copy at the stack top with user SP = top - iTotalSize, SPSR user/ARM, r4 = 1 and PC = DProcess::iReentryPoint (+0x118, = the codeseg entry point stored by 0x80091e00) because the thread's flags bit 0x10 is clear. See struct sthreadcreateinfo and web/src/kernel/threads.js.

slow 0x690x80083b980x00000000handle_closedispatched (closes chunk handle)
evidence

ekern 0x80083b98: h&0x8000->KErrGeneral; else resolve+close returns TObjectType (cmp #2=EChunk); eka2l1 svc.cpp handle_close

slow 0x6a0x80083be40x00000000rchunk_createdispatched (chunk handle; max size read from the caller's TChunkCreate)
evidence

ekern 0x80083be4 copies a 0x14-byte TChunkCreate from r2 (0x80083c28 `bl 0x8006baac` with r2=0x14) and routes its five words into the kernel create-info; see struct `tchunkcreate` for the field evidence. We therefore take the requested maximum size from tchunkcreate.max_size (+0x10) instead of inventing one — it is what exec 0x3 must report back (euser panics USER 41 if it is smaller than the requested heap minimum).

slow 0x6b0x80083d480xa0000003rchunk_opdispatched (returns a TInt error code; selector 0 = Adjust)
evidence

ekern 0x80083d48 disassembled from 5320.rom: 0x80083da8 `cmp r7,#6 ; addls pc,pc,r7,lsl #2` dispatches selectors 0..6 to seven DChunk virtuals ([iVtbl+0x1c], +0x20, +0x28, +0x30, +0x2c, +0x38, +0x34); every branch funnels through 0x80083e34 `mov r5,r0` and the epilogue returns r5, and the failure legs load r5 with 0x80083e68 `mvn #0x14` = -21, 0x80083e84 `mvn #0x2d` = -46 and 0x80083eb8 `mvn #5` = -6 for an out-of-range selector — so this exec returns a TInt error code for EVERY selector, never an address. Selector 0 is RChunk::Adjust: euser's wrapper 0x801a68d0 panics USER 102 (`movs r0,#0x66 ; bl 0x801ab7c4`) when the new size is negative, then issues svc 0x6b with r1=0 and r2=the new size; its caller inside UserHeap::ChunkHeap does 0x801b04cc `movs r5,r0 ; beq 0x801b04d6` and proceeds ONLY on zero, returning a null heap otherwise. CORRECTS the previous row, which named the family from the off-by-two euser-svc-map api column ("RChunk::Base") and returned the chunk base for selector 0.

slow 0x6d0x80083f840x00000000handle_duplicatedispatched (new handle onto the same object; -8 on a bad source handle)
evidence

ekern 0x80083f84 disassembled from 5320.rom: r4=r0 = the source thread/process handle, r8=r1 = the owner type, r5=r2 = the handle to duplicate. 0x80083fac resolves r0 to the owning thread (r6); 0x80083fb4 `mvn r4,#7` preloads -8 = KErrBadHandle; 0x80083fc4 looks r5 up in that thread's handle table and `movs r5,r0 ; beq 0x80083fe4` keeps the -8 when absent; on success 0x80084030 calls the make-a-handle helper 0x80090b5c with (current thread r7, owner type r8, object r5, out-param sp) and `movs r4,r0 ; ldreq r4,[sp]` takes the NEW handle out of that out-param, so the return is a fresh handle onto the same kernel object or a negative error. euser's wrapper 0x801a6222 loads the source handle from [r4], passes it as r2 and then zeroes [r4] — an ownership transfer; UserHeap uses it to hand the lock semaphore created by exec 0x6f to the heap it is building. Only a pseudo source handle (bit 0x8000, the same bit the exec 0x69 handler tests) is modelled; a real cross-process handle HALTs.

slow 0x6e0x800840b40x00000000mutex_createimplemented (anonymous mutex handle; a named mutex HALTs)
evidence

ekern 0x800840b4 = Exec::MutexCreate(const TDesC* aName, TOwnerType aType): 0x800840c4 sizes a TKName scratch (0x50); a non-null name is copied in with KUDesGet 0x80098040 (0x800840e8), a null one selects the owner at 0x800840f4..0x80084104 (`cmp r7,#1 ; ldrne r5,[cur,#-0x290]` = the process, `subeq r5,r0,#0x2c0` = the thread); 0x8008410c `mov r3,#0xfe` = KMutexOrdUser and 0x80084124 `bl 0x80082024` = K::MutexCreate(&out, name, owner, 1, 0xfe), which allocates 0x160 bytes (0x8008202c), constructs (0x800860e8), calls DMutex::Create 0x80086088 and stores the object through r6 on success (0x80082078), -4 KErrNoMemory otherwise (0x80082054 `mvn r5,#3`); 0x80084138..0x8008414c sets the name-type byte (+0x11) 1 anonymous / 2 named; 0x80084158 `bl 0x80090de0` makes the handle (the helper exec 0x7e's evidence traces to `bl 0x80090b5c ; ldreq r0,[sp]`) and 0x8008415c..0x80084170 closes the object when that fails. Caller in efile.exe's startup: euser 0x801a6612 `movs r4,r0 ; movs r0,#0 ; blx 0x8019c190` (= `svc #0x6e`) = RMutex::CreateLocal(aType) with a null name, reached at 7,235 instructions. NOT euser-svc-map's 'RMutex::OpenGlobal'.

slow 0x6f0x800841840x00000000sema_createdispatched
evidence

ekern 0x80084184 + eka2l1 semaphore_create; euser 0x801a6284 panics if count<0 = RSemaphore::CreateLocal(count); alloc handle (single-thread: never blocks)

slow 0x720x8008d5a00x00000000thread_killHALTs on a current-thread EExitPanic/kill naming the guest category+reason (User::Panic ord 650 reached the kernel = an upstream answer of ours is wrong); a cross-thread handle HALTs unmodelled. The honest boot frontier is the first such panic — see db/frontier.json current
evidence

ekern 0x8008d5a0 = Exec::ThreadKill(TInt aHandle, TExitType aType, TInt aReason, const TDesC8* aCategory): `cmp aType,#2` (EExitPanic) copies the category into a TKName (0x8008d5cc bl 0x8008cb18), resolves the handle to a DThread (0x8008d5ec bl 0x8006b410) and runs the thread-death path 0x80092638 (which reschedules, 0x800926a0 b 0x80068b3c). euser reaches it through wrapper 0x8019c71c (`mvn r0,#0xfe ; bic r0,r0,#0x7f00` = KCurrentThreadHandle 0xffff8001; r1=type, r2=reason, r3=&category) from User::Panic (ord 650, 0x801a599a). EKA2L1 names SVC 0x72 thread_kill. The old exec72_setname ("sel 2 -> 0") was the misnomer that de-fabrication removed: it returned KErrNone for (KCurrentThreadHandle, type 2), swallowing the three main-thread panics this direct boot issues and inflating the frontier 57k instrs to op-22. api hint in the gen is the defective euser-svc-map (RRegion, UNVERIFIED) like every row; the impl name is the verified identity.

slow 0x750x800843900x00000000dll_set_tlsimplemented (per-thread TLS keyed by handle; returns KErrNone)
evidence

ekern 0x80084390 = Exec::DllSetTls(TInt aHandle, TInt aDllUid, TAny* aPtr): enters a critical section (0x8006ec84), 0x800843a4..0x800843bc loads TheScheduler.iCurrentThread - 0x2c0 = the current DThread and calls DThread::SetTls 0x800931b4(thread, handle, uid, ptr), which searches the RArray<STls> at DThread+0x58 by iHandle in signed key order (0x8006ca78) and either overwrites the found entry's three words {handle, uid, ptr} (0x800931e4..0x800931f0 `ldm r1,{r2,r3,r4} ; stm r0,{r2,r3,r4} ; mov r0,#0`) or inserts a new one (0x80098d48), returning that call's result (0, or KErrNoMemory from the array growth, which this model cannot run out of). EKA2L1's trace names this exec dll_set_tls at the _E32Startup landmark. First reached by efile.exe's startup at 1,207 instructions.

slow 0x760x800843d00x00000000dll_free_tlsimplemented (drops the per-thread TLS entry)
evidence

Handler 0x800843d0 = Exec::DllFreeTls(TInt aHandle): critical section, then DThread::FreeTls 0x80093264(current DThread, handle): find by iHandle (0x8006cc10); when found, RArray::Remove 0x80098dcc + Compress 0x80098e18; no return value is produced (the exec's r0 is whatever LeaveCS leaves, and no euser caller reads it). The row this replaces (slow 0x14 'DllFreeTls') pointed at handler 0x80082464, which is not this routine.

slow 0x7e0x8008a9280x00000000session_createdispatched (session handle; -1 KErrNotFound for an unknown server, -6 for a bad slot count/mode). Every case the firmware does not fix HALTs: a non-null aPolicy, aMsgSlots>0, aMode>0, a wildcard name, and a KState with no ROM behind its memory.
evidence

ekern 0x8008a928 = Exec::SessionCreate(const TDesC8& aServer, TInt aMsgSlots, const TSecurityPolicy* aPolicy, TInt aMode), disassembled from the standalone ekern.exe (which agrees with 5320.rom). It is NOT euser-svc-map's 'PlatSec::IsCapabilityEnforced' — that column is UNVERIFIED and off-by-two, and the only PlatSec contact here is the epilogue 0x8008aa68 `cmn r7,#0x15`, which raises a diagnostic when the RESULT is -21. Flow: 0x8008a938/0x8008a950 `mov r1,#0x50` size two TKName scratch buffers (KMaxKernelName = 0x50) via 0x8009976c; 0x8008a960 `ldr r4,[K::Containers,#0x1c]` = container index 7 = EServer; 0x8008a968 Kern::KUDesGet 0x80098040 copies the caller's TDesC8 name in (its head decoder 0x8006b934 is what struct `tdesc` records); 0x8008a978 skips a null aPolicy, else kumemgets exactly 8 bytes (0x8008a980 `mov r2,#8`) and validates them at 0x8009a66c (`ldrb r1,[r0] ; cmp r1,#5 ; addls pc,pc,r1,lsl #2`, a 6-way TSecurityPolicy::iType table returning 1/0), 0x8008a99c `mvneq r0,#5` = -6 KErrArgument when invalid; 0x8008a9c0 DSession::New 0x8008a27c bounds aMsgSlots with `add r0,r1,#1 ; subs ip,r0,#0x200 ; subshs ip,ip,#2 ; bhs` (= the range [-1,0x200]) and aMode with `cmp r5,#2 ; bls`, -6 otherwise, then allocates a 0x60-byte DSession and stores aMode at +0x32, with aMsgSlots<0 taking the global pool (`ble` to the success return), ==0 the 0x8008a2d4 leg and >0 a dedicated pool at 0x8008a2fc; 0x8008a9f4 DObjectCon::FindByName 0x8008bd64 scans the EServer container, skipping entries with iAccessCount<=0 (0x8008be10) or a null iName (0x8008be1c) and calling TDesC::Match 0x800992c0 (= `mov r2,#1 ; b 0x80099048`, the unfolded variant) on each survivor, returning `mvn r0,#0` = -1 KErrNotFound at 0x8008bebc when the loop runs off the end; on a hit 0x8008a330 attaches, returning -15 KErrServerTerminated when the server's iAccessCount is 0, -46/-21 when aMode exceeds the server's TIpcSessionType at DServer+0x3c; finally 0x8008aa38 -> 0x8008a424 adds the session to K::Containers[8] (`mov r1,#8` = ESession, bl 0x8008c22c) and tail-calls 0x80090de0, whose `bl 0x80090b5c ; cmp r0,#0 ; ldreq r0,[sp]` returns the NEW HANDLE, so a success return is a positive session handle. The EServer container itself is derived from the ROM rather than asserted: TRomHeader.iSecondaryFile (+0xA4) is a TRomEntry naming 'efile.exe' with iAddressLin 0x801CB600 (its TRomImageHeader), i.e. the first user-side process the kernel starts; efile.exe holds the TLitC16 '!FileServer' at 0x801E7EBC and feeds it to TFindServer at 0x801D2D74/0x801D2D80; the identical literal at 0x801F2FBC belongs to \Sys\Bin\EFSrv.dll, whose RFs::Connect is the caller issuing this exec; and the kernel counterpart closes the loop — a sweep of all 12 call sites of ekern's container-add helper 0x8008C22C finds r1=7 (EServer) at exactly one, 0x8008A8E0, inside the handler of slow exec 0x7d (Exec::ServerCreate), which also does `ldrb r0,[r0] ; cmp r0,#0x21` at 0x8008a850 for the '!'-prefixed protected-server convention and writes aMode to DServer+0x3c at 0x8008a8a8. See web/src/kernel/servers.js.

slow 0x9b0x800897040x00000000dll_lock_waitacquire the process DLL lock. Recursive on the same thread (DMutex::Wait increments iHoldCount); the blocking leg for a lock held by ANOTHER thread is not modelled and HALTs rather than pretending to acquire. euser-svc-map calls this 'E32Loader::ProcessCreate' and that name is WRONG: the handler takes no arguments, creates nothing, and is one half of a lock pair.
evidence

ekern 0x80089704 (ARM), no register arguments — the stub 0x8019C2F8 is bare `svc #0x9b ; bx lr` and r0..r3 at the svc are leftovers of the euser caller. The handler: `ldr r0,[pc,#-0x988]` -> literal [0x80088D84] = 0xC800043C = &TheScheduler (the same constant struct.tsv's tscheduler row is built on, via the SVC vector's literal 0x800683A0); `ldr r0,[r0,#0x130]` = TScheduler::iCurrentThread (NThread*); `sub r0,r0,#0x2c0` = the embedding DThread (struct_field dthread.nthread 0x2c0); `ldr r4,[r0,#0x30]!` sets r4 = DThread::iOwningProcess (dthread.owning_process 0x30) AND, because of the write-back, leaves r0 = DThread+0x30, so the following `add r0,r0,#0x108` addresses DThread+0x138 — NOT DProcess+0x108. 0x8009D2D8 is SDblQue::IsEmpty (`ldr r1,[r0] ; cmp r1,r0 ; movne r0,#0 ; moveq r0,#1`, a head whose next points at itself), and `cmp r0,#0 ; moveq r0,#0x25 ; bleq 0x8008D484` panics KERN-EXEC 37 when that queue is NOT empty. It then tail-calls 0x80087C18: `mov r4,r0 ; bl 0x80068BA8 (LockSystem) ; ldr r0,[r4,#0x114] ; bl 0x800865A8 ; mov r4,r0 ; bl 0x80068B3C (UnlockSystem) ; mov r0,r4`. 0x800865A8 IS DMutex::Wait — this table's own slow 0x11 row already records ekern 0x80082438 `b 0x800865a8` as exactly that — so no handle is made and the exec returns Wait's TInt (0 on acquisition, -2 KErrGeneral while resetting). DProcess+0x114 is the process's DLL lock, created by DProcess::Create 0x800917C4 through K::MutexCreate 0x80082024 with order 0xFE and the name TLitC at 0x8009E280 = 'DLL$LOCK' (its sibling at +0x110, order 0x48, is named '$LOCK' at 0x8009E274).

slow 0x9c0x800897380x00000000dll_lock_signalrelease the process DLL lock; the mirror of slow 0x9b.
evidence

ekern 0x80089738 (ARM), the identical shape to slow 0x9b's handler with panic reason 0x2b = KERN-EXEC 43 instead of 0x25, tail-calling 0x80087C3C: `push {r4,lr} ; mov r4,r0 ; bl 0x80068BA8 (LockSystem) ; ldr r0,[r4,#0x114] ; pop {r4,lr} ; b 0x80085BF4`. 0x80085BF4 is DMutex::Signal — this table's own slow 0x12 row records it as such (`beq 0x80085bf4` after the holder check). euser proves the two execs are one acquire/release pair on the same lock: the caller at 0x801A78FE issues `blx 0x8019C2F8` (svc 0x9b) at 0x801A793E, tests the result with `movs r6,r0 ; bne`, and on the failure path issues `blx 0x8019C300` (svc 0x9c) at 0x801A7964.

slow 0x9d0x800897740x00000000library_attachpublishes the fresh codeseg entry points deps-first, count to *aTotal, state->2, keeping the DLL lock; state 3 -> count 0 and release; count>capacity HALTs (kernel panic 39)
evidence

ekern 0x80089774 (ARM), disassembled from 5320.rom; the per-library analogue of exec 0x9f (StaticCallList). TAKES THREE ARGS (handle, TInt* aTotal, TLinAddr* aList): 0x80089778 `mov r5,r0` handle, 0x80089780 `mov r8,r2` aList, 0x800897a4 `bl 0x8006baac` kumemget's *aTotal (capacity) into [sp+0xc]. PRECONDITIONS 0x800897a8..0x800897d4: 0x800897ac `ldr r0,[DProcess+0x114] ; ldr r0,[r0,#0x3c] ; cmp r0,DThread ; bne` requires the DLL lock held by the current thread, 0x800897bc SDblQue::IsEmpty on DThread+0x138, and 0x800897c8 `cmpne r0(capacity),#0 ; bgt` requires capacity>0, else 0x800897d0 `mov r0,#0x26 ; bl 0x8008d484` = KERN-EXEC 38 (0x8008d484 tail-calls the panic helper 0x8008d468). HANDLE LOOKUP 0x800897f0 `mov r2,#3 ; mov r1,handle ; mov r0,DThread ; bl 0x8006b2ac` resolves object TYPE 3 (ELibrary) in the CURRENT THREAD's handle table (0x8006b2ac `adds ip,r1,r1` takes the handle in r1); a miss panics KERN-EXEC 0 at 0x80089810. 0x80089814 `ldrb r0,[DLibrary+0x24]` the state byte, 0x80089818 `ldr r2,[DLibrary+0x30]` the DCodeSeg; 0x8008981c `cmp #3 ; beq 0x80089890` SKIPS to the tail (count stays 0) when state==3. Else 0x8008983c `bl 0x80087bc4` builds the codeseg dependency closure of the DLibrary's DCodeSeg (0x80087bec TraverseDeps 0x800874d8, deps-first/self-last) minus what the process already holds (0x80087c0c 0x80087560 subtraction), returns the count; 0x8008984c `movgt r5,#0x27 ; bgt` = kernel panic 39 when count>capacity (HALT, as exec 0x9f does); 0x80089858 `movle r0,#3 ; ble` sets state 3 when count<=0; else 0x8008986c `bl 0x80088798` copies each DCodeSeg+0x24 entry point out one 4-byte word per queue entry (0x800887c8 `ldr r0,[r5,#0x24]`, 0x800887dc kumemput 0x8006aff0), and 0x80089888 `mov r0,#2 ; strb r0,[DLibrary+0x24]` sets state 2. TAIL 0x800898a0..0x800898b4 releases the DLL lock (DMutex::Signal 0x80085bf4) ONLY when count==0 (0x800898a8 `bne 0x800898c4` skips the release otherwise); 0x800898d0 kumemput's the count to *aTotal. EKA2L1 (cross-reference) names SVC 0x9d library_attach, and every Loader::LoadLibrary in its traces is followed by 0x9d then 0x9e. The DLibrary is a KState object (state/codeseg are JS fields, as every kernel object here); the state-1-vs-3 determinant (DCodeSeg+0x54 bit 0x02000000, ekern 0x80089118 create-branch) is set by load_library, not read from guest memory.

slow 0x9e0x800898e00x00000000library_attachedcommits the library state 2 -> 3 and releases the DLL lock; state != 2 HALTs (kernel panic 41)
evidence

ekern 0x800898e0 (ARM), disassembled from 5320.rom; the commit half of the library attach. Same preconditions as 0x9d: 0x800898fc `ldr r0,[DProcess+0x114] ; ldr r0,[r0,#0x3c] ; cmp r0,DThread ; bne` DLL lock held by the current thread, 0x8008990c SDblQue::IsEmpty on DThread+0x138, else 0x80089918 `mov r0,#0x29 ; bl 0x8008d484` = KERN-EXEC 41. 0x80089938 `mov r2,#3 ; mov r1,handle ; mov r0,DThread ; bl 0x8006b2ac` resolves object type 3 (miss panics KERN-EXEC 0 at 0x80089958). 0x8008995c `ldrb r6,[DLibrary+0x24]` reads the state, 0x80089960 `cmp r6,#2 ; moveq r0,#3 ; strbeq r0,[DLibrary+0x24]` transitions 2 (EAttaching) -> 3 (EAttached), and 0x80089974 `cmp r6,#2 ; movne r0,#0x29 ; blne 0x8008d484` PANICS KERN-EXEC 41 if the state was not 2. 0x80089980..0x80089988 `bl LockSystem ; ldr r0,[DProcess+0x114] ; bl 0x80085bf4` = DMutex::Signal RELEASES the DLL lock that 0x9d kept held across the client's EDllProcessAttach; 0x8008998c `mov r0,#0` returns KErrNone. EKA2L1 (cross-reference) names SVC 0x9e library_attached.

slow 0x9f0x800899940x00000000process_static_call_listdispatched (process-attach static call list: aList = attached codeseg entry points deps-first EXE-last, *aTotal = count, 0 = KErrNone; -21 once consumed; HALTs if count > capacity or nothing is attached)
evidence

ekern handler 0x80089994 (docs/data/ekern-exec-table.tsv slow 0x9f; ROM word at 0x8006a394 in the 8-byte slow-exec dispatch table). Disassembled from 5320.rom: 0x800899b8 `ldr r0,[r4,#0x2c]` / 0x800899bc `tst r0,#0x10` / 0x800899c0 `bicne r0,r0,#0x10` (one-shot consume) / 0x800899c4 `mvneq r5,#0x14` = -21 when already consumed / 0x800899d8 `bne 0x80089a7c` returns it. r4 is the current thread (r0-0x2c0) and r6 the process (ldr r6,[r0,#-0x290]); the 0x10 bit is SET on a process initial thread at ekern 0x80091ac0 `orr r1,r1,#0x12` into [DThread+0x2c] (enclosing fn 0x800917c4, callers 0x800853ec and 0x80087284, NOT the RThread::Create handler 0x800839fc). On the first call: 0x800899e4 umemget32s the caller's capacity into sp+0xc; 0x800899fc `ldr ip,[r6,#0x108]` = DProcess::iCodeSeg; 0x80089a00 mov r0,#3 + bl 0x8008762c = ClearMarks(3); 0x80089a1c `bl 0x800874d8` = TraverseDeps, whose body (0x800874e0 tst [cs+0x54] with the mark -> already-done; 0x800874f8 sets the mark BEFORE recursing; 0x80087508..0x80087534 recurses over iDeps[0..[cs+0x4c]) from [cs+0x48]; 0x80087538..0x80087550 links SELF onto the queue afterwards; r6 = count) proves the order is deps-first / self-last and that a self-referencing dep is a no-op; 0x80089a28 `cmp r0,r1` vs the capacity with 0x80089a2c `movgt r4,#0x27` = kernel panic 39; 0x80089a40 `bl 0x80088798` walks that queue and for each node umemput32s DCodeSeg+0x24 (0x800887c8 `ldr r0,[r5,#0x24]`, 0x800887dc the copy-out) into the caller's aList; 0x80089a74 copies the count to *aTotal; 0x80089a78 `mov r0,#0` = KErrNone. The published DCodeSeg+0x24 for an XIP module is its TRomImageHeader.entry_point: euser's runner blx's each published address with r0=2 and every ROM DLL's entry_point is the veneer `tst pc,#0 / ldr ip,[pc,#4] / bx ip` whose literal targets a function that does `cmp r0,#2` -> run .init_array, `cmp r0,#3` -> destructors (euser 0x801b4bac, libc 0x82e81620 — byte-identical shape). Caller = EUser ordinal 585 = 0x801b2d35, which mgs_ngi.exe imports (import slot at code offset 0x51fc = 0x249 = 585) and calls at code VA 0x85d0 via the veneer 0xd1f8 `ldr pc,[pc,#-4]`.

slow 0xa00x80089a840x00000000library_detachdrains the pending-library-detach queue: -25 when empty, else publishes the head library's entry points (state->5) and returns 0. A library with deps, or count>capacity, HALTs
evidence

ekern 0x80089a84 = Exec::LibraryDetach(TInt* aTotal, TLinAddr* aList), the DETACH analogue of exec 0x9d (NOT the constructor path — that is exec 0x9f). EKA2L1 (cross-reference) names SVC 0xa0 library_detach. Disassembled from 5320.rom: 0x80089ae4 `mvn r4,#0x18` = -25 (KErrEof) is the DEFAULT; 0x80089ae8 `add r0,r5,#0x138 ; bl 0x8009d2d8` = SDblQue::IsEmpty on the per-thread pending-library-DETACH queue (DThread+0x138), and 0x80089af4 `bne 0x80089b74` returns -25 exactly when it is EMPTY — the common case every routine handle_close then sees. NON-empty: 0x80089af8 `ldr r0,[r5,#0x138]` peeks the head link node (= DLibrary+0x28), 0x80089b04 `sub r5,r0,#0x28` the DLibrary, 0x80089b00 `ldr r2,[r0,#8]` = DLibrary+0x30 = its DCodeSeg; 0x80089b18 `bl 0x80087bc4` builds the codeseg closure (same callList as 0x9d), 0x80089b28 `movgt r4,#0x27` = panic 39 when count > capacity ([sp+0xc], kumemget of *aTotal), 0x80089b4c `bl 0x80088798` umemput32s each DCodeSeg+0x24 entry point to aList one word, 0x80089b64 `mov r0,#5 ; strb r0,[DLibrary+0x24]` sets state 5 (EDetaching), 0x80089ba8 kumemput's the count to *aTotal, and 0x80089bac returns r4 = the copy-out result (0). The euser runner 0x801a617c calls each published entry with r0=3 (0x801a61a2 `movs r0,#3` before `blx r1`, EDllProcessDetach) then issues exec 0xa1. Preconditions (DLL lock held by the current thread, capacity > 0, 0x80089ac0..0x80089ad8 panic KERN-EXEC 40) are NOT modeled — the routine drains run with the lock unheld, exactly as the -25 path always did. hal.dll is a leaf (no TRomDllRefTable), so its detach list is [its own entry point]; a library WITH deps needs the subtraction 0x80087560 and HALTs.

slow 0xa10x80089bb80x00000000library_detacheddequeues the head detached library, resets it, returns -25 when the queue is now empty else 0; an empty queue HALTs (KERN-EXEC 42)
evidence

ekern 0x80089bb8 = Exec::LibraryDetached(void), the commit half of the detach (mirror of exec 0x9e). EKA2L1 (cross-reference) names SVC 0xa1 library_detached. Disassembled from 5320.rom: 0x80089bcc..0x80089bd8 requires the DLL lock held by the current thread and 0x80089be0 `bl 0x8009d2d8` requires DThread+0x138 NON-empty, else 0x80089bec `mov r0,#0x2a ; bl 0x8008d484` = KERN-EXEC 42; 0x80089c00 `ldr r0,[r4,#0x138]` the head link node, 0x80089c04 `ldm r0,{r1,r2}` its next/prev, 0x80089c0c/0x80089c10 `str r2,[r1,#4] ; str r1,[r2]` UNLINK it (dequeue), 0x80089c08 `sub r5,r0,#0x28` the DLibrary; 0x80089c30 `mov r0,#1 ; strb r0,[DLibrary+0x24]` resets state to 1, 0x80089c3c..0x80089c4c `ldr r0,[DLibrary] ; ldr r2,[r0,#8] ; blx r2` releases the object via its vtable+8; 0x80089c54 IsEmpty again, 0x80089c5c `mvnne r7,#0x18` = -25 when the queue is now EMPTY (and 0x80089c70..0x80089c78 releases the DLL lock, not modeled), else 0. The runner loops back to 0xa0 while this returns 0. In the model the detached codeseg leaves ks.codesegs.loaded (a later reload re-attaches it).

slow 0xad0x800848540xa0000002process_security_infoRProcess::SecureId / VendorId / HasCapability read this block (EKA2L1 process_security_info, cross-reference)
evidence

ekern 0x80084854 = Exec::ProcessSecurityInfo(DProcess*, SSecurityInfo* aInfo) (flags 0xa0000002, preprocessed PROCESS handle): 0x8008485c..0x80084868 `add r1,r0,#0x94 ; ldm r1,{r2,r3,ip,lr} ; stm sp,{r2,r3,ip,lr}` copies the 16 bytes at DProcess+0x94, 0x8008486c unlocks, 0x80084870..0x8008487c `mov r2,#0x10 ; mov r1,sp ; mov r0,r4 ; bl 0x8006bca8` kumemput's them to aInfo. DProcess+0x94 is the SSecurityInfo {iSecureId, iVendorId, iCaps[2]} the loader copies from the image header, the same 16 bytes at E32ImageHeaderV+0x80 (web/src/loader/e32.js reads secureId at 128, vendorId at 132, cap1/cap2 at 136/140), which KState.attachProcess records as process.securityInfo. No return value is produced.

slow 0xd00x80084b500xc0000000process_get_data_parameterRProcess::GetParameter (EKA2L1 process_get_data_parameter, cross-reference)
evidence

ekern 0x80084b50 = Exec::ProcessGetDataParameter(TInt aSlot, TUint8* aData, TInt aLength) on the CURRENT process (0x80084b68 `ldr r6,[r0,#-0x290]`): 0x80084b5c/0x80084b70/0x80084b78 `cmp r5,#0x10 ; movhs r0,#0x33 ; blhs 0x8008d468` panics the thread with 0x33 for a slot >= 16; 0x80084b7c..0x80084b8c `add r0,r6,r5,lsl #2 ; ldr r1,[r0,#0xbc] ; cmp r1,#0 ; mvneq r0,#0` = -1 KErrNotFound when the DProcess+0xbc parameter slot is empty; 0x80084b90..0x80084bb8 handle a slot whose low two type bits are 2 (a descriptor: length check against aLength, -6 KErrArgument otherwise), not modelled because no code in this KState has ever written a parameter slot (RProcess::SetParameter, exec 0x79, is unimplemented), so every slot is 0 and the answer is -1, which is also what EKA2L1 logs for the game ('Parameter slot unused').

Every exec number the user-thread tables dispatch

269 rows: the fast table (count-first, at 0x80069E1C) and the slow table (base 0x80069E98), each handler address re-read from the ROM. The last column is the name docs/data/euser-svc-map.tsv gives the number — a lead only, from a table whose ordinal column is known to be two slots low.

show the 269 rows
execekern handlerflagsEUser wrapper ordinalsestablished aslead (unverified)
fast 0x000x80068708wait_for_any_requestUser::RaiseException(TExcType)
fast 0x010x8006a674allocator_get
fast 0x020x80082140heap_switchUser::Critical()
fast 0x030x8006a67c
fast 0x040x8006a680
fast 0x050x8006a684
fast 0x060x8006a68cCActiveScheduler::Start()
fast 0x070x8008f528TExtendedLocale::TExtendedLocale() (base object constructor)
fast 0x080x8006a694trap_handler_getCTrapCleanup::~CTrapCleanup() (complete object destructor)
fast 0x090x8006a69cset_trap_handlerTCapabilitySet::HasCapability(TCapability) const
fast 0x0a0x8006a620
fast 0x0b0x8006a634
fast 0x0c0x80082168
fast 0x0d0x80082178UserSvr::HalSet(int, void*)
fast 0x0e0x80082194User::ResetInactivityTime()
fast 0x0f0x80069880User::SetProcessCritical(User::TCritical)
fast 0x100x8006f2b0User::LowerCase(unsigned int)
fast 0x110x8006e2701215
fast 0x120x8006e2501219UserSvr::HalFunction(int, int, void*, void*)
fast 0x130x80072b0c1223rom_header_addressUserSvr::ResetMachine(TMachineStartupType)
fast 0x140x80072b181230User::AfterHighRes(TTimeIntervalMicroSeconds32)
fast 0x150x8006a648UserSvr::SetMemoryThresholds(int, int)
fast 0x160x8006a660RAllocator::__DbgMarkStart()
fast 0x170x8006a6ac
fast 0x180x800821a4
fast 0x190x8008f530TLocale::FormatCurrency(TDes16&, TDes16Overflow&, int)
fast 0x1a0x800821d8
fast 0x1b0x800979e42208
slow 0x000x800821ec0x00000000
slow 0x010x800822980xe0000003rchunk_baseRCondVar::Wait(RMutex&)
slow 0x020x800822a00xe0000003RChunk::AdjustDoubleEnded(int, int) const
slow 0x030x800822a80xe0000003rchunk_max_sizeHBufC8::__DbgTestInvariant() const
slow 0x040x800822b00x00000000CDeltaTimer::Queue(TTimeIntervalMicroSeconds32, TDeltaTimerEntry&)
slow 0x050x8008e2cc0x00000000RArrayBase::DoReserve(int)
slow 0x060x8008137c0xa000000aTDblQueBase::TDblQueBase() (base object constructor)
slow 0x070x800813ec0xa000000aRDevice::IsAvailable(int, TDesC16 const*, TDesC8 const*) const (complete object constructor)
slow 0x080x8008146c0xa000000aCObject::FullName() const
slow 0x090x800822b80x000000001401
slow 0x0a0x800814e40xa000000cRBusLogicalChannel::Open(RMessagePtr2, int, TOwnerType)
slow 0x0b0x800911ec0xc0000000
slow 0x0c0x800712c40x00000000Time::MinTTime()
slow 0x0d0x800823400x00000000
slow 0x0e0x800823480x00000000library_lookupCBufBase::Read(int, TDes8&, int) const
slow 0x0f0x800823b80xa0000004RLibrary::Type() const
slow 0x100x800824040x000000001229
slow 0x110x800824380xe0000006mutex_waitRCriticalSection::Wait()
slow 0x120x8008243c0xa0000006mutex_signalRMutex::Open(RMessagePtr2, int, TOwnerType)
slow 0x130x8008245c0xe0000002
slow 0x140x800824640x00000000UserSvr::DllFreeTls(int)
slow 0x150x800824d40xe0000002RProcess::SetJustInTime(int) const
slow 0x160x800825100xa0000002process_filenameRProcess::Logon(TRequestStatus&) const
slow 0x170x800825640xa0000002process_command_lineUser::JustInTime()
slow 0x180x800825e40xe0000002
slow 0x190x800825ec0xe0000002RProcess::Type() const
slow 0x1a0x800825f40xa0000002RProcess::LogonCancel(TRequestStatus&) const
slow 0x1b0x800826300xe0000002
slow 0x1c0x800826400xa0000002RProcess::FileName() const
slow 0x1d0x800827740xe0000002RProcess::Rendezvous(TRequestStatus&) const
slow 0x1e0x8008277c0xe0000002User::HandleException(void*)
slow 0x1f0x800828180xa0000005RSemaphore::Open(int, TOwnerType)
slow 0x200x800828600xa0000005RSemaphore::Open(RMessagePtr2, int, TOwnerType)
slow 0x210x800828640xa0000005RSemaphore::Wait(int)
slow 0x220x8006df7c0xa0000008RLibrary::Close()
slow 0x230x8008a5fc0xa0000008
slow 0x240x8008a6000xa0000021
slow 0x250x8006acec0xb0000009
slow 0x260x800828a40xe0000001thread_idRChunk::Bottom() const
slow 0x270x8008a6640x00000000session_shareCleanupStack::PushL(CBase*)
slow 0x280x800828ac0xe0000001thread_resumeRThread::RendezvousCancel(TRequestStatus&) const
slow 0x290x8008d5640xe0000001RThread::Id() const
slow 0x2a0x800828e40xe0000001RThread::Context(TDes8&) const
slow 0x2b0x800828ec0xe0000001RThread::Suspend() const
slow 0x2c0x800829940xe0000001RThread::HandleCount(int&, int&) const
slow 0x2d0x80082a880xa0000001RThread::ExitCategory() const
slow 0x2e0x80082a900xe0000001
slow 0x2f0x80082a980xe0000001
slow 0x300x80082b080xe0000001RThread::RequestComplete(TRequestStatus*&, int) const
slow 0x310x80082b100xe0000001RThread::SetPriority(TThreadPriority) const
slow 0x320x80082b180xe0000001RThread::Process(RProcess&) const
slow 0x330x80082b200xa0000001RThread::LogonCancel(TRequestStatus&) const
slow 0x340x8006a8480xa0000001
slow 0x350x8008f53c0xe0000007
slow 0x360x8008f55c0xe0000007RTimer::At(TRequestStatus&, TTime const&)
slow 0x370x8008f5a00xe0000007User::SetUTCTimeAndOffset(TTime const&, TTimeIntervalSeconds)
slow 0x380x8008f5f40xe0000007RTimer::CreateLocal()
slow 0x390x8008b28c0xa000000dRServer2::CreateGlobal(TDesC16 const&) (complete object constructor)
slow 0x3a0x8008b29c0xa000000d
slow 0x3b0x80082ba80x00000000request_signal
slow 0x3c0x80082bb40x00000000RArrayBase::FindIsq(void const*, int (*)(void const*, void const*), int) const
slow 0x3d0x80082c200x00000000RHandleBase::Attributes() const
slow 0x3e0x80082c8c0x00000000
slow 0x3f0x80082e400xa0000001
slow 0x400x80082e980x00000000User::ProcessCritical()
slow 0x410x80082f180x00000000
slow 0x420x8006ab9c0xa0000020RMessagePtr2::WriteL(int, TDesC16 const&, int) const (complete object constructor)
slow 0x430x8008a7740xa0000021RMessagePtr2::WriteL(int, TDesC8 const&, int) const
slow 0x440x8008f6340x00000000TSwap::TSwap() (complete object constructor)
slow 0x450x8008f6800x00000000User::LeaveIfNull(void*)
slow 0x460x80082f940x00000000User::CommandLineLength()
slow 0x470x80081ba00xc0000000
slow 0x480x80081bec0x80000000UserSvr::IpcV1Available()
slow 0x490x80081c300x00000000UserSvr::ChangeLocale(TDesC16 const&) (complete object constructor)
slow 0x4a0x80081c880x80000000
slow 0x4b0x80081cd40x00000000UserSvr::WsRegisterThread()
slow 0x4c0x8006ac800xb0000009session_send_sync
slow 0x4d0x800831280x00000000dll_tlsUserSvr::RomRootDirectoryAddress()
slow 0x4e0x800831440x00000000hal_getMath::Round(double&, double const&, int)
slow 0x4f0x800832100xc0000000UserSvr::UnlockRamDrive()
slow 0x500x800832540xc0000000E32Loader::ReadExportDir(void*, unsigned long*)
slow 0x510x8008329c0xe0000002process_command_line_lengthUser::Allocator()
slow 0x520x8008f7580xe0000007RMutex::Wait()
slow 0x530x8008f7980x00000000Time::NullTTime()
slow 0x540x8008f7a40x00000000User::LeaveNoMemory()
slow 0x550x800832dc0x00000000RChunk::Create(unsigned int, int, int, int, TDesC16 const*, TOwnerType) (complete object constructor)
slow 0x560x800700ec0xc0000000
slow 0x570x800700f40xc0000000
slow 0x580x800700fc0xc0000000
slow 0x590x8008336c0xe0000001User::GetDesParameter(int, TDes8&)
slow 0x5a0x800833b00xe0000001User::SwitchAllocator(RAllocator*)
slow 0x5b0x800834040xe0000001User::SetPriorityControl(int)
slow 0x5c0x8008345c0xe0000001
slow 0x5d0x8008357c0xa0000001User::SetJustInTime(int)
slow 0x5e0x800835f00x00000000
slow 0x5f0x800836880x00000000RCondVar::Broadcast()
slow 0x600x800837200x00000000User::Exit(int)
slow 0x610x800838d40xc0000000UserSvr::WsRegisterSwitchOnScreenHandling(int)
slow 0x620x800839200xe0000004CBufBase::Read(int, TDes8&) const
slow 0x630x800839700xe0000002RDevice::GetCaps(TDes8&) const
slow 0x640x8008399c0xe0000003rchunk_topRChunk::Top() const
slow 0x650x800839a40xe0000003RChunk::Size() const
slow 0x660x800839b40xa0000001RThread::GetCpuTime(TTimeIntervalMicroSeconds&) const
slow 0x670x800839fc0x00000000thread_create
slow 0x680x80083b2c0x00000000
slow 0x690x80083b980x00000000handle_closeRHandleBase::SetHandleNC(int)
slow 0x6a0x80083be40x00000000rchunk_create
slow 0x6b0x80083d480xa0000003rchunk_opRChunk::Base() const
slow 0x6c0x80083ee80x00000000
slow 0x6d0x80083f840x00000000handle_duplicateRHandleBase::Open(TFindHandleBase const&, TOwnerType)
slow 0x6e0x800840b40x00000000mutex_createRMutex::OpenGlobal(TDesC16 const&, TOwnerType) (complete object constructor)
slow 0x6f0x800841840x00000000sema_createRArrayBase::RArrayBase(int, int, int) (base object constructor)
slow 0x700x800842ac0x00000000RThread::Rendezvous(int)
slow 0x710x800842b40x00000000RProcess::SetParameter(int, int)
slow 0x720x8008d5a00x00000000thread_killRRegion::RRegion() (base object constructor)
slow 0x730x800842bc0xa0000001RThread::ExitReason() const
slow 0x740x8008432c0xa0000001
slow 0x750x800843900x00000000dll_set_tlsUserSvr::AddEvent(TRawEvent const&)
slow 0x760x800843d00x00000000dll_free_tlsUnknown
slow 0x770x800843f80x00000000RThread::Create(TDesC16 const&, int (*)(void*), int, RAllocator*, void*, TOwnerType) (complete object constructor)
slow 0x780x800844dc0x00000000RProcess::Create(TDesC16 const&, TDesC16 const&, TUidType const&, TOwnerType) (complete object constructor)
slow 0x790x8008d6380x00000000RProcess::SetParameter(int, TDesC16 const&) (complete object constructor)
slow 0x7a0x800845b40xa0000002RProcess::ExitReason() const
slow 0x7b0x800846240xa0000002
slow 0x7c0x800846f00xa0000001
slow 0x7d0x8008a80c0x00000000RRefBase::DoAllocL(void const*, int)
slow 0x7e0x8008a9280x00000000session_createPlatSec::IsCapabilityEnforced(TCapability)
slow 0x7f0x8008aa800x00000000PlatSec::ConfigSetting(PlatSec::TConfigSetting)
slow 0x800x800815780x000000001397
slow 0x810x800816700x00000000User::RequestComplete(TRequestStatus*&, int)
slow 0x820x8008176c0x00000000
slow 0x830x8008f7c00x00000000Huffman::HuffmanL(unsigned long const*, int, unsigned long*)
slow 0x840x8008f8440xe0000007RTimer::After(TRequestStatus&, TTimeIntervalMicroSeconds32)
slow 0x850x800847380xc0000000User::SetHomeTime(TTime const&)
slow 0x860x8008b2ac0x00000000
slow 0x870x8008b3200x00000000TCapabilitySet::TCapabilitySet(TCapability, TCapability) (base object constructor)
slow 0x880x8008b39c0xe000000eRHandleBase::Close()
slow 0x890x8008b3a00xa000000e
slow 0x8a0x800847700x00000000User::WaitForRequest(TRequestStatus&, TRequestStatus&)
slow 0x8b0x800847740xa0000001RThread::ExitType() const
slow 0x8c0x80088e300x000000001403
slow 0x8d0x80088ee00x000000001404E32Loader::LocaleExports(void*, int (**)())
slow 0x8e0x80088f640x000000001405E32Loader::CodeSegClose(void*)
slow 0x8f0x800891c40x000000001400E32Loader::CodeSegCreate(TCodeSegCreateInfo&)
slow 0x900x800892540x000000001402E32Loader::CodeSegInfo(void*, TCodeSegCreateInfo&)
slow 0x910x800892a80x000000001399E32Loader::CodeSegOpen(void*, int)
slow 0x920x8008936c0x000000001398E32Loader::DeviceLoad(void*, int)
slow 0x930x800893d40x000000001412CSession2::~CSession2() (base object destructor)
slow 0x940x8008942c0x000000001411E32Loader::GetV7StubAddresses(unsigned long&, unsigned long&)
slow 0x950x8008944c0x000000001414E32Loader::CheckClientState(int)
slow 0x960x8008946c0x000000001406E32Loader::CodeSegAddDependency(void*, void*)
slow 0x970x800895700x000000001407E32Loader::CodeSegLoaded(TCodeSegCreateInfo&)
slow 0x980x800896940xe00000221409E32Loader::LibraryCreate(TLibraryCreateInfo&)
slow 0x990x800896d00xe00000011413E32Loader::ProcessLoaded(TProcessCreateInfo&)
slow 0x9a0x800896d80x000000001408E32Loader::CodeSegDeferDeletes()
slow 0x9b0x800897040x00000000dll_lock_waitE32Loader::ProcessCreate(TProcessCreateInfo&, TDesC8 const*)
slow 0x9c0x800897380x00000000dll_lock_signal
slow 0x9d0x800897740x00000000library_attachRLibrary::Load(TDesC16 const&, TDesC16 const&, TUidType const&) (complete object constructor)
slow 0x9e0x800898e00x00000000library_attached
slow 0x9f0x800899940x00000000process_static_call_listUser::CommandLine(TDes16&)
slow 0xa00x80089a840x00000000library_detach
slow 0xa10x80089bb80x00000000library_detached
slow 0xa20x800847c80x00000000
slow 0xa30x800848040x00000000RThread::ProcessPriority() const
slow 0xa40x8008482c0x00000000RProcess::GetMemoryInfo(TProcessMemoryInfo&) const
slow 0xa50x8008ab8c0xe0000021
slow 0xa60x8008abc00xe0000021CConsoleBase::WhereY() const
slow 0xa70x8008abf40xe8000021
slow 0xa80x8008ad600xa0000022RMessagePtr2::GetDesMaxLengthL(int) const
slow 0xa90x8008adb80xa0000022
slow 0xaa0x8006ae780xa0000020RFastLock::Wait()
slow 0xab0x8008adf40x00000000RMessagePtr2::ClientProcessFlags() const
slow 0xac0x8008ae5c0xa0000021RArrayBase::RArrayBase(int, int) (base object constructor)
slow 0xad0x800848540xa0000002process_security_infoTSecurityInfo::TSecurityInfo(RMessagePtr2) (complete object constructor)
slow 0xae0x800848840xa0000001TSecurityInfo::SetToCreatorInfo()
slow 0xaf0x800848b80xa0000022TMonthNameAbb::TMonthNameAbb() (base object constructor)
slow 0xb00x800848ec0x00000000RProcess::VendorId() const
slow 0xb10x800849040x00000000CArrayVarBase::FindIsq(void const*, TKeyArrayVar&, int&) const
slow 0xb20x800849100xe0000003RChunk::CreateDisconnectedLocal(int, int, int, TOwnerType)
slow 0xb30x80093f100x00000000TFindServer::Next(TBuf<256>&)
slow 0xb40x800941380x00000000
slow 0xb50x800942100xa000000f
slow 0xb60x800942d80xa000000fRMsgQueueBase::NotifyDataAvailable(TRequestStatus&)
slow 0xb70x800943580xa000000f
slow 0xb80x800943ec0xa000000fRMsgQueueBase::ReceiveBlocking(void*, int)
slow 0xb90x800943fc0xa000000f
slow 0xba0x800944080xe000000fRMsgQueueBase::SendBlocking(void const*, int)
slow 0xbb0x800954940x00000000
slow 0xbc0x800955300x00000000RProperty::Cancel()
slow 0xbd0x800956640x00000000RProperty::Set(TDesC16 const&) (complete object constructor)
slow 0xbe0x800956f00xa0000010RProperty::Define(TUid, unsigned int, int, int)
slow 0xbf0x8009570c0xa0000010
slow 0xc00x8009571c0xa0000010RProperty::Set(int)
slow 0xc10x8009575c0xa0000010RProperty::Get(TUid, unsigned int, TDes16&)
slow 0xc20x800957b80xa0000010
slow 0xc30x800958d40x00000000RProperty::Set(TUid, unsigned int, TDesC16 const&) (complete object constructor)
slow 0xc40x800959cc0x00000000
slow 0xc50x80095a780x00000000RNotifier::RNotifier() (complete object constructor)
slow 0xc60x80095b380x00000000
slow 0xc70x80095b740x00000000RProperty::Get(TDes16&)
slow 0xc80x8008c6a80x00000000TTime::RoundUpToNextMinute()
slow 0xc90x8008c7280x00000000CIdle::~CIdle() (complete object destructor)
slow 0xca0x8008c7b80x00000000CIdle::~CIdle() (base object destructor)
slow 0xcb0x8008c8100x00000000
slow 0xcc0x8008ca440x00000000Power::EnableWakeupEvents(TPowerState)
slow 0xcd0x800849380xe0000002RProcess::SetPriority(TProcessPriority) const
slow 0xce0x800849bc0x00000000RProcess::DataCaging(int)
slow 0xcf0x80084aa80xc0000000RSemaphore::CreateGlobal(TDesC16 const&, int, TOwnerType) (complete object constructor)
slow 0xd00x80084b500xc0000000process_get_data_parameterUser::CountAllocCells()
slow 0xd10x80084c100xc0000000
slow 0xd20x80084c680xe0000022
slow 0xd30x80084c740xa0000001RThread::Logon(TRequestStatus&) const
slow 0xd40x80084cd80xe0000001
slow 0xd50x80093d8c0xa0000001
slow 0xd60x80086f900x00000000RCondVar::OpenGlobal(TDesC16 const&, TOwnerType) (complete object constructor)
slow 0xd70x800870780xa0000011RCondVar::Open(RMessagePtr2, int, TOwnerType)
slow 0xd80x800871280xa0000011RCondVar::Signal()
slow 0xd90x8008712c0xa0000011RCondVar::Open(int, TOwnerType)
slow 0xda0x80096d980x00000000PlatSec::EmitDiagnostic()
slow 0xdb0x80089c840x00000000UserSvr::WsSwitchOnScreen()
slow 0xdc0x8006a82c0xa0000001RThread::RequestCount() const
slow 0xdd0x80084d400xe0000006RMutex::Open(int, TOwnerType)
slow 0xde0x80089cc40x00000000TKeyArrayVar::TKeyArrayVar(int, TKeyCmpText, int) (base object constructor)
slow 0xdf0x80089d000x00000000
slow 0xe00x80084d600x00000000User::IsRomAddress(int&, void*)
slow 0xe10x80084dc40x00000000
slow 0xe20x80084e300x00000000TDesC16::CompareC(TDesC16 const&, int, TCollationMethod const*) const (complete object constructor)
slow 0xe30x80084e600x00000000
slow 0xe40x80084e9c0x00000000
slow 0xe50x8008af0c0xe0000021RMessagePtr2::GetDesMaxLength(int) const
slow 0xe60x800701040x00000000User::FreeLogicalDevice(TDesC16 const&) (complete object constructor)
slow 0xe70x80097bec0x08000000Unknown
slow 0xe80x80097cfc0x08000000
slow 0xe90x800979f40x000000002209
slow 0xea0x80084fa00x00000000RProcess::Resume()
slow 0xeb0x80089d340x000000002221
slow 0xec0x80089d6c0x000000002222
slow 0xed0x800700a00x000000002229
slow 0xee0x800700d80x00000000
slow 0xef0x800700a80x00000000
slow 0xf00x8008ffc00x00000000

Struct layouts

Projected from db/os/struct.tsv and struct_field.tsv: 25 layouts, 160 fields, each offset read from the instructions that use it. Typed accessors are generated from these rows so that no hand-written code carries a magic offset.

tld — 0x60 bytes

allocated in kstate.installBootHeap

offsetfieldsizetypenote
0x0heap4ptrRAllocator* — the thread's current heap (exec 0x01/0x51)
0x4trap_handler4ptrTTrapHandler* installed by Exec::SetTrapHandler (fast 0x09)
evidence

euser thread-local data block; [+0]=RAllocator* heap, [+4]=TTrapHandler* (set by exec9)

ttraphandler — 0x8 bytes

TCleanupTrapHandler: {+0 vptr, +4 iCleanup}

offsetfieldsizetypenote
0x0vptr4ptrTCleanupTrapHandler vtable
0x4icleanup4ptrCCleanup* — what GetActiveCleanup derefs and PushL/Pop operate on
evidence

euser GetActiveCleanup @0x801a19ba = `svc8 ; ldr r0,[r0,#4]` -> iCleanup at +4

ctrapcleanup — 0xc bytes

{+0 vptr, +4 iHandler(=ttraphandler), +8 iCleanup}

offsetfieldsizetypenote
0x0vptr4ptrCTrapCleanup vtable
0x4ihandler4ptrembedded TTrapHandler subobject (its address is published to [TLD+4])
0x8icleanup4ptrCCleanup* created by CTrapCleanup::New
evidence

CTrapCleanup::New @0x801a18ca embeds the TTrapHandler at +4 and stores iCleanup at +8

romimageheader — 0x78 bytes

XIP module header, immediately followed by the code section: headerVA = code_address - 0x78. The superseded 128-byte model plus a compensating ROM_CODE_SHIFT=8 loaded the image at the same address but misread every VA out of the header.

offsetfieldsizetypenote
0x0uid14u320x10000079 = DLL, 0x1000007a = EXE (EUser.dll vs ekern.exe, read from the ROM)
0x8uid34u32module UID; EUser 0x100039e5, libc 0x10207369 — matches the [uid] in mgs_ngi.exe's import decoration libc{00010000}[10207369]
0x10entry_point4ptrthe address exec 0x9f publishes. EUser 0x80199078, libc 0x82e63fb8, libz 0x82e3bdd8, libdl 0x82e99458, libpthread 0x82e863b8. 1422 of the 1425 ROM images have entry_point == code_address, and 1359 of them (of 1361 DLLs; the two exceptions 0x8376eda0 and 0x83f74030 start with Thumb code and are exactly the two whose entry_point != code_address) begin with the identical self-contained veneer e31f0000 (tst pc,#0) / e59fc004 (ldr ip,[pc,#4]) / e12fff1c (bx ip) / padding / _E32Dll address. The veneer loads r12 itself, so it needs no register setup — call it verbatim, never +8.
0x14code_address4ptrfirst code byte; == headerVA + 0x78 for all 1425 ROM images found by the sweep
0x18data_address4ptrVA of the initialised-data TEMPLATE in ROM; 0 when data_size = 0 (EUser, libz, libpthread). NOT where the module's writable data lives at runtime — see data_bss_linear_base.
0x1ccode_size4u32EUser 0x32588, libc 0x21a60, libz 0xa2ec, libdl 0x724, libpthread 0x2234
0x20text_size4u32read-only text; equal to code_size in all six modules measured
0x24data_size4u32initialised-data byte count; libc 0x908, libdl 0xc, backend.dll 0x10, ekern 0x1f0
0x28bss_size4u32zero-fill byte count; libc 0x6830, libdl 0x1f8, backend.dll 0x8b4, ekern 0x1a94
0x2cheap_min4u32copied verbatim by the ROM codeseg builder 0x80071e50 `ldr r1,[lr,#0x2c] ; str r1,[r0,#0x1d8]` (lr = the TRomImageHeader; the run continues with +0x30 -> +0x1dc and +0x34 -> +0x1e0); efile.exe reads 0x1000, ekern.exe 0x1000
0x30heap_max4u320x80071e58 `ldr r1,[lr,#0x30] ; str r1,[r0,#0x1dc]`; efile.exe 0x1000000 — the same value mgs_ngi.exe's E32 heapMax has, and what the chunk-create path reports back through exec 0x3
0x34stack_size4u320x80071e60 `ldr r1,[lr,#0x34] ; str r1,[r0,#0x1e0]`; efile.exe 0x4000
0x38dll_ref_table_address4ptrVA of the romdllreftable, or 0 when the module has no init dependencies. libc 0x82e86320, libz 0x82e460c4, libpthread 0x82e885ec, libdl 0x82e99b88, backend.dll 0x82e55d2c; 0 for EUser, LibCIpcClient, libm and ekern.
0x3cexport_dir_count4u32EUser 2229 (0x8b5), libc 560 (0x230), libz 43, libpthread 52, libdl 4
0x40export_dir_address4ptrEUser 0x801b4f04, libc 0x82e816cc; ordinal N is the u32 at +4*(N-1), bit0 = Thumb
0x60data_bss_linear_base4ptrwhere the module's WRITABLE data (.data then .bss) lives at runtime — a RAM linear address, distinct and page-aligned per module: libc 0x3ffb7000, libm 0x3ffb6000, libdl 0x3ffb5000, LibCIpcClient 0x3ffbf000, backend.dll 0x3ffc0000, ekern 0xc8000000. Modules with data_size = bss_size = 0 (EUser, libz, libpthread) carry the placeholder 0x00400000. Decisive cross-check: the null member db/frontier.json recorded at 0x3ffc0094, dereferenced by code at 0x82e462a8, lies inside backend.dll's [0x3ffc0000, +0x8c4) window while 0x82e462a8 lies inside backend.dll's code — the module addresses its own globals at this base.
0x6ctotal_data_size4u32exactly data_size + bss_size for every module measured: libc 0x7138 = 0x908+0x6830, libdl 0x204 = 0xc+0x1f8, backend.dll 0x8c4 = 0x10+0x8b4, ekern 0x1c84 = 0x1f0+0x1a94; 0 where both are 0
0x70module_version4u32EUser 0x000a0000, libc/libz/libdl/libpthread 0x00010000 — exactly the {version} in mgs_ngi.exe's import decorations euser{000a0000} and libc{00010000}, an independent confirmation that this word sits at +0x70 under a 0x78-byte header
evidence

Size MEASURED across the whole ROM, not assumed. (a) Each standalone samples/firmware/5320/sys/bin/<mod> file's first 96 bytes occur EXACTLY ONCE in samples/firmware/5320/5320.rom, and code_address - (0x80000000 + thatFileOffset) = 120 for all six of EUser.dll, libc.dll, libz.dll, libdl.dll, libpthread.dll and ekern.exe. (b) Sweeping the ROM for uid1 in {0x10000079,0x1000007a} with uid2 in {0x1000008d,0x1000008b} finds 1425 places where code_address == headerVA + 0x78 (1361 of them uid1 = DLL). (c) Behavioural: the export directory read at header+0x78 gives EUser ordinal 196 = 0x801a18d3, exactly the CTrapCleanup::New VA that web/src/kernel/kstate.js already calls successfully as a guest subroutine, and ordinal 585 = 0x801b2d35, which is provably the function issuing `svc #0x9f` (ROM 0x8019c318 = ef00009f) and provably the euser ordinal mgs_ngi.exe imports (its import slot at code offset 0x51fc holds 0x249 = 585). Ordinals 196/585/891/1360/1600/2060 all land on a Thumb push prologue with no fixup scan. Reading the directory at +128 instead shifts every ordinal by two export slots.

romdllreftable — 0x4 bytes

fixed 4-byte head; `count` TRomImageHeader* entries follow at +0x4. Dedupe by header VA when walking - a table can name the module itself.

offsetfieldsizetypenote
0x0flags2u160x0000 in every table observed (libc, libz, libpthread, libdl, backend)
0x2count2u16number of TRomImageHeader* entries that follow; 4 for libc/libz/libpthread/libdl, 1 for backend.dll
0x4entry04ptrfirst dependency = another module's TRomImageHeader VA (libc -> 0x82e460e0 backend.dll); subsequent entries at +4*n up to count
evidence

TRomDllRefTable at TRomImageHeader.dll_ref_table_address, read live from 5320.rom. libc.dll's table @0x82e86320 reads flags=0x0000 count=4 then 0x82e460e0 0x82e61340 0x82e63f40 0x82e88600 - each entry is another module's HEADER VA (0x82e63f40 is libc's own header VA, i.e. the table contains a self-reference), not a code address. libz.dll @0x82e460c4, libpthread.dll @0x82e885ec and libdl.dll @0x82e99b88 carry the identical 4-entry table; backend.dll @0x82e55d2c has count=1 -> 0x82e61340; EUser.dll, LibCIpcClient.dll, libm.dll and ekern.exe have dll_ref_table_address = 0. Names resolved from the ROM directory entries whose iAddressLin equals the header VA: 0x82e460e0 backend.dll, 0x82e61340 LibCIpcClient.dll, 0x82e88600 libm.dll.

tdesc — 0x4 bytes

Variable total size by type; only the 4-byte head is fixed, and max_length below is present ONLY for types 2/3/4. A TLitC (what _LIT expands to) is layout-identical to type 0 EBufC, so the same accessors read it. WRITE SIDE (web/src/kernel/descriptor.js writeDes16): K::USetLength ekern 0x8006b9a4 is what Kern::KUDesPut 0x80098088 calls before copying the characters (0x800980c0, then 0x8006bec4): 0x8006b9b0 `ldrt r2,[r0],#4 ; ldrt r3,[r0],#-4` reads word0 and iMaxLength, 0x8006b9b8 `and r2,r2,#0xf0000000 ; cmp r2,#0x20000000 ; blo 0x80097f8c` panics on a non-modifiable type (0 EBufC, 1 EPtrC), 0x8006b9c4 `cmp r1,r3 ; bhi 0x80097f94` panics when the new length exceeds iMaxLength (no KErrOverflow is ever returned), 0x8006b9cc `cmp r2,#0x40000000 ; orr r2,r2,r1 ; strt r2,[r0],#8 ; bxne lr` stores type|newLength, and for type 4 EBufCPtr 0x8006b9dc `ldrt r2,[r0],#0 ; strt r1,[r2],#0` also stores the length into the pointed-to TBufC.

offsetfieldsizetypenote
0x0type_length4u32word0: TDesType in bits 31..28, length in bits 27..0 (ekern 0x8006b944 `bic ip,r3,#0xf0000000`). A value >= 0x50000000 panics the calling thread at 0x8006b954, so only types 0..4 exist.
0x4max_length4u32iMaxLength — read by ekern 0x8006b960 `ldrths ip,[r0],#4` ONLY when C is set, i.e. only for types 2 EPtr, 3 EBuf and 4 EBufCPtr. For types 0 EBufC and 1 EPtrC the kernel leaves maxLength at the `mvn ip,#0` default of -1 and this word is NOT part of the descriptor.
evidence

The fixed 4-byte head EVERY Symbian descriptor starts with, decoded by the kernel at ekern 0x8006b934 (the first act of Kern::KUDesGet 0x80098040, which is how every exec reads a TDesC from user memory). That routine is the specification and is only 16 instructions: 0x8006b93c `ldrteq r3,[r0],#4` takes word0; 0x8006b944 `bic ip,r3,#0xf0000000` = length; 0x8006b94c `mvn ip,#0` presets maxLength to -1; 0x8006b950 `cmp r3,#0x50000000 ; bhs 0x80097f84` PANICS for a type nibble >= 5, which is what fixes the type field at bits 31..28 and the count of valid types at exactly 5; then 0x8006b958 `eor r3,r3,r3,lsr #1 ; msr apsr_nzcvq,r3` converts the nibble (t3..t0) into N=t3, Z=t2^t3, C=t1^t2, V=t0^t1 and three conditional instructions read it back — 0x8006b960 `ldrths ip,[r0],#4` (C set = types 2,3,4: maxLength is word1), 0x8006b968 `ldrtle r0,[r1],#0` (LE = types 1,2,4: the data is behind a pointer) and 0x8006b970 `addeq r0,r0,#4` (Z set = type 4 only: skip the pointed-to TBufC's own length word). Evaluating that flag algebra yields exactly the five documented TDesType layouts — 0 EBufC data at +4; 1 EPtrC data at *(+4); 2 EPtr max at +4, data at *(+8); 3 EBuf max at +4, data at +8; 4 EBufCPtr max at +4, data at *(+8)+4 — so the reading is self-checking rather than assumed. Live confirmation from the boot: the TDesC8 mgs_ngi.exe hands to Exec::SessionCreate reads {0x3000000b, 0x50, '!FileServer'} = type 3 EBuf, length 0xb, maxLength 0x50, text inline at +8.

tromheader — 0x120 bytes

iPrimaryFile = the kernel, iSecondaryFile = the first user-side process the kernel starts. On this ROM that is efile.exe, the F32 file server.

offsetfieldsizetypenote
0x7crestart_vector4u32iRestartVector: ekern's restart path 0x80071b9c..0x80071ba8 `ldr r1,[TheRomHeader] ; add r1,r1,#0x7c ; bx r1` jumps here; 5320.rom +0x7c holds 0xea0006df, an ARM `b` — executable, as a vector must be
0x80time4u32iTime low word (TInt64 at +0x80/+0x84): ekern 0x80072858/0x8007285c `ldr r1,[r0,#0x84] ; ldr r0,[r0,#0x80]` reads both halves off TheRomHeader before scaling them with umull at 0x80072878
0x8crom_base4ptriRomBase: 5320.rom +0x8c reads 0x80000000 — the ROM's own statement of the address it is linked and mapped at, which is where web/src/cpu/memory.js setRomBacking places it and where every tromheader accessor already decodes coincidence-free (iPrimaryFile/iSecondaryFile -> real TRomEntry/TRomImageHeader chains)
0x90rom_size4u32iRomSize: 5320.rom +0x90 reads 0x04200000 (the ROM window size, page-rounded above the image)
0x94rom_root_dir_list4ptrTRomRootDirectoryList* — 0x800458D0 in 5320.rom. Already the value exec 0x4d (rom_root_dir_address) hands back.
0x98kern_data_address4ptr0xC8000000 — identical to ekern.exe's romimageheader.data_bss_linear_base, an independent pin for this field run
0x9ckernel_limit4ptr0xC9100000
0xa0primary_file4ptrTRomEntry* for the kernel image. 0x800458E0 -> iName 'ekern.exe', iAddressLin 0x80067F88 = ekern.exe's TRomImageHeader VA.
0xa4secondary_file4ptrTRomEntry* for the first USER-side process the kernel starts. 0x800462F0 -> iName 'efile.exe', iAddressLin 0x801CB600 = efile.exe's TRomImageHeader VA. This is the F32 file server, and it is why a real 5320 already has a server named '!FileServer' before any application runs.
0xccvariant_file4ptriVariantFile: ekern 0x80071a5c `ldrne r0,[r0,#0xcc]` off TheRomHeader (the extension/variant loader walk 0x80071a28); 5320.rom +0xcc = 0x80045950, a TRomEntry
0xd0extension_file4ptriExtensionFile: ekern 0x80071a58 `ldreq r0,[r0,#0xd0]`; 5320.rom +0xd0 = 0x800458fc, a TRomEntry
0xf4uncompressed_size4u32iUncompressedSize: 5320.rom +0xf4 reads 0x04193000 = 68,759,552 = the byte length of 5320.rom exactly; ekern 0x80072a48..0x80072a64 uses [TheRomHeader+0xf4] as the extent of the ROM for its address-in-ROM test, 0x80078300..0x80078320 page-rounds it
0x100trace_mask04u32iTraceMask[0] of 8 words: kernel entry 0x80072a8c..0x80072aa4 copies [TheRomHeader+0x100+4i] to [TheSuperPage+0x108+4i] for i < 8 — the words fast execs 0xc/0xd/0xe (0x80082168/0x80082178/0x80082194) then serve as the debug mask; 5320.rom +0x100 = 0x80000000
evidence

NOT the whole TRomHeader — the RE'd extent, through iTraceMask[8] at +0x100..+0x11F; the real header is longer and its total size is not measured here. The header lives at THE ROM BASE and the kernel keeps its address in the global 0xc800004c (TheRomHeader, stored at kernel entry 0x80072a84 from r0 of _E32Startup 0x80068110 -> 0x8006e430 -> 0x80072a74; served to user code by fast exec 0x13, handler 0x80072b0c): its iRomBase (+0x8c) reads 0x80000000 and its iUncompressedSize (+0xf4) reads exactly the byte length of 5320.rom, so the base is self-describing. The three anchors are mutually confirming rather than assumed: (a) +0x94 = 0x800458D0 is already relied on by exec 0x4d, and walking it as TRomRootDirectoryList{TInt iNumRootDirs; {TUint iHardwareVariant; TLinAddr iAddressLin}[]} — 1 root dir — yields 2933 named entries whose iAddressLin values include \Sys\Bin\EUser.dll = 0x80199000 and \Sys\Bin\ekern.exe = 0x80067F88, i.e. exactly the TRomImageHeader VAs the loader independently derives; (b) +0x98 = 0xC8000000 is byte-identical to ekern.exe's own romimageheader.data_bss_linear_base already recorded in struct_field.tsv, which pins the field run; (c) +0xA0 and +0xA4 each point at a TRomEntry whose iName decodes to 'ekern.exe' and 'efile.exe' and whose iAddressLin (0x80067F88, 0x801CB600) is a real TRomImageHeader — a coincidence-free decode.

tromentry — 0xa bytes

TRomEntry{TInt iSize; TLinAddr iAddressLin; TUint8 iType; TUint8 iNameLength; TText16 iName[]}; iType bit 0x10 = KEntryAttDir (iAddressLin is then a TRomDir).

offsetfieldsizetypenote
0x0size4u32byte size of the file's image; 0x37CB8 for ekern.exe, 0x205D4 for efile.exe
0x4address_lin4ptrthe file's linear address — for an executable, its TRomImageHeader VA; for a directory (iType & 0x10) a TRomDir*
0x8type1u8entry attributes; 0x81 on both boot files, bit 0x10 = KEntryAttDir
0x9name_length1u8count of UTF-16 units in iName (9 for 'ekern.exe' and 'efile.exe')
0xaname02u16first UTF-16 unit of iName; unit n is at +0xA+2n. Entry stride is ((0xA + name_length*2 + 3) & ~3).
evidence

Fixed head of a ROM directory entry. Decoded live from 5320.rom at both TRomHeader.iPrimaryFile (0x800458E0) and iSecondaryFile (0x800462F0): iSize 0x37CB8/0x205D4, iAddressLin 0x80067F88/0x801CB600, iType 0x81/0x81, iNameLength 9/9, and the 9 UTF-16 units at +0xA read 'ekern.exe'/'efile.exe'. Stepping the whole directory tree with entry stride ((0xA + iNameLength*2 + 3) & ~3) walks all 2933 entries without desynchronising, which is what proves the head is 10 bytes and the name is 16-bit.

tchunkcreate — 0x14 bytes

TChunkCreate as passed to Exec::ChunkCreate (slow 0x6a).

offsetfieldsizetypenote
0x0att4u32attributes; ekern 0x80083c34 / 0x80083c44 mask it with 0x10 and 0xf, and 0x80083c60 tests bit 0x20 to pick kernel chunk type 6 or 8. 0 in the mgs_ngi.exe boot call.
0x4force_fixed4u32copied straight through by ekern 0x80083c4c. 0 in the mgs_ngi.exe boot call.
0x8initial_bottom4u32copied by ekern 0x80083c78. 0 in the mgs_ngi.exe boot call.
0xcinitial_top4u32copied by ekern 0x80083c80. 0 in the mgs_ngi.exe boot call.
0x10max_size4u32the requested maximum size; copied by ekern 0x80083c70. Live value in the mgs_ngi.exe boot call is 0x1000000 = the game's E32 header heapMax, and it is what exec 0x3 (DChunk+0x28) must later report.
evidence

The user-side chunk-creation struct exec 0x6a receives in r2. ekern 0x80083be4 copies exactly 0x14 = 20 bytes from it (0x80083c28 `bl 0x8006baac` with r0=sp+0x44, r1=the user pointer, r2=0x14) and then routes the five words: [+0x0] is masked &0x10 and &0xf and bit-tested with 0x20 to pick the kernel chunk type, while [+0x4], [+0x8], [+0xc] and [+0x10] are copied to the kernel create-info at sp+0x10, sp+0x24, sp+0x28 and sp+0x20. Live confirmation from the boot: mgs_ngi.exe's TChunkCreate at 0x7fff2c reads att=0 +4=0 +8=0 +0xc=0 +0x10=0x1000000, and 0x1000000 is exactly mgs_ngi.exe's E32 header heapMax.

nthread — 0x6c bytes

the nanokernel thread: state, priority, request semaphore, exec tables

offsetfieldsizetypenote
0x8priority1u8absolute scheduling priority 0..63: NKern::ThreadCreate 0x8006f36c `strb r0,[r4,#8]` from NThreadCreateInfo.iPriority; the ready-list code indexes queues by it (0x8006883c `ldrb r2,[r0,#8]`)
0x9nstate1u8iNState: 0 EReady (0x80068844 strb #0 when made ready), 1 ESuspended (0x8006f3b8 at suspended creation, 0x800688ec), 2 EWaitFastSemaphore (0x80068740 `mov r3,#2 ; strb r3,[r1,#9]` in wait_for_any_request), 5 dead (0x8006e5dc `cmp r2,#5` makes Resume a no-op)
0xaattributes1u80x8006f3ac/0x8006f3b0 copied from NThreadCreateInfo+0x14
0x18time4i320x8006f368 initialised to the timeslice; the unready routine 0x800687ac `ldr ip,[r1,#0x1c] ; str ip,[r1,#0x18]` reloads it
0x1ctimeslice4i320x8006f358..0x8006f364: NThreadCreateInfo+0x10, forced to -1 when <= 0; DThread::DoCreate 0x80092a28 passes 0x8006f550(0x14) = 20 ms converted to ticks for a user thread
0x20request_semaphore_count4i32iRequestSemaphore.iCount: wait_for_any_request 0x80068710..0x8006871c `ldr r2,[r1,#0x20] ; subs r2,r2,#1 ; str r2,[r1,#0x20] ; bxge lr`; NKern::ThreadRequestSignal 0x8006ea6c `add r0,r4,#0x20` passes the semaphore to SignalN 0x80068750; 0x8006f2e8 zeroes it at construction
0x24request_semaphore_owner4ptriRequestSemaphore.iOwningThread = the NThread itself: 0x8006f308 `str r4,[r4,#0x24]`; SignalN 0x80068760 `ldr r0,[r0,#4]` is what it wakes
0x28wait_obj4ptriWaitObj: 0x80068730/0x80068738 `add r2,r1,#0x20 ; str r2,[r1,#0x28]` records the request semaphore before blocking; SignalN 0x80068768 clears it before readying
0x2csuspend_count4i32iSuspendCount: -1 for a thread created suspended (0x8006f3bc/0x8006f3c0 `mvneq r0,#0 ; streq r0,[r4,#0x2c]`), 0 for one created running (0x8006f3d8); NKern::ThreadResume 0x8006e60c..0x8006e620 increments a negative count and readies the thread when it reaches 0; CheckSuspendThenReady 0x80068828 `ldr r1,[r0,#0x2c] ; cmp r1,#0 ; bne` keeps a suspended thread off the ready list
0x54stack_base4ptrsupervisor stack: 0x8006f348/0x8006f34c from NThreadCreateInfo+4
0x58stack_size4u320x8006f350/0x8006f354 from NThreadCreateInfo+8
0x5chandlers4ptrSNThreadHandlers*: 0x8006f374..0x8006f380, the default literal when the info leaves it 0
0x60fast_exec_table4ptrthe SVC vector's `ldr r2,[r1,#0x60]` at 0x80068140; 0x8006f384..0x8006f390 installs the default 0x80069e1c whose word 0 is the count (0x1c) and word n the handler of fast exec n
0x64slow_exec_table4ptrthe SVC vector's `ldr r4,[sb,#0x64]` at 0x80068174; 0x8006f394..0x8006f3a8 installs default+0xc, so [r4,#-0xc] is the count (0xf1 at 0x80069e8c), [r4,#-8] the invalid-number handler, [r4,#-4] the argument preprocessor and entry n = {flags, handler} at +8n
0x68saved_sp4ptrthe reschedule 0x80068c8c `str sp,[r1,#0x68]` saves the outgoing thread's supervisor SP here
evidence

NThreadBase, embedded in the DThread at +0x2c0: ekern 0x8006ad54 `sub sb,sb,#0x2c0` turns TScheduler.iCurrentThread into the current DThread, 0x80092aa0 `add r0,r4,#0x2c0` hands the sub-object to NKern::ThreadCreate 0x8006f320, and every fast-exec accessor (0x8006a674..0x8006a6b4) reaches DThread fields as negative offsets from it. Fields read out of the SVC vector 0x80068120/wait_for_any_request 0x80068708, NFastSemaphore::SignalN/Signal 0x80068750/0x8006880c, the ready-list ops 0x800687ac/0x80068828, NKern::ThreadResume 0x8006e5d4 and NThreadBase construction 0x8006f2d8..0x8006f3e8. The RE'd extent ends at iSavedSP +0x68 (reschedule 0x80068c8c `str sp,[r1,#0x68]`); the full size is not measured.

dthread — 0x334 bytes

kernel thread object; the NThreadBase lives at +0x2c0, the sync RMessageK at +0xEC

offsetfieldsizetypenote
0x24user_stack_run_address4ptrAllocateUserStack 0x8007a9cc..0x8007a9d8: stack-chunk base [DProcess+0x100] + the chunk offset + the guard size, stored at +0x24; EpocThreadFunction 0x8006a89c `ldr r5,[r4,#0x24]` is where the 0x29 fill and the info copy start
0x28user_stack_size4u32AllocateUserStack 0x8007a9c8 `str r6,[r5,#0x28]` after 0x80074a00 rounds the requested size up to a 4K page; 0x8006a8a0 reads it back
0x2cflags4u32bit 0x10 marks the process's INITIAL thread (set at 0x80091ac0, consumed by exec 0x9f 0x800899c0); EpocThreadFunction 0x8006a908 `tst r8,#0x10` uses it to choose r4 = 0 + the codeseg entry (initial) or r4 = 1 + DProcess::iReentryPoint (every later thread)
0x30owning_process4ptr0x8006a8a4 `ldr r7,[r4,#0x30]`; 0x800828b8; every [NThread-0x290] in the fast handlers (e.g. 0x8006a6ac)
0x3cthread_priority4i32relative TThrdPriority: DThread::Create 0x80092b54/0x80092b58 from info+0x20, SetThreadPriority 0x80092920 stores it and 0x800928bc maps it (negative values via table[8*DProcess::iPriority + prio + 8]) to the NThread priority
0x44id4u320x80092b4c/0x80092b50 `bl 0x80091198 ; str r0,[r4,#0x44]` at creation; 0x800916d4 reads it for the thread-created notification
0x48allocator4ptriAllocator: fast exec 1's handler 0x8006a674 `ldr r0,[r1,#-0x278]` (= NThread-0x278 = DThread+0x48); HeapSwitch (fast 2) 0x8008214c/0x80082150 returns the old value and stores r0
0x4ccreated_allocator4ptriCreatedAllocator: HeapSwitch 0x80082154..0x8008215c `ldr r3,[r1,#0x4c] ; cmp r3,#0 ; streq r0,[r1,#0x4c]` records the first heap ever installed
0x54trap_handler4ptrfast exec 8/9 handlers 0x8006a694 `ldr r0,[r1,#-0x26c]` and 0x8006a69c `ldr r2,[r1,#-0x26c] ; str r0,[r1,#-0x26c] ; mov r0,r2` (SetTrapHandler returns the previous)
0x58tls4ptrRArray<STls{iHandle,iDllUid,iPtr}> head: DThread::SetTls 0x800931c0, ::Tls 0x8009321c and ::FreeTls 0x80093270 all `add r0,r0,#0x58` before the RArray calls; entries are found by iHandle in signed key order (0x8006ca78/0x8006cc10) and Tls 0x8009323c..0x80093254 returns iPtr only when iDllUid matches
0xecsync_msg4ptrfirst word (queue link) of the RMessageK embedded for synchronous sends: 0x8006ac94 `sub ip,sb,#0x1d4` against sb = NThread (= DThread+0x2c0), 0x8006ac98 `ldr r7,[ip] ; cmp r7,#0` tests it
0x138pending_library_q4ptriNext of an SDblQue of DLibrary pending detach (linked at DLibrary+0x28). Only this word is evidenced: SDblQue::IsEmpty 0x8009D2D8 reads [head] and compares it against head, and the DThread ctor self-links it at 0x800927F8. The full SDblQue extent (iPrev, iOffset) is NOT established here. Execs 0x9b/0x9c require the queue empty and panic KERN-EXEC 37/43 otherwise
0x149mstate1u8iMState: 0x80091620/0x80091624 `mov r0,#2 ; strb r0,[r5,#0x149]` at creation; DThread::Resume 0x8009384c..0x80093854 `ldrb r0,[r4,#0x149] ; cmp r0,#9 ; addls pc,pc,r0,lsl #2` dispatches on it
0x15csupervisor_stack4ptrDoCreate 0x800929e0/0x800929e4 takes info+0x10 for thread types 0/2, else 0x800929f8 allocates one
0x160supervisor_stack_size4u32DoCreate 0x800929b8/0x800929bc from info+0x14, the kernel default when 0 (0x800929c4..0x800929d0)
0x165thread_type1u8DThread::Create 0x80092b5c..0x80092b68 `ldr r0,[r5,#4] ; and r0,r0,#0xff ; strb r0,[r4,#0x165]` from info.iType; 3 = EThreadUser (0x8006a880 `cmp r1,#3` selects the user-mode start, 0x80092b8c gates the user-stack allocation)
0x2c0nthread4u32first word of the embedded NThreadBase (struct nthread): 0x80092aa0 `add r0,r4,#0x2c0`; 0x8006ad54 `sub sb,sb,#0x2c0`; 0x8008313c `sub r0,r0,#0x2c0`
evidence

RE'd extent only: fields between +0x24 and +0x165 come from Exec::ThreadCreate 0x800839fc -> DProcess::NewThread 0x80091570 -> DThread::Create 0x80092ae8 -> DoCreate 0x800929ac, AllocateUserStack 0x8007a920, EpocThreadFunction 0x8006a868, HeapSwitch 0x80082140 and the fast-table accessors 0x8006a674..0x8006a6b4 (which address them as [NThread-0x2c0+off]). 0x80092a94/0x80092ab0 save and restore +0x32c/+0x330 around ThreadCreate, so the object extends at least to 0x334; the full size is not measured.

tscheduler — 0x134 bytes

the single scheduler: ready-list bitmap + 64 queues, reschedule flags, current thread

offsetfieldsizetypenote
0x0ready_present_mask4u32two-word priority bitmap at +0/+4: the reschedule 0x80068c84 `ldr lr,[r0,#4]` / 0x80068c94 `ldr r1,[r0],#8` then clz picks the highest set bit; unready 0x800687d8 `bic r1,r1,ip,lsl r3` clears a bit when a queue empties
0x8queue04ptriQueue[0]; queue p is at +8+4p (0x800687c4 `add r1,r0,#8` then `[r1,r2,lsl #2]`). 0x800688a8..0x800688b4 links a newly ready thread before the head, i.e. at the TAIL: FIFO within a priority
0x108reschedule_needed1u8iRescheduleNeededFlag: wait_for_any_request 0x8006872c `strb r3,[r0,#0x108]`; the reschedule entry 0x80068c5c tests it
0x109dfc_pending1u80x80068c4c `ldrb r1,[r0,#0x109]` runs pending DFCs first
0x10ckern_cs_locked4i32iKernCSLocked: wait_for_any_request 0x80068724 `str r3(1),[r0,#0x10c]` takes the kernel lock before unready; 0x800681e4 releases it after a slow exec
0x130current_thread4ptriCurrentThread (NThread*): the SVC vector 0x80068138 `ldr r1,[fp,#0x130]`; every `ldr r0,[r0,#0x130] ; sub r0,r0,#0x2c0` in the exec handlers
evidence

TScheduler, the object the SVC vector loads through the literal at 0x800683a0 (= 0xc800043c, inside ekern's writable data at TRomHeader.kern_data_address 0xC8000000). Fields from the vector 0x80068120..0x80068268, wait_for_any_request 0x80068708, the ready ops 0x800687ac/0x80068828..0x800688b8 and the reschedule 0x80068c40..0x80068cb4. RE'd extent ends at iCurrentThread +0x130; the full size is not measured.

nfastsemaphore — 0x8 bytes

NFastSemaphore; the thread's request semaphore is the one at NThread+0x20

offsetfieldsizetypenote
0x0count4i32SignalN 0x80068750 `ldr r2,[r0] ; adds r2,r2,r1 ; str r2,[r0] ; bxlo lr` wakes the owner only when the old count was negative; Signal 0x8006880c `adds r1,r1,#1 ; bxgt lr`
0x4owning_thread4ptrSignalN 0x80068760 `ldr r0,[r0,#4]` = the NThread to ready; 0x8006f308
evidence

{iCount, iOwningThread}: SignalN 0x80068750 reads the count at [r0] and the owner at [r0,#4]; Signal 0x8006880c `ldm r0,{r1,r2}` the same pair; NThreadBase construction 0x8006f308 `str r4,[r4,#0x24]` makes the NThread that embeds one at +0x20 its own owner.

sthreadcreateinfo — 0x40 bytes

SThreadCreateInfo head (0x30) + the SStdEpocThreadCreateInfo tail euser passes (allocator, heap sizes); iTotalSize says how much

offsetfieldsizetypenote
0x4type4i320x80083a7c/0x80083a80 `mov r0,#3 ; str r0,[sp,#0x6c]` (= copy+4) forces EThreadUser; DThread::Create 0x80092b5c reads it back
0x8function4ptriFunction: not read by the kernel for a user thread; the EXE's RunThread (mgs_ngi.exe 0x8422 `ldr r0,[r4,#8]`, r4 = the info copy at user SP) calls it after SetupThreadHeap
0xcptr4ptriPtr: RunThread 0x8424 `ldr r1,[r4,#0xc]` is the argument passed to iFunction
0x10supervisor_stack4ptrzeroed by the exec handler 0x80083a9c `str r4,[sp,#0x78]`; DoCreate 0x800929e0 only honours it for thread types 0/2
0x14supervisor_stack_size4u32zeroed by 0x80083a98; DoCreate 0x800929b8 substitutes the kernel default for 0
0x1cuser_stack_size4u320x80083a48..0x80083a54 `ldr r0,[sp,#0x84] ; cmp r0,#0x200 ; mvnlt r4,#5` rejects < 0x200 with -6; DThread::Create 0x80092b94 hands it to AllocateUserStack 0x8007a920
0x20initial_thread_priority4i320x80083a84..0x80083a8c `mvn r0,#4 ; str r0,[sp,#0x88]` = -5 (EThrdPriorityNormal) for every RThread::Create; DThread::Create 0x80092b54 copies it to DThread+0x3c
0x24name4u32TPtrC head word of iName: DThread::Create 0x80092b1c/0x80092b20 `ldr r0,[r5,#0x24] ; bics r0,r0,#0xf0000000` — a zero length makes the thread anonymous (0x80092b24..0x80092b28), else SetName 0x8008b75c; the exec handler 0x80083ab0 passes &info+0x24 to 0x800993ec after copying the user's name via KUDesGet
0x28name_ptr4ptrsecond word of the iName TPtrC (type 1 EPtrC per struct tdesc: the data lives behind this pointer)
0x2ctotal_size4u32iTotalSize: 0x80083a24 `ldr r0,[sp,#0x94]` validated as above; 0x80092a54/0x80092a58 becomes NThreadCreateInfo.iParameterBlockSize; EpocThreadFunction 0x8006a878 `ldr sb,[sp,#0x2c]` is the byte count copied to the user stack top
0x30allocator4ptrSStdEpocThreadCreateInfo.iAllocator: UserHeap::SetupThreadHeap 0x801b2d04 `ldr r5,[r1,#0x30]` — non-null is opened (0x801afa5c) and installed via 0x801a4722 = `svc #0x800002` (HeapSwitch)
0x34heap_initial_size4u32iHeapInitialSize: SetupThreadHeap 0x801b2d0c `ldr r1,[r0,#0x34] ; cmp r1,#0 ; ble` creates a heap only when positive; CreateThreadHeap 0x801b0672 page-rounds it (0x801a4bb4 = page size)
0x38heap_max_size4u32iHeapMaxSize: CreateThreadHeap 0x801b067a..0x801b0682 takes max(rounded initial, this) as the chunk maximum
evidence

The block Exec::ThreadCreate 0x800839fc copies from user memory: 0x80083a1c `mov r2,#0x30 ; bl 0x8006baac` takes the 0x30-byte head, then 0x80083a60..0x80083a78 copies iTotalSize-0x30 more once the total is validated — (total-0x30) < 0xd1 and total&7 == 0 at 0x80083a28..0x80083a38, iUserStackSize >= 0x200 at 0x80083a48..0x80083a54, else -6; DThread::Create 0x80092af0..0x80092b04 re-checks 0x30 <= total <= 0x100. The handler overwrites +0x4 with 3 (EThreadUser, 0x80083a7c), +0x20 with -5 (0x80083a84 `mvn r0,#4`), +0x10 and +0x14 with 0 (0x80083a98/0x80083a9c) before the block becomes NThreadCreateInfo.iParameterBlock (0x80092a4c) and is copied, by EpocThreadFunction 0x8006a8dc..0x8006a8f8, to the top of the new thread's user stack with user SP = top - iTotalSize. The 0x40 size is the SStdEpocThreadCreateInfo euser builds: UserHeap::SetupThreadHeap 0x801b2d04/0x801b2d0c and CreateThreadHeap 0x801b0672/0x801b067a read +0x30/+0x34/+0x38 past the kernel head.

dprocess — 0x11c bytes

kernel process object

offsetfieldsizetypenote
0x20priority4i320x800928d8/0x800928e0 `ldr r2,[r0,#0x30] ; ldr r2,[r2,#0x20] ; add r1,r1,r2,lsl #3` — the process priority selects the 8-entry row of the thread-priority table
0x108codeseg4ptriCodeSeg: exec 0x9f 0x800899fc; 0x80091df8 `ldr r0,[r4,#0x108]`; EpocThreadFunction 0x8006a8b4 `ldr sl,[r7,#0x108]`
0x110process_lock4ptrDMutex* — DProcess::Create 0x800917C4 calls K::MutexCreate 0x80082024 with order 0x48 and the name TLitC at 0x8009E274 = '$LOCK', storing the result here
0x114dll_lock4ptrDMutex* — the same DProcess::Create path with order 0xFE (the KMutexOrdUser value Exec::MutexCreate also uses) and the name TLitC at 0x8009E280 = 'DLL$LOCK', created only for user processes; execs 0x9b/0x9c wait and signal it via 0x80087C18/0x80087C3C
0x118reentry_point4ptriReentryPoint: 0x80091dfc/0x80091e00 stores DCodeSeg+0x24 (the codeseg entry point, the same word exec 0x9f publishes) at load; fast exec 0x16 0x8006a6ac..0x8006a6b0 overwrites it; EpocThreadFunction 0x8006a90c `ldreq r7,[r7,#0x118]` + `moveq r4,#1` is where every non-initial user thread starts
evidence

RE'd extent only: iCodeSeg +0x108 (exec 0x9f 0x800899fc; 0x80091df8), iReentryPoint +0x118 (0x80091df8..0x80091e00 `ldr r0,[r4,#0x108] ; ldr r0,[r0,#0x24] ; str r0,[r4,#0x118]` stores the codeseg's entry point at process load; fast exec 0x16's handler 0x8006a6ac rewrites it from user code; EpocThreadFunction 0x8006a90c reads it as a non-initial thread's first user PC), iPriority +0x20 (0x800928d8/0x800928e0 in the thread-priority mapping). The owning process of a thread is DThread+0x30. Full size not measured.

tentry — 0x228 bytes

⚠ 0x1c + 0x204 = 0x220 = 544, but sizeof is 552: EIGHT BYTES AT 0x220..0x227 ARE UNACCOUNTED FOR and are deliberately NOT declared as fields. They are not tail padding — 544 is already 8-aligned and the struct's alignment is 8 (it holds a 64-bit iModified). No read or write of a TEntry at +0x220/+0x224 was found in efile.exe, EFSrv.dll, or across the whole-ROM sweep of RFs::Entry call sites (a later sweep of all 68,759,552 ROM bytes for a literal 0x220/0x224 or an ldr/str at rN+0x220 in 0x801E0000-0x80200000 also found none). CROSS-REFERENCE ONLY, never a committed value here: Symbian's published TEntry reference documents FileSize()/SetFileSize(TInt64) as storing the low word in iSize and the high word in a private iSizeHigh, and f32file.h ends the class with TUint32 iSizeHigh; TUint32 iReserved; - which fits +0x220/+0x224 exactly. Not declared as fields: that reference is Symbian^3, this ROM is 9.3, and nothing in this image reads either word. Also unresolved: the producer side. The efile handler delegates the fill to the mounted filesystem through vtable slot 0x40 (0x801CC8DC), and that plugin is not among samples/firmware/5320/sys/bin, so every field below is evidenced from CONSUMERS. Attribute BIT NAMES are deliberately absent: EFSrv exports five one-line accessors testing 0x1/0x2/0x4/0x10/0x20 and the stripped ROM names none of them.

offsetfieldsizetypenote
0x0iatt4u32attribute bitmask. EFSrv ords 268/270/271/269/272 (0x801ED6F5/0x801ED6FD/0x801ED705/0x801ED70D/0x801ED715) are five one-line accessors, each `ldr r0,[r0]` plus one bit test: 0x1, 0x2, 0x4, 0x10, 0x20. Only bit 0 has behavioural support elsewhere (efile 0x801CC996 -> -21); the ROM names none of the bits, so no names are recorded here
0x4isize4i32byte count, SIGNED: a ROM module at 0x83B2B01E builds a TEntry at sp+0x38, fills it via RFs::Entry, then reads sp+0x3c four times as a length and clamps it with `cmp r0,r5 ; bge` — a signed compare. Corroborated at 0x83DAD01C, which moves entry+4 alone into a separate slot
0x8imodified_lo4u32low word of iModified (TTime). Proven to be ONE 64-bit quantity, not two 32-bit fields: 0x82FDBEF8 loads +8/+0xc of two filled TEntry objects as register PAIRS into EUser 0x8019519C, which is `subs r0,r0,r2 ; sbcs r1,r1,r3 ; bx lr` — a 64-bit subtract used only to set flags for a following blt
0xcimodified_hi4u32high word of iModified; see tentry.imodified_lo. Split into two words because the codegen emits a 32-bit accessor per field — an 8-byte field would silently generate a wrong r32 read
0x10itype04u32TUidType[0]. The ctor 0x801ED69D does `adds r0,#0x10` then calls EUser 0x801A53B5, which zero-fills exactly 12 bytes, so iType is 3 UIDs at +0x10..+0x1b
0x14itype14u32TUidType[1]; see tentry.itype0
0x18itype24u32TUidType[2]; see tentry.itype0
0x1ciname_length4u32TDesC16 head of iName: length in bits 27..0, descriptor type in bits 31..28. EFSrv EntrySize 0x801F28E1 reads exactly this word and masks the top nibble off (`lsls #4 ; lsrs #3` = 2*length). The TBufC16 ctor 0x801AF70B stores 0 here at construction
0x20iname_first2u16first UTF-16 unit of iName; unit n is at 0x20 + 2n. Capacity is 256 units (0x20..0x21f), not assumed: EFSrv 0x801F140E passes 256 in r2 to TBufCBase16::Copy (EUser 0x8019DE44), whose body `ldr r3,[r1],#4 ; bic ip,r3,#0xf0000000 ; cmp ip,r2 ; bgt <panic> ; str ip,[r0],#4 ; add r2,ip,ip` uses r2 as the TBufC template bound and writes the characters at this+4
evidence

sizeof = 0x228, established three independent ways: EFSrv.dll's RFs::Entry wrapper 0x801ECCC7 builds the TPckg with `movs r3,#0x45 ; lsls r3,r3,#3` = 0x228; efile.exe's fn-22 handler 0x801D3530 reserves a 0x238 frame with the TEntry at sp+8 and a TPtrC16 immediately after it at sp+0x230 = sp+8+0x228; and a third ROM module at 0x80BA9A80 heap-allocates it with `movs r0,#0x45 ; lsls r0,r0,#3` before calling the ctor 0x801ED69D and RFs::Entry. The ctor fixes the layout: `adds r0,#0x10` (TUidType ctor, 12 bytes zeroed by EUser 0x801A53B5) then `adds r0,#0xc` (TBufC16 ctor 0x801AF70B, which stores a zero length word), so iType is at +0x10 and the name head at +0x1c. EFSrv's EntrySize 0x801F28E1 is `ldr r0,[r0,#0x1c] ; lsls r0,r0,#4 ; lsrs r0,r0,#3 ; adds r0,#0x20` = 0x20 + 2*length, independently fixing the head at 0x1c and the characters at 0x20; EFSrv 0x801F140E passes 256 as the TBufC capacity bound to TBufCBase16::Copy (EUser 0x8019DE44), so iName is TBufC16<256> = 4 + 512 bytes at 0x1c, ending at 0x220.

tldrinfo — 0x20 bytes

the block the loader load/create requests carry in slot 0. ⚠ +0x14 and +0x1C are NEVER written on the library path (only the process path writes +0x1C), so their meaning is undetermined and they are deliberately not declared as fields. Names here are labels: this stripped ROM carries no symbols.

offsetfieldsizetypenote
0x0uid14u32TUidType[0]. euser 0x801A7918 `ldm r6!,{r1,r2,r3}` then 0x801A791C `stm r0!,{r1,r2,r3}` with r0 = the block base copies the caller's TUidType into +0x00..+0x0B. efile's fn-2 handler 0x801D1480 reads THIS word (`ldr r0,[r0]`) and accepts only 0 or 0x10000079, then forces 0x10000079 at 0x801D14A0
0x4uid24u32TUidType[1]; see tldrinfo.uid1
0x8uid34u32TUidType[2]; see tldrinfo.uid1
0xcone_at_0c4u32set to literal 1 by the caller: euser 0x801A791E..0x801A7920 `movs r0,#1 ; str r0,[sp,#0x30]` with the block at sp+0x24. efile's exec-0x8e argument block takes this word as its 4th element (0x801D1548). Named for the value it carries, not for a meaning — no instruction establishes what it selects
0x10handle4u32THE OUTPUT. efile 0x801D1550 `ldr r0,[sp,#8] ; str r0,[r4,#0x10]` stores the handle slow exec 0x8e returned; the client reads it from its OWN stack copy after the send completes (euser 0x801A795E `ldr r0,[sp,#0x34] ; str r0,[r4]`, sp+0x34 = block+0x10). This is why the server's 0x20-byte write-back into slot 0 is load-bearing
0x18version4i32initialised to -2 by euser 0x801A5B64 (`movs r0,#1 ; mvns r0,r0 ; str r0,[r4,#0x18]`) and overwritten by the caller with its 5th argument (0x801A7924 `str r7,[sp,#0x3c]`; r7 = the arg the RLibrary::Load chain injects as -2). Only the value is measured
evidence

size 0x20: euser 0x801A5B64 builds it by zeroing exactly 0x20 bytes through 0x8019E65C (Mem::Fill with the byte forced to 0) and then `movs r0,#1 ; mvns r0,r0 ; str r0,[r4,#0x18]`. The caller at 0x801A78FE wraps it in a TPtr8 of length 0x20 (0x801A7914 `bl 0x801b8af8`) and sends that as TIpcArgs slot 0 of !Loader fn 2. efile reads it back with RMessagePtr2::Read(0, TDes8&, 0) into the first 0x20 bytes of its request (0x801D19E2) and writes it back with RMessagePtr2::Write(0, TPtrC8(&req,0x20), 0) at 0x801D13A4.

rarraybase — 0x20 bytes

RArrayBase as this ROM lays it out — NOT the order Symbian's published e32cmn.h documents, which is the reason it is measured here rather than taken from a header. ⚠ +0x18 and +0x1C are zeroed by all five constructors and read by NOTHING: a sweep of every Thumb `ldr`/`str` with those two offsets across EUser's whole code section [0x80199078,+0x32588) finds 245 hits, of which the 10 inside the array region 0x801AC800..0x801AD600 are all STORES, all in the constructors. They are deliberately not declared as fields.

offsetfieldsizetypenote
0x0count4i32iCount. Ordinal 1510 = 0x801ACE39 is the whole accessor, `ldr r0,[r0] ; bx lr`; Append 0x8019CC14 `ldm r0,{r3,ip}` takes it as word 0 and 0x8019CC24 `add r2,r3,#1 ; str r2,[r0]` bumps it; Remove 0x801ACEFA `ldr r0,[r4] ; subs r0,#1 ; str r0,[r4]` decrements it; the free path 0x801ACE2A zeroes it
0x4entries4ptrthe entry block. Append's `ldm r0,{r3,ip}` takes it as word 1 and `mla r0,r2,r3,ip` (0x8019CC30) addresses entry n at iEntries + iEntrySize*n; Remove 0x801ACEE2 does the same by hand; the free path 0x801ACE2C hands it to User::Free 0x801A46BE and 0x801ACE32 then zeroes it
0x8entry_size4i32iEntrySize, ROUNDED UP to a multiple of 4 by every constructor (0x801ACCB0 `adds r5,r5,#3 ; lsrs r0,r5,#2 ; lsls r0,r0,#2 ; str r0,[r4,#8]`). The requested size is bounds-checked against the literal 0x27F at 0x801ACD6C, so 1..640, else USER 129. Append 0x8019CC2C and Remove 0x801ACEE0 both read it as the stride
0xckey_offset4i32iKeyOffset. Only the 3-argument constructor 0x801ACD02 writes a non-zero value (`str r3,[r4,#0xc]` at 0x801ACD20); the other four zero it. 0x801ACD3E..0x801ACD4C is its whole contract: `cmp r6,#0 ; blt` non-negative, `lsls r0,r6,#0x1e ; bne` 4-aligned, `cmp r6,r5 ; blt` strictly inside the entry — otherwise USER 128
0x10allocated4i32iAllocated. Append 0x8019CC18 `ldr r2,[r0,#0x10] ; cmp r3,r2 ; beq` takes the grow path exactly when the count has reached it; the free path 0x801ACE34 zeroes it; ordinal 2169 0x801AD08E `ldr r0,[r4,#0x10] ; cmp r0,r5 ; blt` returns KErrNone at once when it already covers the request; the over-an-existing-buffer constructor 0x801ACE04 sets it equal to the count
0x14granularity4i32iGranularity. 8 unless given: 0x801ACCA0 `movs r1,#8 ; str r1,[r4,#0x14]` in the 1-argument constructor, from r2 in the 2- and 3-argument ones (0x801ACCD8, 0x801ACD1C), which then panic USER 127 unless it is positive AND aEntrySize*aGranularity <= 0x10000000 (`movs r0,#1 ; lsls r0,r0,#0x1c ; cmp r6,r0 ; ble`)
evidence

RE'd extent: every constructor writes through +0x1C, so the object is at least 0x20 bytes; the full size is not measured. Six of the eight words are fixed by four independent bodies rather than one. (a) The 1-argument constructor, EUser ordinal 55 = 0x801ACC8B, is the whole shape in one function: it zeroes +0x00, +0x04, +0x0C, +0x10, +0x18 and +0x1C, stores 8 at +0x14, checks aEntrySize against the literal 0x27F at 0x801ACD6C (`subs r0,r5,#1 ; cmp r0,r1 ; bls`, else USER 129 through the panic helper 0x801AB7C4) and stores it at +0x08 ROUNDED UP TO 4 (`adds r5,#3 ; lsrs r0,r5,#2 ; lsls r0,r0,#2`). (b) Append, ordinal 48 = 0x8019CC10 and ARM, addresses four of them in six instructions: `ldm r0,{r3,ip}` = iCount and iEntries, `ldr r2,[r0,#0x10]` = iAllocated, `ldr r2,[r0,#8]` = iEntrySize and `mla r0,r2,r3,ip` = the slot to copy into. (c) Remove, ordinal 50 = 0x801ACECD, recomputes the same address, shifts the tail with 0x8019E7BC and decrements +0x00. (d) The free path, ordinals 46 and 47 (two byte-identical bodies at 0x801ACE24 and 0x801ACF28), zeroes +0x00, hands +0x04 to User::Free 0x801A46BE and then zeroes +0x04 and +0x10. Ordinal 1510 = 0x801ACE39 is `ldr r0,[r0] ; bx lr`, i.e. Count() is word 0 outright.

cactive — 0x18 bytes

CActive: the active object. vtable slot 3 (+0xC) = DoCancel, slot 4 (+0x10) = RunL, slot 5 (+0x14) = RunError — fixed by Cancel's `ldr r1,[r0,#0xc] ; blx r1` and RunIfReady's `ldr r1,[r0,#0x10] ; blx r1` / `ldr r2,[r0,#0x14] ; blx r2`.

offsetfieldsizetypenote
0x0vptr4ptrCActive vtable. Slot 3 (+0xC) is DoCancel — Cancel 0x8019F0E2 `ldr r0,[r4] ; ldr r1,[r0,#0xc] ; blx r1`; slot 4 (+0x10) is RunL — the ARM run loop 0x8019BA54 `ldr r3,[r3,#0x10] ; bx r3` and RunIfReady 0x8019F59E; slot 5 (+0x14) is RunError — RunIfReady's leave path 0x8019F5CC `ldr r2,[r0,#0x14] ; blx r2`, whose default implementation (ordinal 1089) is `movs r0,r1 ; bx lr`
0x4status4i32iStatus, a TRequestStatus and one word. The ARM run loop 0x8019BA34 `ldmdane lr,{r2,ip,lr}` loads it as [&iLink-8] and 0x8019BA3C `cmp r2,#0x80000001` compares it against KRequestPending inline; RunIfReady 0x8019F584 does the same against the literal at 0x8019F744 = 0x80000001; Cancel 0x8019F0EA `adds r0,r4,#4 ; bl 0x801A4594` passes its address to User::WaitForRequest. The constructor deliberately leaves it uninitialised
0x8flags4u32the active-object flags word, NOT a plain TBool. Bit 0 is the "request issued" flag: SetActive 0x8019F1B6 `ldr r0,[r4,#8] ; movs r1,#1 ; orrs r0,r1 ; str r0,[r4,#8]` ORs it in, and SetActive/Cancel/the destructor/RunIfReady/the ARM loop all test it with `lsls #0x1f` or `ands #1`. ⚠ Bit 1 exists and its setter was NOT found: three independent sites clear bits 0 AND 1 together — Cancel 0x8019F0F2 `lsrs r0,r0,#2 ; lsls r0,r0,#2`, RunIfReady 0x8019F58C the same, and the ARM loop 0x8019BA4C `bic ip,ip,#3`
0xclink4ptrfirst word of the embedded TPriQueLink (struct tpriquelink), so iLink.iNext. IsAdded (0x801B7C58) is `ldr r1,[r0,#0xc]` plus a != 0 test and is the precondition of Add, SetActive and Deque; Deque 0x8019F114 `adds r0,#0xc ; bl 0x801A96D8` unlinks it and then stores 0 here; the ARM run loop 0x8019BA30 `sub r0,lr,#0xc` recovers the CActive* from the link
evidence

RE'd extent, and it is closed by construction: the constructor (ordinal 1091 = 0x8019F189) writes +0, +8, +0xC and +0x14, and +0xC is the start of an embedded TPriQueLink whose third word is that +0x14, so the object ends at 0x18. The decisive single instruction is in the scheduler's own ARM run loop: 0x8019BA34 `ldmdane lr,{r2,ip,lr}` with lr = &iLink loads [lr-8], [lr-4] and [lr] into r2, ip and lr, and the next three instructions use r2 as the request status (`cmp r2,#0x80000001`), ip as the flags (`ands r3,ip,#1`, then `bic ip,ip,#3 ; str ip,[r0,#8]`) and lr as the next link — so +4, +8 and +0xC are fixed in one go, with 0x8019BA30 `sub r0,lr,#0xc` giving the link offset a second time. Corroborated from the C++ side by SetActive (0x8019F197), Cancel (0x8019F0D9), Deque (0x8019F0FB), the destructor (0x8019F09B) and IsAdded (0x801B7C58 = `ldr r1,[r0,#0xc]`).

cactivescheduler — 0x14 bytes

CActiveScheduler. Install/Current are nothing but fast execs 6 and 5 (ordinal 428 = 0x8019F3E9 `blx 0x8019BD88`, ordinal 427 -> 0x801B747E `blx 0x8019BD80`), so the installed scheduler is a per-thread kernel slot, not a euser global.

offsetfieldsizetypenote
0x0vptr4ptrCActiveScheduler vtable, installed by the constructor 0x8019F390 and re-installed by the destructor 0x8019F3AA
0x4loop_stack4ptrhead of the TLoop list (struct tschedulerloop), a stack of nested Start() calls. Pushed by the loop driver 0x8019F4C2 `ldr r0,[r4,#4] ; str r0,[sp,#8] ; str r7,[r4,#4]`, popped by its epilogue 0x8019F508, walked by Stop 0x8019F536. Zero when no loop is running
0x8queue4ptrfirst word of the embedded TDblQueBase (struct tdblquebase) that holds the added CActives — so iHead.iNext. The constructor 0x8019F392 `adds r0,#8 ; movs r1,#0xc ; bl 0x801A97CA` builds it with link offset 0xC; Add 0x8019F14E `adds r0,#8` inserts through the priority insert 0x801A97FA; the destructor 0x8019F3AE `ldr r0,[r4,#8] ; ldr r1,[r4,#0x10] ; subs r0,r0,r1` recovers each object by subtracting the stored offset
evidence

RE'd extent: the constructor (ordinal 430 = 0x8019F38B) writes the vtable at +0 and then builds a TDblQueBase at +8 with link offset 0xC through the generic ctor 0x801A97CA, which writes +0, +4 and +8 of ITS object — so the scheduler ends at 0x14. The destructor 0x8019F3A1 reads exactly the same two words back, `ldr r0,[r4,#8]` (head->iNext) minus `ldr r1,[r4,#0x10]` (iOffset), to recover each CActive* and Deque it, then uninstalls itself with fast exec 6 when it is the current one. +4 is fixed by the loop driver 0x8019F49C (`ldr r0,[r4,#4] ; str r0,[sp,#8] ; str r7,[r4,#4]` pushes a TLoop node, and the epilogue 0x8019F508 pops it) and by Stop (ordinal 425 = 0x8019F527), which walks that list.

tdblquebase — 0xc bytes

TDblQueBase {TDblQueLink iHead; TInt iOffset}. iOffset is how far the link sits inside the queued object — 0xC for CActive.

offsetfieldsizetypenote
0x0next4ptriHead.iNext, self-linked at construction (0x801A97E2 `str r4,[r4]`). IsEmpty 0x801A9796 reads it and compares against the head address; the CActiveScheduler destructor loop 0x8019F3AE reads it as the first queued link
0x4prev4ptriHead.iPrev, self-linked at construction (0x801A97DE `str r4,[r4,#4]`)
0x8offset4i32iOffset — how far the TDblQueLink sits inside the queued object. 0x801A97D2 `lsls r0,r1,#0x1e ; bne` panics USER 78 unless it is 4-aligned; the priority insert 0x801A97FC `ldr r2,[r0,#8] ; adds r2,r1,r2` uses it to find a link from an object and the CActiveScheduler destructor 0x8019F3B0 to go the other way
evidence

The generic doubly-linked-queue head, constructed by EUser 0x801A97CA: `movs r0,#0 ; str r0,[r4]`, then `lsls r0,r1,#0x1e ; str r1,[r4,#8] ; beq` stores the link offset at +8 and panics USER 78 when it is not 4-aligned, then `str r4,[r4,#4] ; str r4,[r4]` self-links both words. IsEmpty 0x801A9796 and the destructor loop 0x8019F3B8 read +0 back and compare it against the head address.

TPriQueLink {TDblQueLink iNext, iPrev; TInt iPriority}. Higher priority runs first: the walk stops at the first queued link whose priority is BELOW the new one.

offsetfieldsizetypenote
0x0next4ptriNext. The priority walk 0x801A9806 `ldr r3,[r3]` follows it and 0x801A9808 `cmp r3,r0` stops at the head; Deque 0x801A96D8 unlinks through it
0x4prev4ptriPrev. The insert takes it at 0x801A9812 `ldr r1,[r3,#4]` for the splice at 0x801A96BC
0x8priority4i32iPriority. The insert reads the new link's at 0x801A9802 `ldr r1,[r2,#8]` and each queued link's at 0x801A980C `ldr r4,[r3,#8]`, walking on while `cmp r4,r1 ; bge` — so a new object goes AFTER every link of equal or higher priority, i.e. FIFO within a priority. The CActive constructor 0x8019F18E `str r1,[r0,#0x14]` writes aPriority into this word (CActive+0xC+8)
evidence

The link a CActive embeds at +0xC, fixed by the priority insert EUser 0x801A97FA: `ldr r2,[r0,#8]` takes the queue's iOffset, `adds r2,r1,r2` makes &aLink, `ldr r1,[r2,#8]` reads the NEW link's priority, and the walk `ldr r4,[r3,#8] ; cmp r4,r1 ; bge ; ldr r3,[r3]` compares each queued link's +8 and follows +0 — so +0 is iNext and +8 is iPriority. `ldr r1,[r3,#4]` then takes iPrev for the splice at 0x801A96BC. The CActive constructor 0x8019F189 `str r1,[r0,#0x14]` writes aPriority into exactly this +8 (0xC+8).

tschedulerloop — 0x8 bytes

the TLoop node. Two words, on the stack of whoever called Start — there is no allocation and no fixed count of nesting levels.

offsetfieldsizetypenote
0x0next4ptrthe previously running loop, saved by 0x8019F4C2 `ldr r0,[r4,#4] ; str r0,[sp,#8]` and restored by 0x8019F508. Stop walks the chain with `ldr r0,[r0]` (0x8019F546)
0x4running4i32the loop token, and the flag the run loop re-reads every iteration (0x8019BA10 `ldr r1,[r5]` with `cmp r1,#0 ; bxne r6` at 0x8019BA20, where r5 = this+4). A plain Start() stores 1 here (ordinal 426 `movs r0,#1`), which is what Stop 0x8019F53C `ldr r1,[r0,#4] ; cmp r1,#1` searches for before storing 0; running off the end of the chain panics E32USER-CBase 45
evidence

The stack-allocated node CActiveScheduler::Start pushes onto the scheduler's +4. Built by the loop driver 0x8019F49C at sp+8: `ldr r0,[r4,#4] ; str r0,[sp,#8]` puts the previous head in word 0 and `str r5,[sp,#0xc]` the token in word 1 (r5 = 1 for a plain Start, ordinal 426 = `movs r0,#1 ; b 0x8019F49C`), then `str r7,[r4,#4]` makes it the head; the epilogue 0x8019F508 `ldr r0,[sp,#8] ; str r0,[r4,#4]` pops it. Stop (ordinal 425) walks the same list by `ldr r0,[r0]` looking for word 1 == 1 and stores 0 there; the ARM run loop's caller re-reads word 1 every iteration (0x8019BA10 `ldr r1,[r5]` with `cmp r1,#0 ; bxne`), which is what makes Stop end the loop.

libcstat — 0x60 bytes

struct stat as this libc writes it. ⚠ The FIELD NAMES here say what the binary puts there, not what a POSIX header calls it: this ROM is stripped and backend.dll's producer is the only witness. As a cross-reference only (never a committed value), the offsets line up with the openc/newlib struct stat where +0 is st_dev, +8 st_mode, +0xA st_nlink, +0x14 st_rdev, +0x18 st_atime, +0x20 st_mtime, +0x30 st_size (64-bit) and +0x40 st_blksize; the gaps this producer never touches would then be st_ino, st_uid, st_gid, the spare words and st_ctime at +0x28.

offsetfieldsizetypenote
0x0drive_004u32written together with drive_14 from the same value: backend 0x82E478DA `add r0,sp,#4 ; add r1,sp,#0x238 ; blx 0x82E52858 ; ldr r0,[r0] ; str r0,[r4,#0x14] ; str r0,[r4]`. The veneer resolves to EFSrv 0x801ED603, which is `blx 0x801F25C0 ; ldrh r0,[r0]` — the FIRST UTF-16 character of the path — then `bl 0x801ECE16` (the character-to-drive-number conversion) with the out slot in r1, panicking through 0x801F0A00 when that fails. So both words carry the path's drive number
0x8mode2u16a 16-bit mode word, built by backend 0x82E4B090 from the TEntry's iAtt. Type bits come in through r3 (0x8000 from the worker's default `movs r7,#1 ; lsls r7,r7,#0xf`, 0xA000 from `movs r7,#5 ; lsls r7,r7,#0xd`, 0x1000 from `movs r7,#1 ; lsls r7,r7,#0xc`, and 0x4000 via `asrs r1,r3,#1` when iAtt bit 4 = KEntryAttDir is set); then 0x82E4B0BA `ldr r1,[r2] ; lsls r1,r1,#0x1f` tests iAtt bit 0 = KEntryAttReadOnly and ORs 0x100 for read-only or 0x180 otherwise. Nothing else is ever ORed in, so group and other permission bits stay zero
0xanlink2u16link count, and it takes exactly two values: 0x82E4B0D0 `ldrh r2,[r4,#8] ; movs r1,#2 ; cmp r2,r7 ; beq ; movs r1,#1 ; strh r1,[r4,#0xa]` stores 2 when the mode word equals 0xA000 exactly and 1 in every other case
0x14drive_144u32the same drive number as drive_00, stored by the same instruction pair 0x82E478E4/0x82E478E6
0x18seconds_184i32one of the two timestamp words; 0x82E4B0F8 `str r0,[r4,#0x18]` and 0x82E4B0F6 `str r0,[r4,#0x20]` write the SAME value to both. It is produced by 0x82E4B0E6 `blx 0x82E522E8` = EUser 0x801AA9E5 called with the TEntry's iModified, the 64-bit constant {lo 0x0F2F8000, hi 0x00DCDDB3} = 62,168,256,000,000,000 (the 1970 epoch in TTime units) and an out slot; that routine is `(this - arg) / literal 0x801AAC30 = 1,000,000`, bounded to [KMinTInt, KMaxTInt] (literals 0x7FFFFFFF at 0x801AAC48 and 0x80000000 built at 0x801AAA38) with KErrOverflow -9 outside. So the value is SECONDS since 1970-01-01, and -1 (0x82E4B0EE `movs r0,#0 ; mvns r0,r0`) when the conversion fails
0x20seconds_204i32the second timestamp word, byte-for-byte the same value as seconds_18 — see that field. Two words, one measurement: this producer never distinguishes them
0x30size_lo4i32low word of a 64-bit size, SIGN-EXTENDED from the TEntry's 32-bit iSize: backend 0x82E478D2 `ldr r0,[sp,#0xc] ; asrs r1,r0,#0x1f ; str r1,[r4,#0x34] ; str r0,[r4,#0x30]`, where sp+8 is the TEntry (so sp+0xc is tentry.isize, which struct tentry already records as signed)
0x34size_hi4i32high word of the same 64-bit size; it is `asrs r0,#0x1f` of the low word, i.e. 0 or -1, never an independent quantity on this path. Split into two fields because the codegen emits one 32-bit accessor per field
0x40blksize4u32the constant 512, written unconditionally by 0x82E4B0FA `movs r0,#1 ; lsls r0,r0,#9 ; str r0,[r4,#0x40]` — the last thing the filler does. It is not derived from the volume
evidence

The struct the C library's stat() fills, established end to end from the ROM: libc ordinal 344 = 0x82E70371 converts the path and calls the veneer 0x82E827A4 (`ldr pc,[pc,#-4]` with the target word 0x82E501DF), which is backend.dll 0x82E501DE -> 0x82E46871 -> the worker 0x82E47851. The worker constructs a TEntry at sp+8 (0x82E478_90 `blx 0x82E527F8`, a veneer to 0x801ED69D = the same EFSrv TEntry constructor struct tentry already rests on), then 0x82E478CA `movs r1,#0x60 ; movs r0,r4 ; blx 0x82E52EA8` ZEROES EXACTLY 0x60 BYTES of the caller's buffer — that is the size measurement. Only six places are written afterwards, and every one is quoted in the field rows; everything else stays zero, which is what makes a JS implementation checkable against the real one.

rpointerarraybase — 0x18 bytes

RPointerArrayBase. Its granularity sits at +0xC, exactly where rarraybase keeps iKeyOffset — so one shared set of accessors over both classes would silently write the wrong field. ⚠ +0x10 and +0x14 are zeroed by the constructor and read by nothing, the same gap rarraybase has at +0x18/+0x1C, and they are deliberately not declared as fields. Independently reached by the HLE track from the same address (db/hle/import-impl.tsv euser 481), which is the only reason to state offsets that a second reader also has: they agreed.

offsetfieldsizetypenote
0x0count4i32iCount. Ordinal 1593 = 0x801ACA07 is the whole accessor, `ldr r0,[r0] ; bx lr`; Remove 0x801ACA90 bounds-checks against it and 0x801ACAB8 decrements it; the free path 0x801ACAE8 zeroes it
0x4entries4ptrthe pointer block. Remove 0x801ACAA6 `ldr r0,[r4,#4]` addresses entry n at iEntries + 4*n (`lsls r1,r5,#2`, a fixed four-byte stride); the free path 0x801ACAEA hands it to User::Free 0x801A46BE and 0x801ACAF0 then zeroes it
0x8allocated4i32iAllocated, and it is at +8 here rather than the +0x10 rarraybase uses: the free path 0x801ACAF2 `str r5,[r4,#8]` clears exactly this word alongside the count and the pointer, and the constructor 0x801AC980 zeroes it
0xcgranularity4i32iGranularity, 8 from the constructor (0x801AC984 `movs r2,#8` stored at 0x801AC988 `str r2,[r0,#0xc]`) — the only non-zero word it writes. It sits where rarraybase keeps iKeyOffset, which is why the two classes cannot share accessors
evidence

RE'd extent: the constructor writes through +0x14, so at least 0x18 bytes; the full size is not measured. A SEPARATE CLASS from rarraybase with a different layout, and the difference is not cosmetic — the two share iCount at +0 and iEntries at +4 and then diverge. Three bodies fix it. (a) The constructor, EUser ordinal 481 = 0x801AC97B, is nine instructions with no push and never writes r0, so it returns this unchanged: `movs r1,#0 ; str r1,[r0] ; str r1,[r0,#4] ; str r1,[r0,#8] ; str r1,[r0,#0x10] ; movs r2,#8 ; str r1,[r0,#0x14] ; str r2,[r0,#0xc] ; bx lr`. It takes no argument at all. (b) The free path, ordinal 471 = 0x801ACAE3, zeroes +0, frees +4 through User::Free 0x801A46BE and then zeroes +4 and +8 — +8, NOT the +0x10 that rarraybase's equivalent clears, which is the instruction that proves iAllocated moved. (c) Remove, ordinal 474 = 0x801ACA89, bounds-checks with the same USER 130 and then shifts the tail with `lsls r1,r5,#2` and `lsls r2,r2,#2` — a hardcoded stride of four rather than a stored entry size, which is the second proof that this class has no iEntrySize. Count, ordinal 1593 = 0x801ACA07, is `ldr r0,[r0] ; bx lr` like its rarraybase twin.

The servers and their ops

Projected from db/os/hle-server.tsv and fs-op.tsv. A server is implemented at the session boundary: the game's messages to it are completed by host code instead of by a guest server process. The name is read out of the ROM at the literal's address, the session type is the one efile.exe passed to CServer2, and an op with no implementation is named — its client wrapper found — but HALTs.

!FileServer

name literal 0x801e7ec0 session type 2 implementation fileServer in web/src/fs/fileserver.js

the F32 file server; ops in fs-op.tsv

evidence

TLitC16 at 0x801e7ec0 inside efile.exe (TRomHeader.iSecondaryFile -> TRomEntry 'efile.exe', TRomImageHeader 0x801CB600, size 0x205D4): length 11, '!FileServer'; the UTF-16 text occurs ROM-wide only there and in EFSrv.dll at 0x801f2fbc, the client library whose RFs::Connect 0x801ec4ae hands it to RSessionBase::CreateSession. efile.exe creates the server: 0x801d2a6e allocates 0x70 bytes and calls 0x801d2a5a with r1 = 0x3e8 (priority), which does `movs r2,#2 ; blx 0x801e4bb4` = the XIP import stub (`ldr pc,[pc,#-4]` + literal 0x801a29cd) of euser CServer2::CServer2(TInt aPriority, TServerType aType) ord 1291, whose body stores aType at CServer2+0x1c (0x801a29cd `movs r5,r2 ... str r5,[r4,#0x1c]`); 0x801d2aaa..0x801d2ab0 `ldr r1,[pc,#0x10c] ; ldr r0,[r4] ; subs r1,#0x38 ; blx 0x801e4b8c` calls CServer2::StartL (stub literal 0x801a29a1, ord 1285) with r1 = [0x801d2bb8] - 0x38 = 0x801e7ef8 - 0x38 = 0x801e7ec0, this TLitC; StartL 0x801a29a1 does `ldr r2,[r0,#0x1c] ; adds r0,#0x20 ; bl 0x801a6e96` = RServer2::CreateGlobal(aName, aMode = iSessionType) ord 1335, which issues svc #0x7d = Exec::ServerCreate (ekern 0x8008a80c, wrapper 0x8019c208) at 0x801a6ebc. So !FileServer's TIpcSessionType is 2 (EIpcSession_GlobalSharable), which is what RSessionBase::ShareAuto (Exec::SessionShare mode 2) needs.

functionnameflagsargumentsimplementationnoteevidence
-1Connect0x0slot0 = TVersion packed word (iMajor byte 0, iMinor byte 1, iBuild halfword 1)connectthe HLE answers KErrNone iff QueryVersionSupported holds and stores the session path; whether AppendNumFixedWidth here is the upper-case variant is not resolved (0x801aeec5 not compared with the UC export) — moot for SID 0x20010925, which has no letters
evidence

Client: EFSrv.dll RFs::Connect(TInt aMessageSlots) 0x801ec4ae: `bl 0x801ec49c` = RFs::Version() = TVersion(2, 0, [0x801ec864] = 0x625) built by the TVersion ctor stub 0x801f24f0 with r1 = 2 (0x801ec4a2), r2 = 0 (0x801ec49e), r3 = the pool word 0x801ec864; then `ldr r1,[pc,#0x3a8]` = [0x801ec868] = 0x801f2fbc (EFSrv's '!FileServer' TLitC) and `blx 0x801f2680` = RSessionBase::CreateSession(const TDesC&, const TVersion&, TInt) with r2 = &version, r3 = aMessageSlots; euser's CreateSession (ord 1992, 0x801a6cb1) issues Exec::SessionCreate 0x7e at 0x801a6cda and then the KConnectMessage (fn -1) whose TIpcArgs slot 0 is the version word — the boot's queued connect message reads args[0] = 0x06250002 with flags 0. Server: efile.exe CServerFs::NewSessionL 0x801d2e4a builds TVersion(2, 0, [0x801d2f88] = 0x625) at 0x801d2e50..0x801d2e5a and calls User::QueryVersionSupported (stub 0x801e538c -> euser 0x801a5b27: supported iff requested.iMajor < current.iMajor, or equal iMajor and requested.iMinor <= current.iMinor; iBuild is never read) with the client's version, leaving KErrNotSupported (0x801d2e6a `subs r0,r0,#5` ; 0x801d2e6c User::Leave) otherwise; then CSessionFs::NewL (0x801d2e70 `bl 0x801d4610`) and the new session's path: TLitC 'C:\Private\' (0x801e7edc, reached as [0x801d2f7c] + 0x1c at 0x801d2e7c/0x801d2e80) into a TBuf<30> (0x801d2e82 `movs r2,#0x1e`, stub 0x801e54b4) + AppendNumFixedWidth(aMessage.SecureId() from stub 0x801e4e24, radix 0x10 = EHex, width 8) (0x801d2e8a..0x801d2e92, stub 0x801e5574 -> euser 0x801aeec5) + TLitC '\' (0x801e7668 = [0x801d2f80] - 8 at 0x801d2e96/0x801d2e9a), allocated (stub 0x801e4f84) and stored at CSessionFs+0x30 (0x801d2ea6 `str r0,[r5,#0x30]`). Completion: euser CServer2::DoConnect 0x801a2a6e completes the connect message with the TRAP result r4 — 0 when NewSessionL did not leave — at 0x801a2abc..0x801a2ac0 `movs r1,r4 ; movs r0,r6 ; bl 0x801a63b6` = RMessagePtr2::Complete -> svc #0x42 = Exec::MessageComplete ekern 0x8006ab9c.

5DriveList0x4slot0 = TDes8& aList (4), slot1 = TUint aFlags = [0x801ec878] = 0x40040named onlynamed only
evidence

Client: EFSrv.dll 0x801ec808 `str r1,[sp] ; ldr r1,[pc,#0x68] ; str r1,[sp,#4] ; movs r1,#4 ; str r1,[sp,#0x10] ; movs r1,#5 ; mov r2,sp ; bl 0x801f29a4`

14SessionPath0x5slot0 = TDes16& aPath (type 5 = writable 16-bit descriptor)session_path-
evidence

Client: EFSrv.dll 0x801ec91e `push {lr} ; sub sp,#0x14 ; str r1,[sp] ; movs r1,#5 ; str r1,[sp,#0x10] ; movs r1,#0xe ; mov r2,sp ; bl 0x801f29a4`. Server: writes the session's path (CSessionFs+0x30, set at connect or by fn 85/15) into slot 0 and completes KErrNone (EKA2L1 fs.cpp:541 session_path, cross-reference); the write goes through the kernel descriptor setter K::USetLength 0x8006b9a4 (web/src/kernel/descriptor.js writeDes16)

15SetSessionPath0x7slot0 = const TDesC16& aPath (7)named onlynamed only
evidence

Client: EFSrv.dll 0x801ec932 `str r1,[sp] ; movs r1,#7 ; str r1,[sp,#0x10] ; movs r1,#0xf ; mov r2,sp ; bl 0x801f29a4`

22Entry0x27slot0 = const TDesC16& aName (7), slot1 = TDes8& = TPckg<TEntry> over anEntry (4), sizeof(TEntry) = 0x228entrynamed only, and the halt is now a SHORT one. The not-found answer is settled (KErrNotFound, read above) and the whole write-back is read; what stops it is that THIS boot asks about a file that IS there — c:\private\20010925\Arena\config.xml, 3376 bytes, installed to C: by the container's own <Game> SIS alongside as.dat and nine game.r* files. Filling its TEntry needs two values this VFS does not hold: iModified (ELocal takes the FAT timestamp; a SIS install carries none, and Exec::TimeNow would be the wrong clock anyway) and iAtt (ELocal takes the FAT attribute byte). iType needs nothing — the ctor 0x801ed69d zeroes the TUidType and ELocal never writes it (0x801f7b72..0x801f7b80 touch only iAtt, iSize, iModified, iName) — and iSize is the VFS length. So what is left is a POLICY question, not a reverse-engineering one: what does this emulator report for a file it synthesised? Declare it with a reason and the op is a few lines. POLICY, declared because it is ours and not the device's: this VFS synthesises its files from the container, so two of the four fields ELocal fills have no source here. iModified is left ZERO — a SIS install carries no per-file timestamp, and answering Exec::TimeNow would be a different clock AND would make the run non-deterministic, which is what the gate rests on. iAtt reports only bit 0 (set when the VFS entry is read-only, i.e. the ROM mount): it is the one bit with behavioural support anywhere in this ROM (efile 0x801CC996 -> -21), and the container records no other attribute. iType stays zero, evidenced: ELocal never writes it. iSize is the VFS length and iName the leaf, written as the type-0 TBufC16 head the layout declares.
evidence

Client: EFSrv.dll RFs::Entry 0x801eccc6 (the fn-0x15 wrapper tail sits at 0x801ecca6; the body `push {r4,r5,lr} ; movs r4,r1 ; movs r3,#0x45 …` starts 0x20 later) `movs r4,r1 ; movs r3,#0x45 ; movs r5,r0 ; lsls r3,r3,#3 (0x228) ; movs r1,r2 ; sub sp,#0x24 ; movs r2,r3 ; add r0,sp,#4 ; blx 0x801f2800 (TPckg ctor over anEntry) ; add r0,sp,#4 ; str r0,[sp,#0x14] (slot 1) ; movs r0,#0x27 ; str r0,[sp,#0x20] ; movs r1,#0x16 ; movs r0,r5 ; add r2,sp,#0x10 ; str r4,[sp,#0x10] (slot 0 = aName) ; bl 0x801f29a4` Server: efile.exe 0x801d3530 reserves a 0x238 frame, constructs the TEntry at sp+8 (0x801d353a `blx 0x801e4804`), builds a TPtrC16 at sp+0x230 out of the request's parsed name (0x801d353e..0x801d3550, vtable slots 0x1c and 0x24) and calls the mount dispatcher 0x801cc8e4 with (drive, name, &entry). A NON-ZERO result is propagated verbatim (0x801d3564 `cmp r0,#0 ; bne 0x801d3580`); on zero the entry is wrapped in a TPtrC8 of 0x228 bytes (0x801d3568 `movs r2,#0x45 ; lsls r2,r2,#3`), written into slot 1 (0x801d3574 `movs r1,#1 ; bl 0x801e5770`) and the message completes KErrNone (0x801d357e). The dispatcher runs the mount pre-check 0x801cc650 -> 0x801cc5b4 first, whose opening `ldr r0,[r0,#0x14] ; cmp r0,#0 ; bne` gives KErrNotReady -18 (0x801cc5be) for a drive with no mount and KErrBadName -28 (0x801cc666) for a malformed name; otherwise it TRAPs the mount's own EntryL (0x801cc8ca `ldr r0,[r4,#0x18] ; ldr r1,[r0] ; ldr r3,[r1,#0x40] ; blx r3` = CMountCB vtable slot 0x40) and PASSES THE LEAVE REASON STRAIGHT THROUGH: 0x801cc92a `blx 0x801e50ac` is euser 0x801ab754 (`blx 0x8019c510 ; ldr r0,[r4,#4]` = the trapped reason) and only two values are rewritten afterwards, +1 -> -1 and +2 -> -12 (0x801cc940..0x801cc94e), neither of which a KErr code can be. THE MOUNT IS ELocal.fsy AND IT IS IN THE ROM: TRomEntry 0x80046328 names it and points at the TRomImageHeader 0x801F3BD0 (uid1 0x10000079 DLL, uid2 0x100039DF file system, code 0x801F3C48 size 0xCC68, one export at 0x801FE2E8 -> 0x801F5E01). Ordinal 1 -> 0x801f9bb6 allocates 0x1c and constructs the CFileSystem at 0x801f5dcc with vtable 0x801FFC30; its slot +0x28 (0x801f5d2e -> 0x801f9b96) allocates 0xd0, constructs at 0x801f8dc6 with vtable 0x801FF9DC and runs ConstructL 0x801f9b44 — so NewMountL is that slot and CMountCB::EntryL is mount-vtable +0x40 = 0x801F7B21. EntryL splits the path with LocateReverse(0x5c) (0x801f7ad0 0x801f7adc), resolves the directory through 0x801f70f8 and the leaf through 0x801f7aa6 -> 0x801f6cec, then fills the TEntry: iAtt from the byte at sp+0x5f (0x801f7b72 `ldrb r0,[r3,#0x1f] ; str r0,[r4]`), iSize from sp+0x70 (0x801f7b78 `str r0,[r4,#4]`), iModified into anEntry+8 (0x801f7b80 `bl 0x801fed56`), and it takes anEntry.iName at +0x1c as a TPtr of max 0x100 (0x801f7b3c..0x801f7b44) — independent corroboration of struct tentry. THE NOT-FOUND CODE IS KErrNotFound -1, read rather than chosen: 0x801f7b06 `movs r2,#0 ; mvns r2,r2 ; str r2,[sp,#8]` hands -1 down as the reason, 0x801f7aa6 forwards it (0x801f7ab0 `ldm` of the caller's three stacked args, 0x801f7aba `str r2,[sp,#0x10]`) and 0x801f6cec leaves with it (0x801f6d0e/0x801f6e9c/0x801f6eca `ldr r0,[sp,#0x118] ; blx 0x801fe794`, the stub whose target word 0x801FE798 is euser User::Leave 0x801AB767 — the same VA efile's own stub 0x801E53AC carries). And KErrPathNotFound -12 is NOT in this filesystem at all: a sweep of the whole 0xCC68-byte code section finds zero `movs r0,#0xb ; mvns r0,r0`, zero `movs r0,#0xc ; negs r0,r0` and zero 0xFFFFFFF4 literals, while KErrNotFound appears nine times. Consumer: this call is libc stat() — backend.dll 0x82e47850 builds the TEntry with the same EFSrv ctor 0x801ed69d, sends at 0x82e478be, and on a non-zero result returns -1 with an errno chosen at 0x82e4794a: -12 gets its own leg (0x82e47956/0x82e47958), -18 becomes errno 2 (0x82e4796e) and everything else, KErrNotFound included, goes through the general mapper 0x82e4f130. libc ordinal 344 (0x82e70370) is the entry point.

85SetSessionToPrivate0x0slot0 = TInt aDrive (EDriveA = 0 .. EDriveZ = 25)set_session_to_privateanswered KErrNone for a drive in A..Z with the path set to '<drive>:\Private\<sid>\'
evidence

Client: EFSrv.dll 0x801ed134 `push {lr} ; sub sp,#0x14 ; str r1,[sp] ; movs r1,#0 ; str r1,[sp,#0x10] ; movs r1,#0x55 ; mov r2,sp ; bl 0x801f29a4` (0x801f29a4 = `blx 0x801f2520`, the stub of euser RSessionBase::SendReceive(TInt, const TIpcArgs&) const, which issues Exec::SessionSendSync 0x4c). Server: the path is the drive-qualified private directory — efile carries the drive-less TLitC '\Private\' at 0x801e7f7c for it (EKA2L1 fs.cpp:558 set_session_to_private -> fs.cpp:116 get_private_path = drive letter + ':' + '\Private\' + SID hex + '\', cross-reference); efile's TFsSessionToPrivate handler itself is not disassembled, so a drive outside A..Z HALTs instead of returning its error code

86PrivatePath0x5slot0 = TDes16& aPath (5)private_path-
evidence

Client: EFSrv.dll 0x801ed148 `str r1,[sp] ; movs r1,#5 ; str r1,[sp,#0x10] ; movs r1,#0x56 ; mov r2,sp ; bl 0x801f29a4`. Server: '\Private\' (efile TLitC 0x801e7f7c) + SID hex 8 + '\' without a drive (EKA2L1 fs.cpp:858 private_path -> fs.cpp:108, cross-reference)

87CreatePrivatePath0x0slot0 = TInt aDrivenamed onlynamed only: the server side (mkdir of the private directory on aDrive) is not RE'd
evidence

Client: EFSrv.dll 0x801ed15c `str r1,[sp] ; movs r1,#0 ; str r1,[sp,#0x10] ; movs r1,#0x57 ; mov r2,sp ; bl 0x801f29a4`

!Loader

name literal 0x801e7630 session type 0 implementation loaderServer in web/src/fs/loader.js

the loader server (RLibrary::Load / RProcess::Create go through it). CServerLoader::NewSessionL is slot 7 of its vtable 0x801E96E8 -> 0x801D2225, the same slot that holds CServerFs::NewSessionL (0x801D2E4B) in the file server's vtable 0x801E9E8C, which is what pins the index.

evidence

TLitC16 at 0x801e7630 inside efile.exe: length 7, '!Loader'. Created by the same image: 0x801d0c3e allocates 0x68 bytes and calls 0x801d0c30 `movs r2,#0 ; blx 0x801e4bb4` = CServer2::CServer2 with aType 0 (EIpcSession_Unsharable), then 0x801d0c5a..0x801d0c60 `ldr r1,[pc,#0x14c] ; adds r1,#0x58 ; blx 0x801e4b8c` = CServer2::StartL with r1 = [0x801d0da8] + 0x58 = 0x801e75d8 + 0x58 = 0x801e7630, this TLitC.

functionnameflagsargumentsimplementationnoteevidence
-1Connect0x0slot0 = TVersion packed word (iMajor byte 0, iMinor byte 1, iBuild halfword 1)connectanswered KErrNone iff QueryVersionSupported holds, exactly as !FileServer's connect is; the loader session carries no path (CServerFs's path setup has no counterpart here — the loader's session object is 0x18 bytes and NewSessionL sets only its vtable)
evidence

Client: euser RSessionBase::CreateSession issues Exec::SessionCreate 0x7e and then the KConnectMessage whose TIpcArgs slot 0 is the packed TVersion, exactly as the !FileServer -1 row describes; the run's own connect arrives with args[0] = 0x063f0001 and flags 0, i.e. the client asks for iMajor 1, iMinor 0, iBuild 0x63f (measured at the halt this row resolves, run-rom halt text). Server: CServerLoader::NewSessionL 0x801D2225, reached as slot 7 of the loader server's vtable 0x801E96E8 (the constructor at 0x801D0C30 does `movs r2,#0 ; blx 0x801e4bb4` = CServer2::CServer2(aPriority, aType=0) then `ldr r1,[pc,#0x194] ; str r1,[r0]` with the literal at 0x801D0DD0 = 0x801E96E8); slot 7 is the NewSessionL slot because the file server's vtable 0x801E9E8C holds the known CServerFs::NewSessionL 0x801D2E4B in that same slot. Its body: `movs r2,#0 (iMinor) ; ldr r3,[pc,#0x3d8] (iBuild = [0x801D2604] = 0x625) ; movs r1,#1 (iMajor) ; mov r0,sp ; blx 0x801e4c14` = TVersion::TVersion, then `movs r1,r4 ; mov r0,sp ; blx 0x801e538c` = User::QueryVersionSupported(current, requested) — the SAME stub the !FileServer -1 row cites — and `cmp r0,#0 ; bne 0x801d2246 ; subs r0,r0,#5 ; blx 0x801e53ac` leaves KErrNotSupported (-5) when it is not supported; otherwise it allocates the 0x18-byte session (`movs r0,#0x18`) and stores its vtable [0x801D2608] = 0x801E9810. QueryVersionSupported itself, euser 0x801A5B27, was disassembled rather than assumed: it does `ldrsb r2,[r1,r3]/ldrsb r4,[r0,r3]` with r3=0 (iMajor) and then r3=1 (iMinor) and returns 1 iff requested.iMajor < current.iMajor, or the iMajors are equal and requested.iMinor <= current.iMinor. OFFSET 2 (iBuild) IS NEVER READ, which is why the client's 0x63f against the server's 0x625 does not fail the connect. So 1.0.x is supported and the connect completes KErrNone — the device's own arithmetic, not a chosen answer.

2LoadLibrary0x1fcslot0 = TDes8& over a 0x20-byte tldrinfo (OUT: handle at +0x10), slot1 = const TDesC16& aFileName, slot2 = const TDesC16& aPathload_library⚠ PARTLY MODELLED, and the seam is here. Everything up to the handle is evidenced: the uid1 gate, the '.DLL' rule, the write-back. What is NOT disassembled is slow exec 0x8e, so the kernel object the handle NAMES is unknown; this impl allocates a library object in KState the way every other kernel object here is allocated and returns that handle. A non-empty aPath and a library the VFS cannot find both HALT rather than guess, because efile answers those through the drive search at 0x801D19FE, also not disassembled. First real call in this run: hal.dll, which IS in samples/firmware/5320/sys/bin.
evidence

Client: euser ordinal 1135 at 0x801A78FE sends it — 0x801A7954..0x801A7958 `ldr r0,[sp,#0x44] ; movs r1,#2 ; bl 0x801b7c66`, so r1 = 2 IS the function number. The flag word is NOT arithmetic on the measured value: the 3-arg TIpcArgs ctor euser 0x801B8B04 bakes it in as a constant (`stm r0!,{r1,r2,r3} ; subs r0,#0xc ; movs r1,#0xff ; adds r1,#0xfd ; str r1,[r0,#0x10]` = 0xFF+0xFD = 0x1FC), which is what verifies the argFlags 0x1fc the run delivers. Slot 0 is a TPtr8 of length 0x20 over the tldrinfo the caller built at 0x801A790C; slot 1 the filename; slot 2 aPath (length 0 in this run). Server: the session vtable is [0x801D2608] = 0x801E9810 and its ServiceL is 0x801D1DCC, which accepts ONLY fn 1..14 — 0x801D1DD6 `subs r0,r5,#1 ; cmp r0,#0xe ; blo` is an UNSIGNED-lower test on (fn-1), so 15 is rejected with -5 (a first RE pass read this as 1..15 and its own skeptic refuted it; re-disassembled here). fn 2 dispatches through __ARM_switch8 at 0x801D1ED6 to 0x801D1F5A, which appends the TLitC8 '.DLL' at 0x801E7574 only when the name has no '.', then calls the handler 0x801D1480. That handler reads tldrinfo.uid1 and accepts only 0 or 0x10000079 = [0x801D16E8] - 1 (0x1000007A - 1), forcing 0x10000079 at 0x801D14A0 — which is what makes fn 2 the DLL load and fn 1 (which checks 0x1000007A and appends '.EXE') the process load. The handle from slow exec 0x8e is stored at tldrinfo+0x10 (0x801D1550) and the whole 0x20-byte block written back into slot 0 (0x801D13A4, RMessagePtr2::Write(0, TPtrC8(&req,0x20), 0)); the client then reads the handle out of its OWN copy at euser 0x801A795E, which is why the write-back is load-bearing.

!Windowserver

name literal 0x804aa60c session type 0 implementation wsServer in web/src/fs/wserv.js

the window server; its IPC functions are in fs-op.tsv and its command-buffer opcodes in wserv-op.tsv. EwSrv.exe is XIP-only and is NOT among the extracted samples/firmware/5320/sys/bin files, which is exactly why the session boundary is the seam.

evidence

TLitC16 at 0x804AA60C inside EwSrv.exe (TRomImageHeader 0x80489720, uid3 0x10003B20, code 0x80489798 size 0x262B4): length 13, '!Windowserver'; the same UTF-16 text is at 0x804B6618 inside Ws32.dll (header 0x804AFC60), the client library whose RWsSession::Connect 0x804B33A4 loads it (0x804B33D0 `ldr r1,[pc,#0x3e8]`) and passes it to RSessionBase::CreateSession (Ws32 stub 0x804B5B00 -> euser 0x801A6CF9) with r3 = -1 message slots. EwSrv.exe creates the server: 0x80499504 allocates 0x9c bytes and calls 0x804994D8, which does `movs r1,#0x64 ; ldr r2,[pc,#0x184] ; adds r2,#8 ; movs r3,#0 ; blx 0x804a74f8` = the XIP import stub (`ldr pc,[pc,#-4]` + literal 0x801A2C15) of euser CPolicyServer::CPolicyServer(TInt aPriority, const TPolicy&, TServerType aType) with aType = 0; that body forwards to `bl 0x801a29cc` = the same CServer2::CServer2 the !FileServer row cites (0x801a29cd), which stores aType at CServer2+0x1c. The name is registered at 0x804A2F4E: `ldr r1,[pc,#0x38c]` (= [0x804A32DC] = 0x804AA60C) `; ldr r0,[r6,#0x24] ; blx 0x804a7038` = euser CServer2::Start 0x801A29C1 (`bl 0x801a29a0` StartL then `bl 0x801ab79a` LeaveIfError). So !Windowserver's TIpcSessionType is 0 (EIpcSession_Unsharable), which is what the client's 3-argument CreateSession asks for.

functionnameflagsargumentsimplementationnoteevidence
-1Connect0x0slot0 = TVersion packed word (iMajor byte 0, iMinor byte 1, iBuild halfword 1)connectanswered KErrNone iff QueryVersionSupported holds, exactly as the !FileServer and !Loader connects are
evidence

Client: Ws32.dll RWsSession::Connect 0x804B33A4 builds the version with `bl 0x804b3392` = RWsSession::Version (Ws32 ord 71), whose body is `movs r2,#0 ; movs r1,#1 ; movs r3,#0x97 ; mov r0,sp ; blx 0x804b59f8` = TVersion(1, 0, 151), and passes it to RSessionBase::CreateSession (stub 0x804B5B00 -> euser 0x801A6CF9) with the '!Windowserver' TLitC and aMessageSlots = -1 (`movs r3,#0 ; mvns r3,r3`), then `blx 0x804b5ba0` = User::LeaveIfError. euser's CreateSession issues Exec::SessionCreate 0x7e and then the KConnectMessage (fn -1) whose TIpcArgs slot 0 is the packed version, exactly as the !FileServer -1 row describes. Server: EwSrv.exe 0x80499710: `movs r2,#0 ; movs r1,#1 ; movs r3,#0x97 ; mov r0,sp ; blx 0x804a70a0` = TVersion(1, 0, 151), then `movs r1,r4 ; add r0,sp,#4 ; blx 0x804a7860` = the XIP import stub (literal 0x801A5B27) of User::QueryVersionSupported — the SAME euser export the !FileServer and !Loader connect rows cite — and `cmp r0,#0 ; bne ; subs r0,r0,#5 ; blx 0x804a7878` leaves KErrNotSupported otherwise. Client and server build the identical TVersion in two independent images, so 1.0.151 is measured twice, and QueryVersionSupported (equal iMajor, requested.iMinor <= current.iMinor) holds.

0CommandBuffer0x4slot0 = the client-side command buffer, a TDes8 at RWsBuffer+0xC (type 4 = writable 8-bit descriptor); slots 1..3 come from the caller of RWsBuffer::DoFlush and are 0 for a plain flushcommand_bufferthe whole window-server protocol arrives through this ONE function; the opcodes inside the buffer are db/os/wserv-op.tsv. flags 0x4 is a plain flush; a reply-buffer flush (RWsBuffer::WriteReplyP 0x804B0716) fills slots 1..3 through a virtual that is not disassembled, so it arrives with different flags and HALTs
evidence

Client: RWsBuffer::DoFlush 0x804B0302 — `ldr r0,[r4,#0xc] ; lsls r0,#4 ; lsrs r0,#4 ; bne` returns immediately on an empty buffer, `movs r5,r4 ; adds r5,#0xc` takes the buffer descriptor, `ldr r1,[sp,#0x10] ; movs r0,#4 ; orrs r0,r1 ; str r0,[sp,#0x10]` ORs slot-0 type 4 into the TIpcArgs type word at +0x10, `str r5,[sp]` puts the descriptor in slot 0, then `ldr r0,[r4] ; mov r1,sp ; bl 0x804b4058`, and 0x804B4058 is `adds r0,#8 ; movs r2,r1 ; movs r1,#0 ; blx 0x804b5a10` = RSessionBase::SendReceive(aFunction = 0, aArgs) on the RSessionBase at RWsSession+8 — the euser export at 0x801A8B03, which EFSrv.dll reaches through its own stub 0x801F2520, i.e. the very export the !FileServer op rows cite. After the send, `movs r7,r0 ; movs r0,r5 ; blx 0x804b5c28` empties the descriptor and `str r6,[r4,#0x1c]` clears the last-handle cache, and the SendReceive result is returned to the caller — which is how a create op gets its handle. A TIpcArgs is 0x14 bytes (0x804B032A `movs r2,#0x14` copies the caller's).

2Init0x0TIpcArgs() with the type word zeroed (0x804B33E0 `str r0,[sp,#0x14]` with r0 = 0); the four slots are left uninitialised and no slot is typed, so the server reads none of theminitPARTLY MODELLED, and this is the seam: EwSrv's handler for fn 2 is not disassembled, so the number the server would return is unknown. The client only rejects a negative one (User::LeaveIfError) and otherwise echoes it back in command records, so this impl allocates a session handle the way !Loader's LoadLibrary allocates its library handle and returns that
evidence

Client: Ws32.dll RWsSession::Connect 0x804B33A4, immediately after CreateSession: `movs r0,#0 ; str r0,[sp,#0x14] ; movs r1,#2 ; movs r0,r5 ; add r2,sp,#4 ; bl 0x804b611e` where r5 = this+8 = the RSessionBase and 0x804B611E is `push {r4,lr} ; blx 0x804b5a10 ; pop {r4,pc}` = the same RSessionBase::SendReceive(TInt, const TIpcArgs&) the fn-0 row cites; so the function number IS 2. `blx 0x804b5ba0` = User::LeaveIfError on the result, then `str r0,[r4]` stores it as the RWsSession's OWN iWsHandle, which is the handle every session-scope command record (wserv-op.tsv cls=session) then carries.

!Fontbitmapserver

name literal 0x8046b53c session type 0 implementation fbsServer in web/src/fs/fbserv.js

the font and bitmap server: it owns the shared chunk the CFbsBitmap pixels live in, so it is where a frame's bytes come from. Only its connect is answered; CFbsBitmap::Create (fn 0xe) HALTs, see fs-op.tsv.

evidence

TLitC16 at 0x8046B53C inside FbServ.exe (TRomImageHeader 0x80466CF0, uid3 0x10003A16, code 0x80466D68 size 0x4FF4): length 17, '!Fontbitmapserver'. Reached as a literal-pool value minus a constant, the same shape the !FileServer row cites: 0x804697A0 `ldr r1,[pc,#0xf0]` = [0x80469894] = 0x8046B564, then `subs r1,#0x28` = 0x8046B53C, `movs r0,r4 ; blx 0x8046a484` = the XIP import stub of euser CServer2::Start 0x801A29C1. (FbServ.exe carries a second identical TLitC16 at 0x8046B40C; the one the server actually registers with is 0x8046B53C.) The session type comes from the constructor at 0x80469742: `movs r1,#0 ; ldr r2,[pc,#0x14c] ; movs r3,#0 ; blx 0x8046a664` = euser CPolicyServer::CPolicyServer(aPriority 0, aPolicy, aType 0) -> CServer2::CServer2 0x801A29CD, so the TIpcSessionType is 0. The client side is FbsCli.dll (header 0x8046BD60, code 0x8046BDD8 size 0xBDF0), which holds its own copy of the name at 0x80476FA0.

functionnameflagsargumentsimplementationnoteevidence
-1Connect0x0slot0 = TVersion packed word (iMajor byte 0, iMinor byte 1, iBuild halfword 1)connectanswered KErrNone iff QueryVersionSupported holds against 1.0.94; the version the client asks for is whatever arrives in slot 0, and the arithmetic is the device’s own
evidence

Server: FbServ.exe 0x804697B4 — `movs r2,#0 ; movs r1,#1 ; movs r3,#0x5e ; mov r0,sp ; blx 0x8046a4b4` = TVersion(1, 0, 94), then `movs r1,r5 ; mov r0,sp ; blx 0x8046a81c` = the XIP import stub (literal 0x801A5B27) of User::QueryVersionSupported, and `cmp r0,#0 ; bne ; subs r0,r0,#5 ; blx 0x8046a83c` leaves KErrNotSupported otherwise. Client: RFbsSession's own Connect is NOT disassembled — what is established is that a live RFbsSession is reached through FbsCli 0x804744B2 (`bl 0x8047526c`), that it holds an RChunk at +0x10 whose base FbsCli 0x8047452E reads (`adds r0,#0x10 ; blx 0x80475930`), and that every request goes through 0x804744EA, which panics with 2 unless the session handle at +4 is positive.

14CreateBitmap0x4slot0 = TDes8& (type 4) over a 0x220-byte command struct whose first three words are {TInt iWidth, TInt iHeight, TInt iDisplayMode}; the answer is written back into the same descriptor and read at descriptor+0x21C, +0x220 and +0x224create_bitmapHALTS, and names why: two of the three answer words are addresses inside the FBS shared chunk (RFbsSession+0x10, base via euser RChunk::Base at FbsCli stub 0x80475930), and neither RFbsSession::Connect's chunk handshake nor the chunk's allocator is disassembled. Returning a handle with a fabricated data pointer would let every later CFbsBitmap call dereference a lie. The halt prints the {width, height, mode} the client asked for, which is the surface geometry the game wants
evidence

Client: FbsCli.dll CFbsBitmap::DoCreate 0x80472496, reached from ord 26 CFbsBitmap::Create(TSize, TDisplayMode) 0x80472568 (`ldr r3,[pc,#0x124] ; ldr r3,[r3,#0xc] ; b 0x80472496`). It rejects aDisplayMode outside 1..13 except 9 (0x804724BA..0x804724C4) and a negative width or height, calls Reset (0x80472312), stages the command at sp+0x244 with `ldr r0,[r5] ; ldr r1,[r5,#4] ; add r2,sp,#0x244 ; stm r2!,{r0,r1,r6}` = {iWidth, iHeight, aDisplayMode}, copies 0x220 bytes of it into the descriptor at sp+0x18 (`movs r2,#0x11 ; lsls r2,r2,#5 ; blx 0x80475b88`), then `movs r0,#4 ; str r0,[sp,#0x14] ; str r7,[sp,#4] ; ldr r0,[r4,#4] ; movs r1,#0xe ; add r2,sp,#4 ; bl 0x804744ea` — function number 0xe, TIpcArgs type word 4, slot 0 the descriptor. On KErrNone it reads the answer back out of that same descriptor: `ldr r0,[sp,#0x234] ; str r0,[r4,#0x10]` (the address pointer Handle() and Reset() test), `ldr r0,[sp,#0x238] ; str r0,[r4,#0x14]` (the handle FbsCli ord 130 CFbsBitmap::Handle returns, and therefore the handle wserv op 0x1e and op 0x33 carry), and `ldr r0,[r4,#4] ; bl 0x8047452e ; ldr r1,[sp,#0x23c] ; adds r0,r0,r1 ; str r0,[r4,#8]` = the RFbsSession chunk base plus an offset, which is the CBitwiseBitmap the client dereferences from then on.

The 31 walls, in order

Projected from db/frontier.json, the recorded honest halt of the deep ROM run. The single recorded honest halt of the deep ROM run (web/test/run-rom.mjs asserts the run stops exactly here). GREEN = the run halts at `current` and every audit passes; PROGRESS = `current` advances AND the change is backed by new evidence (mapped real code, or new evidence rows). Moving this file by hand WITHOUT the run actually reaching the new halt is the failure the whole db/ tooling exists to prevent. Run `node tools/next.mjs` for the next action.

The wall standing now: unimplemented-import, at 68477 instructions

mgs_ngi.exe's Arena initialisation calls ArenaEnv ordinal 1 at 0x249f8 once the file server has answered for Arena/config.xml

arenaenv@1 — the N-Gage Arena environment's first entry point. The ROM-backed profile has now reached the SAME wall the container-only profile measured from the other side (db/frontier-hle.json): the game's own startup wants the N-Gage platform, whose runtime.dll resolves features through a Central Repository .cre the platform provisions and whose implementations are separately installed Arena DLLs. samples/launcher/installed/sys/bin holds ArenaEnv.dll, and it is a 340-byte stub with 2 exports and 0 imports — so the question to settle first is what that stub actually is, and whether the real implementation is ngiNAF.dll, which carries the same uid3 0x2000106d.

Read ArenaEnv.dll and ngiNAF.dll from samples/launcher/installed/sys/bin with tools/e32info.py before implementing anything: two images share uid3 0x2000106d and only one of them can be the one the game links against. The container-only profile reached this same platform boundary at 19,801 instructions and answered runtime@9/@10 honestly (KErrNone for the environment, the real -1000004 for an unresolvable feature); the ROM profile arrives here with the whole file server and loader behind it, so the two profiles now differ only in what they answer, not in where they stop. Whichever is implemented next, the evidence must come from those DLLs, not from the game's expectations.

Wall 1: unimplemented !FileServer op 22 (RFs::Entry) at 68,015 instrs — the game's Arena runtime asking for the metadata of C:\private€10925\Arena\config.xml

By answering it from the VFS the container installed, with the two fields this filesystem cannot source DECLARED rather than invented. Everything the device does was read first (db/os/fs-op.tsv (!FileServer,22)): efile's handler 0x801d3530 delegates to the mounted filesystem through CMountCB vtable slot 0x40 and passes the mount's leave reason through unchanged; the mount is ELocal.fsy, which IS in the ROM (TRomEntry 0x80046328, image 0x801F3BD0, EntryL 0x801F7B21), and its not-found code is KErrNotFound -1, read at 0x801f7b06 rather than chosen. EntryL writes exactly four fields — iAtt, iSize, iModified, iName — and never iType, which independently corroborates struct tentry. TWO of those four have no source in a VFS synthesised from a SIS: iModified is left ZERO (a SIS install carries no per-file timestamp, and the host clock would be both the wrong clock and non-deterministic, which is what the gate rests on) and iAtt reports only bit 0, set when the VFS entry is read-only — the one bit with behavioural support anywhere in this ROM (efile 0x801CC996 -> -21). A name that is a directory in the VFS HALTs rather than guess the attribute ELocal would report for it, and a name with neither a drive nor a leading separator HALTs because efile's TParse defaults for that case are not disassembled. MEASURED: 68,015 -> 68,477, the reply is 'C:\private€10925\Arena\config.xml -> 3376 B' (asserted in run-rom.mjs), no message is left outstanding, and the run walks on into the game's own Arena initialisation — where it meets, from the ROM side, the same wall the container-only profile measured: arenaenv@1.

evidence

db/os/fs-op.tsv (!FileServer,22); web/src/fs/fileserver.js entry/absolutePath/writeEntryHeader/writeEntryName; web/test/run-rom.mjs !FileServer sequence + the Entry reply assertion

Wall 2: null-branch from euser 0x8019fb36 (1014 instrs) — the honest frontier

evidence

STILL OPEN after the carry fix (boot path doesn't hit it). Upstream divergence causes a mid-function entry at fb14. Next: deeper back-trace + more oracle coverage.

Wall 3: Thumb ADDS/SUBS dropped carry/overflow (reg+imm3+imm8 forms set only N/Z)

cpu.js addF/subF — found by the expanded differential oracle (64-bit ADC). NOT on the boot path before 1014, but a real systemic bug.

evidence

web/test/cpu-oracle.mjs 64-bit add; tools/lib/elf-link.mjs

Wall 4: libc@2 import (600 instrs)

Mapped the REAL libc/pthread/zlib/dl from firmware (run pristine code, not HLE stubs) — resolved 95 imports honestly.

evidence

run-rom FIRMWARE_LIBS; samples/firmware/5320/sys/bin

Wall 5: exec 0x4c (676 instrs)

WAS FABRICATION-INFLATED: only reached because libc@2 silently returned 0 at ~600 instrs. Making import stubs HALT exposed the real frontier.

evidence

registry _stub now throws

Wall 6: cleanup-stack panic 66/63 (hand-modeled CCleanup)

let real euser CTrapCleanup::New build it; artefact of the cond bug

evidence

web/src/kernel/kstate.js installBootHeap

Wall 7: Thumb cond() HI/LS/VS/VC missing -> every bhi/bls always taken

cpu.js cond() explicit cases 6..9 (real CPU bug)

evidence

web/test/thumb-cond.mjs

Wall 8: boot diverged → mid-function entry at euser fb14 → null pop (~1014 instrs)

ROOT CAUSE: the ROM places XIP DLL code at codeAddress+8, but mapRomImage loaded at codeAddress (8 low). DLLs were self-consistent internally but ROM veneers (absolute VAs) landed 8 bytes into target functions, skipping the prologue. Fixed: ROM_CODE_SHIFT=8 (rom.js) + bootstrap VA +8 (kstate.js). Boot now reaches exec 0x4d cleanly at 890 instrs.

evidence

rom.js ROM_CODE_SHIFT; the standalone euser code section appears in the 5320 ROM at codeAddress+8

Wall 9: exec 0x4d UserSvr::RomRootDirectoryAddress (890 instrs)

returns TRomHeader.rom_root_dir_list (read live from ROM header @+0x94). Evidence: eka2l1 svc.cpp + rom.h. Boot advanced to 1536 instrs.

evidence

db/os/exec-impl.tsv slow 0x4d; web/src/kernel/exec-impl.js rom_root_dir_address

Wall 10: exec 0x69 handle_close / RHandleBase::Close (1536 instrs)

ekern 0x80083b98: h&0x8000->KErrGeneral; else close+return TObjectType (EChunk=2). Impl drops the handle and returns the type; euser user-side cleanup ran clean. The earlier 'lifecycle gap' was a false alarm — exec 0x6a DID create handle 0x40000001. Boot advanced to 1587.

evidence

db/os/exec-impl.tsv slow 0x69; exec-impl.js handle_close

Wall 11: exec 0xa0 static_call_list + the static-init loop (1587-1852 instrs)

RE'd the euser runner (0x801a6174): a loop StaticCallList->run ctors->StaticCallDone that stops when either returns -25. Our model has no codesegs in that loop -> StaticCallList returns -25 (done) on call 1, exiting cleanly (+256). Skips static_call_done (0xa1) entirely. MUST revisit when game DLLs are loaded.

evidence

db/os/exec-impl.tsv slow 0xa0; euser runner adds r0,#0x19;beq

Wall 12: fast exec 0x2 User::Critical (1852 instrs)

ekern 0x8006a67c returns thread's TCritical field; impl returns tracked KState.thread.critical (default ENotCritical=0). Boot advanced to 1885.

evidence

db/os/exec-impl.tsv fast 0x02 user_critical

Wall 13: slow exec 0x6f RSemaphore::CreateLocal (1885 instrs)

ekern 0x80084184 + eka2l1 semaphore_create; euser caller panics on count<0 (the semaphore guard). Alloc a 'semaphore' handle (TObjectType 4); single-threaded so it never blocks. Creating it (the heap lock) unblocked +2963 instrs to 4848.

evidence

db/os/exec-impl.tsv slow 0x6f sema_create

Wall 14: slow exec 0x67 RThread::Create (4848 instrs)

ekern 0x800839fc copies a 0x30B SThreadCreateInfo (eka2l1 thread_create); alloc a suspended 'thread' object (TObjectType 0) capturing entry/arg/stack/size; return handle. Boot advanced to 4874. Resume/scheduler is the next subsystem step.

evidence

db/os/exec-impl.tsv slow 0x67 thread_create

Wall 15: exec 0x28 RThread::Resume + word LDREX/STREX (4874-5963 instrs)

thread_resume marks the thread runnable (eka2l1 thread_resume). Added ARMv6 word LDREX/STREX to arm.js (single-CPU: monitor always succeeds; v6K byte/half/dword stay unimplemented per the decode audit). +1089 to 5963, then a null fn-pointer call in the thread-entry region.

evidence

db/os/exec-impl.tsv slow 0x28; web/src/cpu/arm.js LDREX/STREX

Wall 16: exec 0xa0 mis-identified as the C++ constructor path (the 2025 revision of this ledger)

CORRECTED BY RE, not by a code change. ekern 0x80089a84 walks [DThread+0x138], an SDblQue that the DThread constructor self-links empty (0x800927f8 `add r1,r0,#0x138`) and that ONLY DLibrary::Close feeds (0x80088cec-0x80088d08); its euser runner 0x801a617c passes r0=3. It is the pending-library-DETACH queue, so -25 was always the faithful answer for a process that has closed no RLibrary. Constructors come from exec 0x9f (ekern 0x80089994) off DProcess::iCodeSeg at +0x108. The 2025 note claiming 0xa0 walks a codeseg init-list, and the REFUTED experiment built on it (publishing entry_point+8 and calling with r0=3), were both wrong for the same reason: entry_point+8 is the `bx ip` of a self-contained veneer whose `ldr ip,[pc,#4]` sits at +4, and r0=3 selects the destructor branch, whose guard word in this ROM is 0.

evidence

db/os/exec-impl.tsv slow 0xa0 and slow 0x9f evidence columns

Wall 17: null function-pointer call at backend.dll 0x82e462a8, lr=0x82e531a7 (5963 instrs) — the member at [0x3ffc0094] never constructed

The codeseg / static-initialiser subsystem, plus THREE pre-existing fabrications the old import resolution had been hiding. (1) TRomImageHeader is 120 (0x78) bytes, not 128: `HDR=128` + a compensating `ROM_CODE_SHIFT=8` loaded each XIP image at the right address but read the export directory two slots too far, so every one of the 95 firmware imports resolved to ordinal+1's VA + 8 bytes. euser@1360 was landing on 0x801b05c5 (which happened to return 0) instead of the real UserHeap::SetupThreadHeap 0x801b2d01. Removing it dropped the run 5963 -> 340 — de-fabrication, exactly like the 0x4c entry of 2025, and every later gain is real work. (2) `msr cpsr_f,Rm` was a silent no-op in arm.js, so euser's descriptor decoder at 0x8019ddf4 kept the flags from the preceding `cmp` and every TLitC decoded to garbage; SetupThreadHeap rejected its chunk name with KErrBadName. 340 -> 932. (3) exec 0x3 was recorded as `mov r0,#0xf ; bx lr`, which is the handler of exec 0x4 at 0x800822b0 — the +8 misread of the same header bug, via tools/disasm.py. The real handler at 0x800822a8 is `ldr r0,[r0,#0x28]` = RChunk::MaxSize, and exec 0x6b selector 0 (RChunk::Adjust) was returning the chunk base where euser's `beq` demands KErrNone. With those fixed the real heap builds and the game reaches EUser ordinal 585, which issues svc #0x9f. Exec 0x9f now walks DProcess::iCodeSeg deps-first/self-last (ekern TraverseDeps 0x800874d8) and publishes 8 TRomImageHeader.entry_point veneers; euser's runner calls each with r0=2 and skips the last (the EXE). Also fixed: XIP writable data lives at TRomImageHeader+0x60 (data_bss_linear_base), not at data_address — the old loader was zeroing bss over libc's own TRomDllRefTable in ROM. VERIFIED, not assumed: exec 0x9f dispatched exactly once with count 8; all 7 DLL veneers executed exactly once each with r0==2; euser and libc _E32Dll both reached their .init_array walkers; LibCIpcClient's ctor (0x82e61cd6) and libc's (0x82e78cec) ran; the game's own walker 0x83ac -> ctor 0x22adc still ran afterwards; and [0x3ffc0094] — the exact word this frontier recorded as null — reads 0x1000. Boot advanced 5963 -> 5581 at a NAMED unimplemented exec (0x7e) instead of a null branch.

evidence

db/os/struct.tsv romimageheader + romdllreftable + tchunkcreate; db/os/exec-impl.tsv slow 0x9f, 0x3, 0x6b, 0x6a, 0x6d and the rewritten 0xa0; web/src/kernel/codeseg.js; web/src/cpu/rom.js HDR=0x78; web/src/cpu/arm.js MSR; web/test/run-rom.mjs codeseg assertions

Wall 18: slow exec 0x7e Exec::SessionCreate (5581 instrs) — fully RE'd but deliberately left unimplemented, because the only answer our kernel could justify was KErrNotFound over an EMPTY EServer container

By BUILDING the container the previous rung said to build, and by letting the ROM — not the author — say what is in it. The blocker was never the exec's algorithm (that was already RE'd); it was the claim 'a real 5320 has a server named !FileServer at this moment', which now rests on the firmware itself. TRomHeader carries two boot-file pointers, and both decode without ambiguity: iPrimaryFile (+0xA0) -> TRomEntry{iSize 0x37CB8, iAddressLin 0x80067F88, iNameLength 9, iName 'ekern.exe'} and iSecondaryFile (+0xA4) -> TRomEntry{iSize 0x205D4, iAddressLin 0x801CB600, iNameLength 9, iName 'efile.exe'} — each iAddressLin is a real TRomImageHeader VA, so the decode is coincidence-free. iSecondaryFile is the first USER-side process the kernel starts, i.e. the F32 file server runs before any application does; our emulator simply skips device boot. The NAME is read out of efile.exe's own TLitC16 at 0x801E7EC0 (length 11, '!FileServer'), never typed in, and web/src/kernel/servers.js verifies every structural expectation around it (iSecondaryFile really names efile.exe, its uid1 really is 0x1000007a EXE, the literal really lies inside that image, the text really is 11 characters and '!'-prefixed) and HALTs otherwise. Cross-checks that close the loop: searching all of 5320.rom for the UTF-16 text finds it exactly TWICE — in efile.exe and in \Sys\Bin\EFSrv.dll (header 0x801EBC50) at 0x801F2FC0, the client library whose RFs::Connect is the very code issuing this exec; efile.exe treats the string as a SERVER name in its own code (0x801D2D74 loads it into a TFindServer, 0x801D2D80 calls TFindServer::Next); and on the kernel side a sweep of ALL TWELVE call sites of ekern's container-add helper 0x8008C22C finds r1 = 7 (EServer) at exactly one, 0x8008A8E0, inside the handler of slow exec 0x7d = Exec::ServerCreate — which also does `ldrb r0,[r0] ; cmp r0,#0x21` at 0x8008a850 (the '!'-prefixed protected-server convention) and writes aMode to DServer+0x3c at 0x8008a8a8, the exact byte DSession attach reads back at 0x8008a37c. The exec itself is implemented in the handler's own order and every case the firmware does not fix HALTs rather than defaulting: a non-null aPolicy (the six in-range arms of the validator 0x8009a66c are unread), aMsgSlots>0 (the dedicated message pool at 0x8008a2fc is unmodelled), aMode>0 (would require efile's TIpcSessionType, which aMode=0 does not), a wildcard name (only the '*' comparison of TDesC::Match 0x80099048 has been read), and a KState with no ROM behind its memory. Also added on the way: web/src/kernel/descriptor.js, which reproduces the kernel's own descriptor-head decoder ekern 0x8006b934 instruction for instruction — its `eor r3,r3,r3,lsr #1 ; msr apsr_nzcvq,r3` flag trick reproduces all five TDesType layouts, and the boot's live TBuf8 {0x3000000b,0x50,'!FileServer'} lands on type 3 EBuf exactly as predicted. VERIFIED, not assumed: exec 0x7e dispatched exactly once and returned handle 0x40000029; the EServer container holds exactly one object, '!FileServer', sourced from efile.exe; and the run's NEXT exec is 0x4c with r0 = 0x40000029 (that same handle) and r1 = -1 = KConnectMessage — so the boot went THROUGH RFs::Connect, not around it. All four are asserted permanently in web/test/run-rom.mjs. Boot 5581 -> 5624.

evidence

db/os/exec-impl.tsv slow 0x7e; db/os/struct.tsv + struct_field.tsv tdesc / tromheader / tromentry; web/src/kernel/servers.js; web/src/kernel/descriptor.js; web/src/kernel/exec-impl.js session_create; web/test/run-rom.mjs EServer/session/KConnectMessage assertions

Wall 19: slow exec 0x4c Exec::SessionSendSync (5624 instrs) — the synchronous send carrying RFs::Connect's KConnectMessage, with the open worry that implementing it would mean fabricating the server's reply

By implementing what the handler ACTUALLY does and nothing more — and the firmware turned out to make that easy, because the exec's return value has nothing to do with the server's answer. ekern 0x8006ac80 was disassembled end to end and reproduced in its own order: `ldr r4,[r0,#0x20]` proves the handler is entered with a DSession* (that field is the one DSession attach writes at 0x8008a3ac `str r5,[r4,#0x20]`, r5 being the DServer whose +8 iAccessCount attach had just tested the same way at 0x8008a33c — two routines cross-confirming one layout); `cmn r1,#2` -> -6 KErrArgument; `cmp r4,#0 / ldrne r6,[r4,#8] / cmpne r6,#0 / beq` -> -15 KErrServerTerminated. The message is the RMessageK embedded in the DThread at +0xEC — `sub ip,sb,#0x1d4` measured against the `sub sb,sb,#0x2c0` that the same routine uses to get the current DThread at 0x8006ad54 — and its first word is the queue link the delivery routine writes at 0x8006ae60, so `ldr r7,[ip] ; cmp r7,#0 ; bne 0x80089e50` is a busy test whose failure leg is `mov r0,#0x1e ; b 0x8008d468`, i.e. a KERN-EXEC PANIC (30), not an error return. 0x8008d468 was disassembled to confirm it is the panic helper (`mov r2,r0` code / `sub r0,r0,#0x2c0` current thread / `mov r1,#2` category / `b 0x80092638`), which also reclassifies the connect branch's two guards: 0x8006adc8 requires DSession+0x4c and +0x2c to be null and panics with 9 / 0x39 otherwise. The tail bumps iMsgCount (+0x34), links the message onto DSession+0x3c/+0x40, copies the caller's TIpcArgs as four words plus a HALFWORD of type flags (`ldmne r2,{r1,r3,r6,r7,r8}` / `stm r2!,{r1,r3,r6,r7}` / `strh r8,[r2]` — corroborated by euser's wrapper preloading the identical five words at 0x8019c590), delivers via 0x8006ae44 and returns `mov r0,#0`. Delivery itself is four instructions of SDblQue append onto DServer+0x34/+0x38 whenever DServer+0x28 (a thread blocked in Receive) is null; the non-null hand-off at 0x8006dfac MMU-switches to the receiver and was left HALTing because it is not RE'd. THE KEY FINDING, which removes the worry entirely: nothing in this handler ever dereferences aStatus — 0x8006acb4 only RECORDS it at msg+0x30. euser proves the division of labour: its send wrapper 0x801a6c58 pre-sets the TRequestStatus to KRequestPending (literal 0x801a6f0c = 0x80000001, iFlags 2), returns the exec's error directly when it is non-zero, and ONLY on zero calls User::WaitForRequest 0x801a4594 — whose loop spins on `blx 0x8019bd58` while [status] == KRequestPending (literal 0x801a48c0, the same constant) — before `ldr r0,[sp]` returns the STATUS as RSessionBase::CreateSession's result. So KErrNone here is the firmware's own answer, not a stand-in for the server's, and the status was deliberately left pending. VERIFIED, not assumed, and asserted permanently in web/test/run-rom.mjs: exec 0x4c dispatched exactly once; !FileServer's message queue holds exactly one message, fn == -1, whose session is the one exec 0x7e minted, which is simultaneously held in DSession+0x4c and in the thread's own sync slot with iMsgCount 1; the TIpcArgs type-flags halfword read 0; and — the anti-fabrication check — the guest word at *aStatus still reads 0x80000001 at the halt, so no reply was invented. Boot 5624 -> 5637, halting inside User::WaitForRequest on fast exec 0, which is the scheduler, not another exec.

evidence

db/os/exec-impl.tsv slow 0x4c session_send_sync (full disasm trace of ekern 0x8006ac80/0x8006ad50/0x8006adc8/0x8006ae44 + the panic helper 0x8008d468 + euser 0x801a6c58/0x801a4594); web/src/kernel/exec-impl.js session_send_sync and the DSession fields added to session_create; web/src/kernel/servers.js DServer msgQ/receiver; web/src/kernel/kstate.js thread.syncMsg; web/test/run-rom.mjs exec-0x4c assertions incl. the KRequestPending check

Wall 20: fast exec 0x0 Exec::WaitForAnyRequest (5637 instrs) — the wait loop inside User::WaitForRequest after exec 0x4c queued RFs::Connect's KConnectMessage, with the handler VA for fast 0 flagged as suspect

By reading the SVC vector instead of the table, and by building the scheduler the wait needs. ekern's __ArmVectorSwi at 0x80068120 (found by its opcode `ldr ip,[lr,#-4]`) decodes the immediate with `lsls ip,ip,#9` — C = bit 23, Z = the number is zero — then `blo` slow, `ldr r1,[fp,#0x130]` (TScheduler.iCurrentThread) and `beq 0x80068708`: fast exec 0 is special-cased BEFORE the fast table is indexed, and the table itself is indexed count-first (`ldr r3,[r2],ip,lsr #7 ; ldr r2,[r2] ; cmp r3,ip,lsr #9 ; bxhi r2` over the table NKern::ThreadCreate installs at 0x80069e1c, whose word 0 is the count 0x1c). So docs/data/ekern-exec-table.tsv's whole fast column is shifted by one slot: its 'fast 0x0 = 0x8006a674' is exec 1 (Heap: DThread+0x48), its 0x8006a684/0x8006a68c are 5/6 (ActiveScheduler get/set), 0x8006a694/0x8006a69c are 8/9 (TrapHandler get/set), and its 'fast 0x1b = 0xf1' is the SLOW table's count word. wait_for_any_request 0x80068708 is eleven instructions: `ldr r2,[r1,#0x20] ; subs r2,r2,#1 ; str r2,[r1,#0x20] ; bxge lr` decrements NThread::iRequestSemaphore.iCount and returns at once while it stays >= 0; otherwise iKernCSLocked/iRescheduleNeededFlag (TScheduler+0x10c/+0x108), iWaitObj = &the semaphore (+0x28), iNState = 2 EWaitFastSemaphore (+9), unready (0x800687ac) and the reschedule 0x80068c40, which saves SP at NThread+0x68 and runs the head of the highest-priority ready queue (the two-word bitmap at TScheduler+0, queues at +8+4p, new entries linked at the TAIL by 0x800688a8..0x800688b4). The only wake-ups are NFastSemaphore::SignalN 0x80068750 / Signal 0x8006880c — reached from NKern::ThreadRequestSignal 0x8006ea50, which is slow exec 0x3b (0x80082ba8 `mov r1,r0 ; mov r0,#0 ; b`) and the tail of every request completion. web/src/kernel/threads.js models exactly that: Thread contexts, the request semaphore, nstate/suspendCount, a FIFO ready queue, and a reschedule that HALTs as scheduler-idle when nothing is ready — naming the blocked thread, its semaphore count and the queued message only !FileServer's thread could complete. RThread::Create (0x67) and Resume (0x28) were re-derived on the way: Exec::ThreadCreate 0x800839fc validates iTotalSize/iUserStackSize (-6), forces type 3 / priority -5 / no supervisor stack, and the new thread's first user context is fixed by EpocThreadFunction 0x8006a868 (stack filled with 0x29, the info copy at the stack top with SP = top - iTotalSize, r4 = 1, PC = DProcess::iReentryPoint = the codeseg entry stored by 0x80091e00), ESuspended with iSuspendCount -1 until NKern::ThreadResume 0x8006e5d4 brings it to 0 — all unit-tested through the dispatcher in web/test/kernel.mjs, including a full block/switch/signal/resume round trip. THREE FABRICATIONS ON THE LIVE BOOT PATH fell out of the same RE and are gone: fast exec 2 is Exec::HeapSwitch (0x80082140 stores r0 into DThread+0x48/+0x4c and returns the old allocator — the old 'user_critical' returned 0 and every allocation kept going through the host bootstrap allocator); slow exec 1 is RChunk::Base (0x80082298 = DChunk+0x2c; euser constructs the RHeap at that address + offset, and with the old 'committed size' answer the heap had been built at 0x40000 in low memory — the next exec in the log, HAL::Get, wrote the page size to 0x40048); slow 0x4d is Exec::DllTls (0x80083128 tail-branches into DThread::Tls), not RomRootDirectoryAddress. Slow 0x14/0x61/0x5a rows, all from the defective euser-svc-map, were removed (their handlers are not what the rows claimed) and DllSetTls (0x75), DllFreeTls (0x76), ThreadRequestSignal (0x3b) and MutexCreate (0x6e) were added from their handlers. With the guest's own RHeap now running, the boot executes 630 more real instructions to the same wait: 5637 -> 6267, halting as scheduler-idle with the request semaphore at -1 and *aStatus still KRequestPending (both asserted in run-rom.mjs). The file server itself now boots in a second lane — see `secondary`.

evidence

db/os/exec-impl.tsv fast 0x00 wait_for_any_request, fast 0x02 heap_switch, fast 0x01, slow 0x01 rchunk_base, slow 0x3b, 0x28, 0x67, 0x4d, 0x75, 0x76, 0x6e (each carrying its disasm trail); db/os/struct.tsv + struct_field.tsv nthread / dthread / tscheduler / nfastsemaphore / sthreadcreateinfo / dprocess and romimageheader heap_min/heap_max/stack_size; web/src/kernel/threads.js; web/src/kernel/kstate.js (Scheduler, attachRomProcess, iReentryPoint); web/test/kernel.mjs scheduler round trip; web/test/run-rom.mjs scheduler + heap assertions

Wall 21: efile.exe secondary lane (9,392 instrs, halted at Exec::ObjectNext slow 0x0) — the dropped kernel-3 direction

RETIRED. It existed to run efile.exe as a guest so its thread could complete the game's connect message; the direction change of 2026-09-02 evening (game first, HLE at the server boundary) makes that unnecessary — the messages are completed at the boundary instead, and the game lane now has its own honest, deeper frontier (37,330 instrs). The RE it produced is preserved: DllSetTls (0x75), MutexCreate (0x6e) and UserSvr::RomHeaderAddress (fast 0x13) are all live execs used by the game lane too, and efile's CServer2 construction (0x801d2a6e / StartL 0x801a29a1 / RServer2::CreateGlobal) is the evidence for hle-server.tsv's server names and session types. The one-KState two-process merge (per-process data windows, the 0x8006dfac direct hand-off, the 0x800928ec priority table) is NOT needed for the game and is dropped, as §8 directed.

evidence

db/os/hle-server.tsv evidence column (efile's server construction); db/os/exec-impl.tsv slow 0x75 / 0x6e, fast 0x13 (RE'd in the efile lane, used by the game lane)

Wall 22: scheduler idle at 6,267 instrs — the main thread blocked in User::WaitForRequest on RFs::Connect's KConnectMessage, with no file-server process to complete it (the halt this session resolved)

By implementing !FileServer as an HLE SERVICE at the session boundary (emulation/CONSOLIDATION-2026-09-02.md §8), NOT by running efile.exe as a guest. Two declared tables drive it: db/os/hle-server.tsv (server -> the TLitC16 VA in efile.exe that names it + its TIpcSessionType, RE'd from efile's own CServer2 construction: 0x801d2a6e allocs the file server and StartL 0x801a29a1 -> RServer2::CreateGlobal 0x801a6e96 -> svc 0x7d with aMode = 2, so !FileServer is EIpcSession_GlobalSharable) and db/os/fs-op.tsv (function number -> client wrapper evidence + TIpcArgs flags). tools/gen-exec.mjs (extended, not a fourth generator) emits web/src/kernel/gen/hle-dispatch.gen.js; web/src/fs/fileserver.js answers the ops from the VFS; web/src/kernel/servers.js completes each message the device's way — the RMessageK bookkeeping of Exec::MessageComplete (ekern 0x8006ab9c: unlink from the session SDblQue, iMsgCount--, DThread+0x20--), then DThread::RequestComplete (0x8006e2ac) writes the reason into *aStatus and NKern::ThreadRequestSignal signals the request semaphore fast exec 0 blocked on — so the waiting thread readies and the run continues with no fabricated wake-up. Six execs the CRT needs alongside the send were RE'd and de-fabricated on the way (each with its handler disasm in db/os/exec-impl.tsv): 0x27 SessionShare (the CRT shares its RFs session right after Connect), 0x16 ProcessFileName, 0x17/0x51 ProcessCommandLine[Length], 0xad ProcessSecurityInfo (all reading the DProcess fields KState.attachProcess now records from the E32 header), 0xd0 ProcessGetDataParameter (-1 for an unset slot, matching EKA2L1's log), 0x11/0x12 MutexWait/Signal, 0x26 ThreadId; the old 0x51='allocator_get' and 0x26='rchunk_bottom' rows were fabrications (remembered API names, not handler reads) and are corrected. Kernel descriptor WRITE side added: web/src/kernel/descriptor.js writeDes16 reproduces K::USetLength (ekern 0x8006b9a4) so the HLE writes TDes16 replies (session path) the kernel's way. VERIFIED against C:\tools\eka2l1\traces\mgs-2026-09-02.log, an independent HLE of the same game: it sends the IDENTICAL sequence to !FileServer — Connect(-1), SetSessionToPrivate(85), then three Connect(-1)+SessionPath(14) pairs — and all eight complete KErrNone here, in that order (asserted in web/test/run-rom.mjs). Boot 6,267 -> 37,330, halting in the GAME's own code at the first call into metalgearslibdl.dll (the game lane's next slice), NOT at a file-server op.

evidence

db/os/hle-server.tsv (!FileServer, !Loader); db/os/fs-op.tsv (Connect, SetSessionToPrivate, SessionPath, PrivatePath implemented; DriveList/SetSessionPath/CreatePrivatePath/Entry named); web/src/fs/fileserver.js; web/src/kernel/servers.js (deliver/completeMessage/closeSession); web/src/kernel/descriptor.js writeDes16; db/os/exec-impl.tsv slow 0x27/0x16/0x17/0x51/0xad/0xd0/0x11/0x12/0x26 (each with handler disasm); tools/gen-exec.mjs HLE codegen + web/src/kernel/gen/hle-dispatch.gen.js; web/test/run-rom.mjs IPC assertions incl. the anti-fabrication *aStatus check and the mgs-2026-09-02.log order

Wall 23: unimplemented import metalgearslibdl@3 at 37,330 instrs — the game reached its own dlsym/dlopen shim and run-rom mapped only the firmware XIP libs, so every import into MetalGearSlibdl.dll resolved to the HALTing stub (the halt this session resolved)

By MAPPING THE GAME'S OWN RAM DLL and running its real code, not by answering ordinal 3. MetalGearSlibdl.dll lives in the container's <Game> SIS at !:\sysin\MetalGearSlibdl.dll (34,395 B, DEFLATE, uncompressedSize 57,692 -> 57,848 B flat); loader/e32.js flatImage already inflates it, and the result is byte-identical in the CODE section to samples/from-hexenwerkzeug/metalgearslibdl_decompressed.bin - the ONLY 4 differing bytes are the compressionType word at 0x1c, which the salvaged copy zeroed. So the container is the source and the salvaged fixture is not needed. Three pieces were added. (1) loader/e32.js getExports: the export directory is a FILE offset into the flat image - read at h.exportDirOffset all 1310 entries land inside [codeBase, codeBase+codeSize), read at codeOffset+exportDirOffset only 1272 do, which is what settles the interpretation. (2) cpu/bridge.js mapImage takes opts.loadBase: codeDelta = loadBase - h.codeBase is applied through the image's OWN 2,664-byte code relocation block (applyRelocs already took a delta, every caller had passed 0), and the mapped image now returns ordinalVA, the same contract cpu/rom.js gives a ROM module, so it can satisfy another image's imports. An ordinal outside the table HALTs rather than returning 0 into the caller's import slot. A relocated image carrying data/bss HALTs too - its writable-data run address is not modelled and would have to be invented. (3) loader/ramcode.js: for each of the EXE's import blocks not already satisfied by a ROM module, look for <drive>:\sysin\<key>.dll in the VFS (skipping z:, which is the XIP ROM) and map it into a RAM code slot at 0x70000000 + (n+1)*0x100000, refusing a module whose code does not fit the slot. MetalGearSlibdl.dll therefore loads at 0x70100000 with codeDelta 0x700f8000 and 1,310 exports. MEASURED: the run advanced 37,330 -> 41,695 instructions and 10 instructions retired INSIDE the DLL's mapped span (run-rom asserts ramSteps > 0, so a future regression that stops entering it fails the gate); 'no wild-branch' still passes once inSane knows the mapped RAM spans, and the !FileServer handshake is unchanged - its 8 messages still complete in the same order and its queue is empty at the halt. CROSS-REFERENCE ONLY (never the source of a committed value, CLAUDE.md §Evidence doctrine precedence): EKA2L1's own trace C: ools\eka2l1 races\mgs-2026-09-02.log creates a code chunk at 0x70100000 for metalgearslibdl.dll and 0x70000000 for mgs_ngi.exe - our independently-chosen slot allocator lands on the same address, which is what makes the two exec traces line up for tools/oracle-diff.mjs.

Wall 24: unimplemented !Loader op -1 (KConnectMessage) at 41,695 instrs — the CRT connects to the loader server once the game's own MetalGearSlibdl.dll has run, and !Loader was NAMED in db/os/hle-server.tsv but had no impl (the halt this session resolved)

By RE-ing CServerLoader::NewSessionL and answering the connect with the device's own arithmetic. The loader server's vtable is 0x801E96E8 (its constructor 0x801D0C30 does `movs r2,#0 ; blx 0x801e4bb4` = CServer2::CServer2(aPriority, aType=0) then `ldr r1,[pc,#0x194] ; str r1,[r0]`, literal at 0x801D0DD0). NewSessionL is SLOT 7 of that vtable = 0x801D2225 — slot 7 because the file server's vtable 0x801E9E8C holds the already-known CServerFs::NewSessionL 0x801D2E4B in the same slot, which pins the index instead of guessing it. Its body builds TVersion(iMajor 1, iMinor 0, iBuild [0x801D2604] = 0x625) and calls the same User::QueryVersionSupported stub 0x801e538c the !FileServer connect uses, leaving KErrNotSupported (`subs r0,r0,#5`) when unsupported. QueryVersionSupported (euser 0x801A5B27) was disassembled rather than assumed, because the answer turned on it: it reads ONLY offset 0 (iMajor) and offset 1 (iMinor) via ldrsb and returns 1 iff requested.iMajor < current.iMajor, or the majors are equal and requested.iMinor <= current.iMinor. iBuild is never read. The client asks for 0x063f0001 = 1.0.1599 against the server's 1.0.1573, so the higher client BUILD does not matter and the connect is supported: KErrNone is the device's result, not a chosen one. Structural change that came with it: a server's HLE impl no longer has to live in fileserver.js. hle-server.tsv gained an impl_file column, tools/gen-exec.mjs reads each server's own module and emits one import per module, and its 'what to do next' text now names that module — so web/src/fs/loader.js holds loaderServer and fileserver.js is not a dumping ground for every future server. User::QueryVersionSupported also stopped being duplicated: web/src/kernel/version.js owns the one predicate and both servers import it, so the rule exists once. MEASURED: 41,695 -> 41,865 instructions, the connect completes KErrNone (run-rom asserts it by server, and the !FileServer sequence is unchanged at -1,85,-1,14,-1,14,-1,14), and nothing is left outstanding (sync slot free, ipcCount 0) because all 9 sends are now answered and waited on.

Wall 25: unimplemented exec 0x9b at 41,865 instrs — issued by euser at 0x8019c2fc as soon as the !Loader session is open (the halt this session resolved)

By identifying it as the DLL-LOCK ACQUIRE, which also CORRECTS what the previous revision of this ledger asserted. That revision said exec 0x9b 'RETURNS A HANDLE to an object hanging off the current thread's owner at +0x114 ... it is an accessor'. That is WRONG on both counts, and the refutation was already sitting in this repo's own tables: exec-impl.tsv's slow 0x11 row records ekern 0x80082438 `b 0x800865a8` as DMutex::Wait, and 0x800865A8 is exactly what 0x80087C18 calls. No handle is created. WHAT IT REALLY IS. The handler 0x80089704 reads [0x80088D84] = 0xC800043C = &TheScheduler (the same constant struct.tsv's tscheduler row already rests on), +0x130 = iCurrentThread, -0x2c0 = the embedding DThread, and `ldr r4,[r0,#0x30]!` takes iOwningProcess while the WRITE-BACK leaves r0 = DThread+0x30 — so the following +0x108 is DThread+0x138, NOT DProcess+0x108 as the previous note claimed. That is an SDblQue of DLibrary pending detach; SDblQue::IsEmpty (0x8009D2D8) must return true or the handler panics KERN-EXEC 37. It then waits DProcess+0x114, the process's 'DLL$LOCK' DMutex (order 0xFE, created by DProcess::Create 0x800917C4 via K::MutexCreate 0x80082024; its sibling +0x110 is '$LOCK', order 0x48). Exec 0x9c is the release half — the identical handler with panic 43, tail-calling 0x80087C3C which signals the same mutex via 0x80085BF4 (this table's slow 0x12 already records that as DMutex::Signal). euser proves the pairing: the caller at 0x801A78FE issues svc 0x9b at 0x801A793E, tests the result, and issues svc 0x9c at 0x801A7964 on the failure path. So euser-svc-map's 'E32Loader::ProcessCreate' is refuted, not merely unverified: the handler takes no arguments, creates nothing, and is one half of a lock pair. Implemented as dll_lock_wait / dll_lock_signal over a DMutex modelled on the process (recursive on the same thread; the blocking leg for a lock held by ANOTHER thread HALTs rather than pretending to acquire), with the pending-library queue modelled as a real list so the KERN-EXEC 37/43 precondition is actually checked rather than skipped. New rows: exec-impl.tsv slow 0x9b + 0x9c, struct_field.tsv dprocess.process_lock 0x110, dprocess.dll_lock 0x114, dthread.pending_library_q 0x138. MEASURED: 41,865 -> 41,904, and the run now sends !Loader function 2 — which is what the RE predicted the lock was being taken FOR.

Wall 26: unimplemented !Loader op 2 at 41,904 instrs — the library-load request the game takes the process DLL lock for (the halt this session resolved)

By RE-ing the whole request and answering it. fn 2 is the DLL load: euser ordinal 1135 at 0x801A78FE sends it (0x801A7956 `movs r1,#2`), the flag word 0x1FC is a CONSTANT baked into euser's 3-arg TIpcArgs ctor 0x801B8B04 (`movs r1,#0xff ; adds r1,#0xfd`) rather than arithmetic on the observed value, and efile's CSessionLoader::ServiceL 0x801D1DCC dispatches it through __ARM_switch8 to 0x801D1F5A. The handler 0x801D1480 reads tldrinfo.uid1 and accepts only 0 or 0x10000079 — the literal [0x801D16E8] = 0x1000007A minus one — forcing 0x10000079; fn 1 checks 0x1000007A and appends '.EXE' instead, which is precisely what makes 2 the LIBRARY load. '.DLL' is appended only when the name carries no '.' (TLitC8 0x801E7574). TWO CORRECTIONS the adversarial pass produced, both re-derived here before adopting. (1) ServiceL accepts fn 1..14, NOT 1..15: 0x801D1DD6 `subs r0,r5,#1 ; cmp r0,#0xe ; blo` is an unsigned-lower test on (fn-1). (2) The 0x20-byte write-back is CONDITIONAL in the handler (skipped when req+0x2C is null, 0x801D1552), and is mandatory only because ServiceL always stores aMessage there. Declared: struct tldrinfo (0x20) with uid1/uid2/uid3/one_at_0c/handle/version — +0x14 and +0x1C are never written on the library path and are deliberately NOT declared; and fs-op row (!Loader, 2). MEASURED: 41,904 -> 42,112, and the first library the game asks for is hal.dll, which is in samples/firmware/5320/sys/bin, so the VFS resolves it (run-rom asserts the reply 'hal.dll -> handle ...'). ⚠ THE SEAM, stated plainly: slow exec 0x8e is NOT disassembled, so the kernel object the returned handle NAMES is unknown. This impl allocates a library object in KState the way every other kernel object here is allocated and hands back that handle. A non-empty aPath, and a library the VFS cannot find, both HALT rather than guess — efile answers those through the drive search at 0x801D19FE, also not disassembled.

Wall 27: unimplemented exec 0x9d at 42,112 instrs — RLibrary::Load's per-library attach list, issued once the loader returned hal.dll's handle (the halt this session resolved)

By implementing the per-library attach protocol from the real handlers (0x80089774 exec 0x9d, 0x800898e0 exec 0x9e) plus the handle bookkeeping euser does around it. exec 0x9d is the per-library analogue of exec 0x9f: r0 = the library handle (resolved as object TYPE 3 = ELibrary in the CURRENT THREAD's handle table, ekern 0x8006b2ac), r1 = TInt* aTotal (in: capacity via kumemget, out: count via kumemput), r2 = TLinAddr* aList. It reads the DLibrary state byte at +0x24: 3 -> nothing to attach (count 0, release the DLL lock); else it builds the library codeseg's dependency closure minus what the process already holds (0x80087bc4 -> TraverseDeps 0x800874d8 deps-first + the 0x80087560 subtraction), copies each DCodeSeg+0x24 entry point out ONE WORD each (0x80088798), sets the state to 2, and KEEPS the DLL lock held for the client's EDllProcessAttach; count > capacity is a kernel panic 39 (HALT, exactly as exec 0x9f). exec 0x9e commits the state 2 -> 3 (0x80089960; panics KERN-EXEC 41 if it was not 2) and releases the DLL lock via DMutex::Signal (0x80089988) that 0x9d kept. load_library now maps the resolved library as a codeseg (hal.dll is XIP, so addRom on code_address-HDR reads its TRomImageHeader already present in the ROM; a RAM library HALTs with the recipe to mount the launcher Sys/Bin) and records its state (1 = needs attach) and codeseg for 0x9d. The state-1-vs-3 determinant is DCodeSeg+0x54 bit 0x02000000 (ekern 0x80089118 create-branch), established as 1 for the loaded DLLs by TWO techniques: the create-branch disasm, and the EKA2L1 traces where every Loader::LoadLibrary is followed by SVC 0x9d then 0x9e (library_attach -> library_attached), the attach protocol state 3 would skip. exec 0x69 (HandleClose) gained ELibrary (TObjectType 3, ekern 0x80083bc8 special-cases only EChunk): euser duplicates the loader's thread-owned handle (exec 0x6d) to the owner the caller keeps, then closes the loader's copy, so the DLibrary survives under the duplicate; the LAST handle closing (the un-modeled EDllProcessDetach path on DThread+0x138) HALTs rather than silently dropping it. hal.dll is a leaf (no TRomDllRefTable), so its published list is exactly [hal's entry point 0x80683968], count 1. MEASURED: 42,112 -> 42,843, halting at the next unimplemented exec 0xe; run-rom asserts the frontier, instruction count and pc.

evidence

db/os/exec-impl.tsv slow 0x9d library_attach + slow 0x9e library_attached (each with the full handler disasm trail); web/src/kernel/exec-impl.js library_attach / library_attached / signalDllLock + handle_close ELibrary=3; web/src/kernel/codeseg.js attachLibrary + loaded[] + _attach(cs,target); web/src/fs/loader.js load_library maps the codeseg and sets state 1; CROSS-REFERENCE ONLY (CLAUDE.md §Evidence doctrine, EKA2L1): C: oolseka2l1 racesmgs-2026-09-02.log line 326->329 shows SVC 0x9b / LoadLibrary / 0x9d library_attach / 0x9e library_attached in that order

Wall 28: unimplemented exec 0xe at 42,843 instrs — RLibrary::Lookup, issued after hal.dll loaded (the halt this session resolved)

By RE-ing the handler (ekern 0x80082348) and the euser wrapper (0x801a414a): exec 0xe = Exec::LibraryLookup(TInt aHandle, TInt aOrdinal) = RLibrary::Lookup. The handler resolves the handle as object TYPE 3 = ELibrary (0x80082378 mov r1,#3 / bl 0x8006b410), takes its DCodeSeg at +0x30 (0x8008237c; null returns 0), and calls the codeseg's address-of-export virtual (vtable+0x14, 0x8008238c) with the ordinal, returning that export VA (bit 0 = Thumb, directly callable), 0 for an out-of-range ordinal. euser RLibrary::Lookup 0x801a414a reads the RLibrary's iHandle at +0 and panics USER 116 for aOrdinal < 0. euser-svc-map's 'CBufBase::Read' is REFUTED (the defective column); EKA2L1 (cross-reference) names SVC 0xe library_lookup. The model reads the ordinal's VA from the ROM export directory (codeseg.exportVA using romimageheader.export_dir_address/count); a RAM library's relocated exports HALT. MEASURED: 42,843 -> 42,965, and 0xe returned hal.dll ordinal 1 = 0x80683ae3 exactly (hal has 5 exports at export_dir_address 0x80684494) — the dlsym/lookup step of MetalGearSlibdl's dlopen('hal.dll') chain. The run then halts where the stub calls dlclose: the LAST handle to hal.dll closing, i.e. the library DETACH path (the next slice).

evidence

db/os/exec-impl.tsv slow 0xe library_lookup (handler + euser wrapper disasm); web/src/kernel/exec-impl.js library_lookup; web/src/kernel/codeseg.js exportVA + the export_dir_address/count accessors; CROSS-REFERENCE ONLY (CLAUDE.md §Evidence doctrine, EKA2L1): the traces name SVC 0xe library_lookup

Wall 29: unimplemented-detach at 42,965 instrs — the last handle to hal.dll closing (dlclose), the library DETACH path (the halt this session resolved)

By implementing the library DETACH, the mirror of the 0x9d/0x9e attach, from the real handlers: ekern 0x80089a84 exec 0xa0 (library_detach), 0x80089bb8 exec 0xa1 (library_detached), and DLibrary::Close 0x80088cb0..0x80088d08. handle_close, on the LAST handle to a library (the model's proxy for iAccessCount reaching 0), sets the DLibrary state to 4 and AddLast's it onto the thread's pending-library-detach queue (DThread+0x138 = ks.thread.pendingLibraryQ), returning TObjectType 3. exec 0xa0 drains that queue: empty gives -25 KErrEof (the routine post-close case every handle_close then sees), else it PEEKS the head, publishes its codeseg detach entry point (hal is a leaf with no TRomDllRefTable, so its own entry point 0x80683968), sets the state to 5, and returns 0 with the count in *aTotal; the euser runner 0x801a617c then calls each published entry with r0=3 (EDllProcessDetach) and issues exec 0xa1. exec 0xa1 requires the queue non-empty (panic 42), DEQUEUEs the head, resets its state to 1, drops the codeseg from ks.codesegs.loaded (a later reload re-attaches it), and returns -25 when the queue empties (else 0, so the runner loops to 0xa0). The DLL-lock precondition (0xa0/0xa1 panic 40/42 when the lock is unheld) is NOT modeled, exactly as the -25 empty-queue path never modeled it: the routine drains run with the lock unheld in this model (no 0x9b precedes them). EKA2L1 (cross-reference) names SVC 0xa0/0xa1 library_detach/library_detached and its traces show the 0x69 handle_close / 0xa0 / 0xa1 order. MEASURED: 42,965 -> 65,163 (+22,198), past the ENTIRE DLL attach/lookup/detach machinery. With hal.dll fully loaded, looked up and detached, the boot runs the CRT's remaining init and the game's Marmalade S3E loader stub, which reaches the FILESYSTEM SCAN (opendir/RFs::Entry for *.s3e under the session path) and halts at the first unimplemented !FileServer file op. run-rom's IPC assertions were updated to this frontier: 13 synchronous sends (was 10), one message outstanding mid-delivery (the halting Entry, *aStatus still KRequestPending), and the extra SessionPath(14)/PrivatePath(86) ops the scan issues.

evidence

db/os/exec-impl.tsv slow 0xa0 library_detach + slow 0xa1 library_detached (each with the full handler disasm trail) + handle_close note; web/src/kernel/exec-impl.js library_detach / library_detached / handle_close detach-queue; web/test/run-rom.mjs IPC assertions updated to the op-22 frontier; CROSS-REFERENCE ONLY (CLAUDE.md Evidence doctrine, EKA2L1): the traces name SVC 0xa0 library_detach / 0xa1 library_detached and show the 0x69 / 0xa0 / 0xa1 order

Wall 30: op-22 (!FileServer RFs::Entry) at 65,163 instrs — DE-FABRICATED: reached only by swallowing three main-thread panics

NOT a real halt. slow 0x72 was mis-modelled as 'exec72_setname' returning KErrNone for (KCurrentThreadHandle, EExitPanic), which swallowed the three User::Panic calls mgs_ngi's main thread issues (~7525 'FSCLIENT panic' 24, ~7779 'USER' 9, ~7897 'FSCLIENT panic' 1) and let the boot run ~57k instrs past them to the RFs::Entry scan for *.s3e. Implementing 0x72 as the real Exec::ThreadKill (ekern 0x8008d5a0; EKA2L1 names SVC 0x72 thread_kill) makes it HALT on the first panic, dropping the run 65,163 -> 7,525 — a de-fabrication, the same move the null-branch history already records as progress (5963 -> 340). Everything the swallowed run reached (hal.dll load/lookup/attach/detach, exec 0x9d/0x9e/0xe/0xa0/0xa1, !FileServer connect/85/14/86, op-22) is real, evidenced code and stays in the engine — it is simply downstream of a panic that must be diagnosed first. See current.

evidence

db/os/exec-impl.tsv slow 0x72 thread_kill (handler 0x8008d5a0 + euser wrapper 0x8019c71c + User::Panic ord 650 0x801a599a); web/src/kernel/exec-impl.js thread_kill; run-rom.mjs asserts the new 7,525 halt; MEASURED 65,163 -> 7,525

Wall 31: guest panic FSCLIENT 24 at 7,525 instrs — TParsePtrC over RProcess::FileName() returned KErrBadName

OUR wrong answer, exactly where the ledger suspected: Exec::ProcessFileName (slow 0x16) hands the name back in an EIGHT-bit descriptor and we wrote it with writeDes16. euser RProcess::FileName 0x801a43ba builds a TPtr8 over aFileName.Ptr()+0x100 with aMaxLength 0x100 (0x801a43cc `blx 0x8019ddf4` = Ptr(), 0x801a43d0 `adds r1,r0,#7 ; adds r1,#0xf9`, 0x801a43da `bl 0x801ade9e` = the TPtr8 ctor), passes THAT to svc 0x16 (0x801a43e2, stub 0x8019bee8 `svc #0x16`), and then widens it with TDes16::Copy(const TDesC8&) 0x801aeda8, whose loop is 0x801aedce `ldrb r1,[r4] ; adds r4,r4,#1 ; strh r1,[r0] ; adds r0,r0,#2`. So our UTF-16 bytes came back widened byte by byte as "e\0:\0\\0s\0..." and EFSrv TParseBase::DoParse 0x801f0cfa read name[1] == 0x0000 instead of 0x3a in the drive component 0x801f0e44 (0x801f0e68 `cmp r0,#0x3a ; bne`), the path component 0x801f0ec2 saw name[0] == 0x65 not 0x5c (0x801f0ee4 `cmp r0,#0x5c ; bne`) and skipped, and the name component 0x801f0f48 located a 0x5c (0x801f0f5c) so it returned KErrBadName -28 (0x801f0f60 `movs r7,#0x1b ; mvns r7,r7`, taken at 0x801f0fea), which the TParsePtrC ctor 0x801f1120 turns into FSClientPanic(24) at 0x801f1162 `movs r0,#0x18 ; bl 0x801f0a00`. Writing 8 bits makes the drive parse succeed, so RFs::SetSessionToPrivate(EDriveE) is sent and the boot runs 7,525 -> 68,015 instrs with ZERO Exec::ThreadKill anywhere in it.

evidence

ekern Kern::KUDesPut 0x80098088 sets the user length from the SOURCE length word (0x80098098 `bic r4,r1,#0xf0000000`, 0x800980c0 `bl 0x8006b9a4` = K::USetLength) and copies that many BYTES (0x800980d0 `bl 0x8006bec4`, whose tail is 0x8006beec `ldrbpl r3,[r1],#1 ; strbtpl r3,[r0],#1`); its scratch is a TBuf8 (0x8009976c `mov r2,#0x30000000 ; str r1,[r0,#4] ; str r2,[r0]`) sized 0x100 by 0x80082520 `mov r1,#0x100`, and DCodeSeg::AppendFullFileName 0x80088a20 appends into it 8-bit (0x80088a38 `mov r1,#0x40` = TDes8::Append(TChar)). db/os/exec-impl.tsv slow 0x16 and slow 0x17; web/src/kernel/exec-impl.js writeDes8; web/test/run-rom.mjs asserts the session drive is E.

A retired diagnosis, kept verbatim: null-branch at 5963 instructions

thread fn 0x82e462a8 (lr=0x82e531a7)

KEPT VERBATIM as the record of a diagnosis that was RIGHT about the symptom and WRONG about the mechanism; the resolution is the first entry of `history`. DIAGNOSIS CONVERGED -> the root is STATIC_CALL_LIST returning -25 (skips C++ ctors). Full trace: the null member [0x3ffc0094] is `str r0,[this+0x84]` from a factory `blx 0x82e52220`. BOTH heap creators inside the factory SUCCEED (0x801a67c8 chunk-create ret 0; 0x801b05bc RHeap-in-chunk ret 0 — the chunk close 0x69 is normal ownership-transfer, not failure). The factory's LAST exec is 0xa0 (static_call_list) -> -25, after which it FREES (RHeap::Free 0x801a1788) and returns NULL. So 0x82e52220 is the C++ STATIC-INIT RUNNER: with -25 ('no ctors') it constructs nothing and returns null, so the member that a ctor should create stays null -> later thunk @0x82e462a0 derefs null -> bx 0. CORRECTS the earlier 'static_call_list=-25 is faithful' assumption (the comment already flagged: MUST revisit). FIX = the E32 static-constructor subsystem. OPEN/subtle EKA2 semantics to RE: does static_call_list return per-process DLL entry points (EDllProcessAttach) and/or the EXE codeseg ctor list? Our XIP firmware DLLs' ctors normally ran at DEVICE boot (which we skip) — so their initialised globals may be the real gap, OR the game exe's own ctors must run via this list. Determine what populates [0x3ffc0094] (which module's ctor) before implementing; extract the ctor/entry list from the E32 images. Do NOT fabricate the list. REFUTED EXPERIMENT (do not repeat): returning the DLL header.entryPoint+8 addresses (0x80199080 euser, 0x82e63fc0 libc, ...) from static_call_list and letting the runner blx them with r0=3 -> boot HALTS EARLIER at a null branch (1614 instrs), member still null. Those addresses are `bx r12` interworking veneers (need r12 set up), so they are NOT the call target. AUTHORITATIVE ROOT (from ekern 0x80089a84): the handler loads r4=mvn #0x18 = -25 as default, then walks the THREAD'S CODESEG INIT-LIST at [thread+0x138] (bl 0x8009d2d0 tests it); returns -25 exactly when that list is EMPTY/exhausted, else takes the next codeseg and calls 0x80087bbc (queries_call_list) to fill *aList + sets *aTotal=count (validated <= capacity, panic 0x27/0x28 otherwise). SO: our model never populates [thread+0x138] — we map the DLLs but never ATTACH them as codesegs to the thread/process, so the list is empty -> -25 -> no ctors -> null member. This is the codeseg/module-init subsystem. PLAN: (1) model codesegs (each loaded module = {entry, deps}) and populate the thread's codeseg-init list in dependency order during load; (2) implement static_call_list to iterate it and return each codeseg's call list (queries_call_list = entry points incl deps), -25 when exhausted; (3) RE the real entry-point/E32Dll call convention (header.entryPoint+8 is a `bx r12` veneer that needs r12 — NOT directly callable; find how the real loader invokes it). This needs careful design; do NOT fake the list. Reminder: euser runtime pc -> disasm standalone at pc-8.

Metal Gear Solid Mobile: object layouts

Projected from db/game/mgs/*.tsv — 15 structs, 32 fields, 19 watches, 1 transforms. Every address is an HLE VA of the S3E image; the size, at, vtable, offset and len columns are derived by simulating the instruction range each derive column names, and drift fails the gate.

Image

imagepathload_base
mgs_ngi.xe3usamples/mgs/extracted/game/drive_!/private/20010925/mgs_ngi.xe3u0x100000

Structs

structclassbasesizeatvtablederiveevidencenote
world_holder0x100x1f2338size=extent; at=val 0x1a9ce8 0x1a9ce8 r00x1a9ce8 `ldr r0,[pc,#4]` pool 0x1a9cf0 = 0x1f2338, `ldr r0,[r0,#4]` returns the CWorld (every radar function calls 0x1a9ce8); 0x1a9d02 `ldr r1,[pc,#8]` (pool 0x1a9d0c) `str r0,[r1,#4]` stores the freshly constructed CWorld; 0x1a8f62 `ldr r4,[pc,#0x10c]` (pool 0x1a9070) then reads +8 and stores +0xcthe .data global that owns the CWorld pointer and the cached IwHashString("Snake")
cworldCWorld0x2780x1ead44size=new 0x1a9cf4; vtable=val 0x1aa37a 0x1aa37a r1factory 0x1a9cf4..0x1a9d04 `movs r0,#0x4f ; push ; lsls r0,r0,#3 ; bl 0x1c5974 ; bl 0x1aa374 ; ldr r1,[pc,#8] ; str r0,[r1,#4]`; ctor 0x1aa374: 0x1aa37a `ldr r1,[pc,#0x20c]` 0x1aa37c `str r1,[r0]`the level: entity array, the tracked player entity
game_registry0x1c*0x1f0578size=extent; at*=val 0x1aa472 0x1aa472 r00x1aa472 `ldr r0,[pc,#0x128]` (pool 0x1aa59c = 0x1f0578) `ldr r0,[r0]` 0x1aa47a `str r4,[r0,#0x14]` registers the CWorld; 0x1ab0e2 `ldr r0,[pc,#0x50]` (pool 0x1ab134) `ldr r0,[r0]` 0x1ab0e6 `str r4,[r0,#0x18]` registers the CCamerapointer at 0x1f0578 to the object holding the live world and camera
centity0x64size=extentthe CIwEnt-level view of every element of cworld.entities; 0x189e40..0x189e4c `ldr r1,[r0] ; ldr r1,[r1,#0x64] ; bl 0x1012ce` calls vtable slot 25 (class-family id) and compares with [0x1f14c8] (guard family) / [0x1f22ac] (security camera family)the pointer in the world array is the primary object pointer; its vtable identifies the class (see the vtable column of the subclasses)
ciwframeCIwFrame0x400x1eb810size=new 0x1ae730; vtable=val 0x1b19aa 0x1b19aa r10x1ae730 `movs r0,#0x40 ; bl 0x1c5974 ; bl 0x1b19a4` (CPickup allocates its frame); ctor 0x1b19a4: 0x1b19aa `ldr r1,[pc,#0xc]` 0x1b19b0 `str r1,[r0]`; slot 14 (0x1eb848) = 0x1b18a1 `adds r0,#0x10 ; bx lr` returns the world matrixMarmalade CIwFrame; CIwFrameLW (vtable 0x1e3648) answers slot 14 with +0x40 instead (0x15d3df..0x15d3f4)
ciwmat0x30size=extentcopy 0x1a8498: `movs r2,#0x24 ; bl 0x100b3c` (memcpy import) then 0x1a84a4..0x1a84ae `ldr/str` +0x24, +0x28, +0x2c; 0x15a1fe..0x15a20e `muls ; muls ; adds ; muls ; adds ; asrs r4,r0,#0xc` = row . vector with 12 fractional bitsMarmalade CIwMat: 3x3 rotation in 1/4096 units, then the CIwVec3 translation in world units
ccharCCharactercentity0x7dc0x1e32d0size=extent; vtable=val 0x15bf02 0x15bf02 r1base ctor 0x15befc (r1 = name, r2 = kind 1 guard / 2 snake): 0x15bf02 `ldr r1,[pc,#0x118]` (pool 0x15c01c) 0x15bf06 `str r1,[r0]`; sub-objects at +0x638 (0x15bf16..0x15bf20 CIwFrame ctor 0x1b19a4), +0x678 (0x15bf24..0x15bf2c ctor 0x1853c4), +0x6a8 (0x15bf30..0x15bf38 ctor 0x188814), +0x770 (0x15bf3c..0x15bf44 `movs r7,#0x77 ; lsls r7,r7,#4 ; adds r0,r0,r7 ; bl 0x1bc230` CViewCone ctor)CCharacter: the layout guard and snake share; size is the extent of the evidenced fields, the allocation is the subclass's
cchar_guardCCharGuardcchar0xac80x1e2fc4size=new 0x1d5f72; vtable=val 0x15506e 0x15506e r1class factory 0x1d5f72..0x1d5f80 `ldr r0,[pc,#0x14]` (pool 0x1d5f88) `bl 0x1c5974 ; bl 0x155064 ; adds r0,#0xc` (hands the +0xc CIwManaged sub-object to the resource system); ctor 0x155064: `movs r2,#1 ; adr r1 ; bl 0x15befc` then 0x15506e `ldr r1,[pc,#0xc8]` 0x155072 `str r1,[r0]`enemy soldier; extends cchar
cchar_snakeCCharSnakecchar0xc540x1e314csize=new 0x1d5fe6; vtable=val 0x15a27e 0x15a27e r1class factory 0x1d5fe6..0x1d5ff4 `ldr r0,[pc,#0x14]` (pool 0x1d5ffc) `bl 0x1c5974 ; bl 0x15a274 ; adds r0,#0xc`; ctor 0x15a274: `movs r2,#2 ; adr r1,#0xa4 ; bl 0x15befc` then 0x15a27e `ldr r1,[pc,#0xa8]` 0x15a282 `str r1,[r0]`the player; extends cchar
cviewconeCViewCone0x6c0x1ec17csize=new 0x16fd0e; vtable=val 0x1bc236 0x1bc236 r10x16fd0e `movs r0,#0x6c ; bl 0x1c5974 ; bl 0x1bc230` (CSecurityCam allocates its cone); ctor 0x1bc230: 0x1bc236 `ldr r1,[pc,#0x2c]` 0x1bc23c `str r1,[r0]`, `str r4,[r0,#0xc]`, sub-object at +0x34 (0x1bc240 ctor 0x1b81e0), `str r1(4),[r0,#0x68]`, `strb 0/1` at +0x64/+0x65, `lsls r1,r2,#0xa` (0x400) stored at +0x2c and +0x30a guard's or security camera's vision cone; CGuiMapViewCones (0x189d9c) draws it on the radar
cviewcone_type0x8size=extent0x189fa8..0x189fac `ldr r0,[r5,#0xc] ; adds r1,#0x80 ; ldr r0,[r0,#4]` then `ldr r1,[r1,#4] ; bl 0x101074` divides it by cguimap.scalethe record cviewcone.type points at; only its range has been read
csecuritycamCSecurityCamcentity0x6cc0x1e5110size=new 0x18c80c; vtable=val 0x16fcae 0x16fcae r1factory 0x18c80c `push {r4-r6,lr} ; movs r5,r0 ; ldr r0,[pc,#0x1c]` (pool 0x18c830 = 0x6cc) `bl 0x1c5974 ; bl 0x16fca8`; ctor 0x16fca8: 0x16fcae `ldr r1,[pc,#0xcc]` (pool 0x16fd7c); CGuiMapViewCones 0x189ea4..0x189ece: after the family-id compare with [0x1f22ac] it reads [ent+0x110] (cone pointer) and [ent+0xc4] (facing angle)CSecurityCam entity; its cone is a separately allocated cviewcone (0x16fd0e..0x16fd20)
cpickupCPickupcentity0x1040x1eb344size=new 0x1d63d2; vtable=val 0x1ae716 0x1ae716 r1class factory 0x1d63d2..0x1d63e4 `movs r0,#0xff ; push ; adds r0,r0,#5 ; bl 0x1c5974 ; bl 0x1ae710 ; adds r0,#0xc`; ctor 0x1ae710: base 0x1b0fd4, 0x1ae716 `ldr r1,[pc,#0x3c]` 0x1ae718 `str r1,[r0]`, then 0x1ae730..0x1ae73e allocates a CIwFrame and stores it through SetFrame 0x1a6e14item lying in the level; position through centity.frame like every entity
ccameraCCamera0xe4*0x1f13cc0x1ead90size=new 0x1aafa2; at*=val 0x1aafac 0x1aafac r1; vtable=val 0x1ab0a0 0x1ab0a0 r1factory 0x1aafa2..0x1aafae `movs r0,#0xe4 ; bl 0x1c5974 ; bl 0x1ab098 ; ldr r1,[pc,#8] (pool 0x1aafb8) ; str r0,[r1]`; ctor 0x1ab098: base 0x1b94e0(this,1), 0x1ab0a0 `ldr r1,[pc,#0x84]` 0x1ab0a2 `str r1,[r0]`, SetName "CAMERA" (0x1ab0dc), registers itself at game_registry+0x18 (0x1ab0e6)the game camera singleton; the pointer lives at 0x1f13cc
cguimapCGuiMap0x1100x1eaf8csize=new 0x128048; vtable=val 0x1abdde 0x1abdde r1factory 0x128048 `movs r0,#0xff ; push ; adds r0,#0x11 ; bl 0x1c5974 ; bl 0x1abdd4` = 0x110; ctor 0x1abdd4: 0x1abdde `ldr r1,[pc,#0xa8]` (pool 0x1abe88) 0x1abde4 `str r1,[r0]`, then loads three int16 defaults (pool 0x1abe8c -> 0x1edbca) into +0x78/+0x7c/+0x80 (0x1abdf8..0x1abe0a) and 1 into +0x84 (0x1abe0c..0x1abe0e), then sub-objects at +0x88 and +0xcc (0x1abe10..0x1abe18, ctor 0x165ff8, 0x44 B each); world->pixel 0x1c208c reads +0x78, +0x80, +0x84the radar GUI element that owns the world->pixel transform (transforms.tsv radar_map); the live instance is a GUI-tree child, no static root is pinned

Fields

structfieldoffsettypelenderiveevidencenote
world_holderworld0x4cworld*offset=ea 0x1a9cea 0x1a9cea0x1a9ce8 `ldr r0,[pc,#4]` (= 0x1f2338) 0x1a9cea `ldr r0,[r0,#4]`; written 0x1a9d04 `str r0,[r1,#4]`CWorld*; null until the world is constructed
world_holdersnake_hash_cached0x8u32offset=ea 0x1a8f66 0x1a8f660x1a8f66..0x1a8f6e `ldr r0,[r4,#8] ; lsls r0,r0,#0x1f ; bne` (bit 0), r4 = 0x1f2338 from 0x1a8f62bit 0 = snake_hash is valid
world_holdersnake_hash0xcu32offset=ea 0x1a8f82 0x1a8f820x1a8f7c..0x1a8f82 `adr r0 ("Snake") ; bl 0x11f238 ; str r0,[r4,#0xc]`; compared at 0x1a8f8cIwHashString("Snake"); the loader compares requested entity names against it
cworldname_hash0x4u32offset=ea 0x15eee4 0x15eee40x1aa466..0x1aa470 `ldr r0,[pc,#0x128] ; adr r1 ("WORLD") ; str r4,[r0,#4] ; movs r0,r4 ; bl 0x15eeda`; 0x15eeda..0x15eee4 `movs r4,r0 ; movs r0,r1 ; bl 0x11f238 ; str r0,[r4,#4]`the CWorld is the CIwManaged named "WORLD" (r4 at 0x1aa464 is the object base after the ctor rewinds its +0x268 cursor)
cworldentities0x84centity**offset=ea 0x18415a 0x18415c0x184156..0x18415c `bl 0x1a9ce8 ; adds r0,#0x84 ; ldr r1,[r0]`; 0x189df0..0x189df6 the sameCIwArray data pointer: array of centity* (primary pointers)
cworldentity_count0x88u32offset=ea 0x18415a 0x18415e0x18415a..0x184164 `adds r0,#0x84 ; ldr r1,[r0] ; ldr r0,[r0,#4] ; lsls r0,r0,#2 ; adds r0,r1,r0` = end = data + count*4elements the radar iterates
cworldsnake0x1d4cchar_snake*offset=ea 0x1a8fb6 0x1a8fbawritten 0x1a8fb6..0x1a8fba `movs r0,#0xff ; adds r0,#0xd5 ; str r4,[r0,r6]` (r6 = the world, r4 = the entity found by the hash lookup 0x18e062 minus 0xc at 0x1a8fa6); read 0x1bfb2e..0x1bfb38 `bl 0x1a9ce8 ; adds r0,#0xff ; adds r0,#0xc1 ; ldr r0,[r0,#0x14] ; cmp r0,r7` and 0x189dd6..0x189de0the player entity (CCharSnake*); the radar treats it as the focus entity
game_registryworld0x14cworld*offset=ea 0x1aa47a 0x1aa47a0x1aa476..0x1aa47a `ldr r0,[r0] ; movs r1,#0xb8 ; str r4,[r0,#0x14]`
game_registrycamera0x18ccamera*offset=ea 0x1ab0e6 0x1ab0e60x1ab0e4..0x1ab0e6 `ldr r0,[r0] ; str r4,[r0,#0x18]`
centityvtable0x0ptroffset=ea 0x155072 0x155072ctors 0x155072 (guard), 0x15a282 (snake), 0x1ae718 (pickup), 0x16fcae.. (security camera) `str r1,[r0]`the class of an entity is its vtable address (structs.tsv vtable column)
centityframe0x14ciwframe*offset=ea 0x1a7bc4 0x1a7bc4SetFrame 0x1a6e14 -> 0x1a7baa: 0x1a7bb6 `ldr r0,[r4,#0x14]` (old frame) 0x1a7bc4 `str r5,[r4,#0x14]`; read 0x1c1756 `ldr r0,[r0,#0x14]` and 0x1c1776 `ldr r4,[r0,#0x14]`ciwframe* (or CIwFrameLW*); the object's transform lives here
centityflags0x60u32offset=ea 0x1ae742 0x1ae7420x1ae742..0x1ae74c (CPickup ctor) `ldr r1,[r4,#0x60] ; movs r0,#1 ; lsls r0,r0,#0xe ; orrs r1,r0 ; str r1,[r4,#0x60]`bit 14 set by the pickup ctor; other bits not RE'd
ciwframevtable0x0ptroffset=ea 0x1c1758 0x1c17580x1c1756..0x1c175c `ldr r0,[r0,#0x14] ; ldr r1,[r0] ; ldr r1,[r1,#0x38] ; bl 0x1012ce` (slot 14 call)0x1eb810 = CIwFrame (matrix at +0x10), 0x1e3648 = CIwFrameLW (matrix at +0x40)
ciwframemat_world0x10ciwmatoffset=val 0x1b18a0 0x1b18a0 r0CIwFrame slot 14 = 0x1b18a0 `adds r0,#0x10 ; bx lr`; slot 15 0x1b18a4..0x1b18aa `movs r4,r0 ; adds r0,#0x10 ; bl 0x1a8498` copies into itthe entity's world transform (a CIwFrameLW keeps it at +0x40: 0x15d3f2)
ciwmatm0x0fix12[9]0x24offset=val 0x1a849a 0x1a849a r6; len=val 0x1a849e 0x1a849e r20x1a849a..0x1a84a0 `movs r6,r0 ; movs r5,r1 ; movs r2,#0x24 ; bl 0x100b3c` (memcpy import); 0x15a1fe..0x15a20e dot product `asrs #0xc`3x3 rotation, row-major, 1/4096 units
ciwmatt0x24i32[3]0xcoffset=ea 0x1a84a4 0x1a84a4; len=span 0x1a84a4 0x1a84ac0x1a84a4..0x1a84ae `ldr r0,[r5,#0x24] ; str r0,[r6,#0x24] ; ... [#0x28] ... [#0x2c]`; 0x1c1764 `adds r0,#0x24` returns it; 0x1aa330..0x1aa336 stores x/y/ztranslation = world position; x,z are the ground plane, y is height (0x189e74 `ldr r7,[r0,#4]` compares heights)
ccharframe_embedded0x638ciwframeoffset=val 0x15bf16 0x15bf1c r00x15bf16..0x15bf20 `movs r4,#0xc7 ; adds r1,#0x14 ; lsls r4,r4,#3 ; adds r0,r0,r4 ; str r1,[r2,#0x34] ; bl 0x1b19a4`a CIwFrame built into the character; whether centity.frame points here is not evidenced
ccharfacing_angle0x754angle12offset=ea 0x189e5a 0x189e620x189e5a..0x189e62 `movs r1,#0x1d ; lsls r1,r1,#6 ; str r0,[sp,#8] ; adds r0,r0,r1 ; ldr r0,[r0,#0x14]` then 0x189e64..0x189e6e `(x + 0x800) & 0xfff`the direction the radar cone is drawn from (guard and snake alike)
ccharview_cone0x770cviewconeoffset=val 0x189e54 0x189e58 r50x189e54..0x189e58 `movs r1,#0x77 ; lsls r1,r1,#4 ; adds r5,r0,r1`; constructed 0x15bf3c..0x15bf44embedded cviewcone
cviewconetype0xccviewcone_type*offset=ea 0x1bc23a 0x1bc23actor 0x1bc23a `str r4,[r0,#0xc]` (null); 0x189fa8 `ldr r0,[r5,#0xc]` then cviewcone_type.rangepointer to the cone's type record
cviewconehalf_angle0x2cangle12offset=ea 0x189f3e 0x189f3e0x189f3e `ldr r7,[r5,#0x2c]`, edges drawn at facing+r7 (0x189fb6 `adds r0,r6,r7`) and facing-r7; ctor 0x1bc258..0x1bc25a `lsls r1,r2,#0xa ; str r1,[r0,#0x2c]` (0x400 = 90 degrees)symmetric half-angle of the drawn cone
cviewconeactive0x64u8offset=ea 0x189f2a 0x189f2c0x189f2a..0x189f30 `adds r0,#0x60 ; ldrb r2,[r0,#4] ; cmp r2,#0 ; beq`; ctor 0x1bc254 `strb r4,[r1,#4]` (0)0 = the cone is not drawn
cviewconevisible0x65u8offset=ea 0x189f2a 0x189f320x189f32..0x189f36 `ldrb r0,[r0,#5] ; cmp r0,#0 ; bne`; ctor 0x1bc256 `strb r2,[r1,#5]` (1)both bytes gate the draw
cviewconemode0x68u32offset=ea 0x189f3c 0x189f3c0x189f3c..0x189f4a `ldr r0,[r5,#0x68] ; cmp r0,#3 ; beq ; cmp r0,#4 ; beq ; cmp r0,#8 ; bne` selects the drawing frame this+0x90 / +0x118 / +0xd4; ctor 0x1bc24c `str r1(4),[r0,#0x68]`3, 4, 8 are the values the radar distinguishes; which alert level each is has not been RE'd
cviewcone_typerange0x4i32offset=ea 0x189fa8 0x189fac0x189fa8..0x189fb0 `ldr r0,[r5,#0xc] ; adds r1,#0x80 ; ldr r0,[r0,#4] ; ldr r1,[r1,#4] ; bl 0x101074`view range in world units, divided by cguimap.scale for the radar
csecuritycamfacing_angle0xc4angle12offset=ea 0x189ecc 0x189ece0x189ecc..0x189ed8 `movs r0,#0xc4 ; ldr r0,[r0,r7] ; movs r1,#1 ; lsls r1,r1,#0xb ; adds ; lsls ; lsrs` = (x + 0x800) & 0xfff
csecuritycamview_cone0x110cviewcone*offset=ea 0x189ec6 0x189eca0x189ec6..0x189eca `movs r0,#0xff ; adds r0,#0x11 ; ldr r5,[r0,r7]`cviewcone* (allocated 0x16fd0e..0x16fd20)
ccameramat0xacciwmatoffset=val 0x1aaf84 0x1aaf88 r00x1aaf84..0x1aaf8a `movs r0,r4 ; ldr r1,[pc,#0x14] (pool 0x1aaf9c = 0x1f4470) ; adds r0,#0xac ; bl 0x1a8498` (12-word copy)the camera's CIwMat, reset from the .bss matrix at 0x1f4470 (zero at load, written at startup); whether IwGx takes its view matrix from here is not evidenced
cguimapoffset_x0x78i32offset=ea 0x1c2092 0x1c20920x1c2090..0x1c2096 `ldr r1,[r1] ; ldr r2,[r0,#0x78] ; movs r4,r0 ; adds r2,r1,r2` (r1 = pos.x); ctor default 0x1abdf8..0x1abdfcadded to world x before scaling
cguimapoffset_y0x7ci32offset=ea 0x1abdfe 0x1abe02ctor 0x1abdfe..0x1abe02 `movs r4,#2 ; ldrsh r4,[r2,r4] ; str r4,[r0,#0x7c]`the y (height) member of the offset vector; not read by the world->pixel function
cguimapoffset_z0x80i32offset=ea 0x1c2098 0x1c20a60x1c2098 `adds r4,#0x80` then 0x1c20a4..0x1c20a8 `ldr r0,[r5,#8] ; ldr r1,[r4] ; adds r0,r0,r1` (r5 = the position, +8 = z); ctor 0x1abe08..0x1abe0aadded to world z
cguimapscale0x84i32offset=ea 0x1c2098 0x1c209a0x1c2098..0x1c209a `adds r4,#0x80 ; ldr r1,[r4,#4]` and 0x1c20aa, divisor of 0x109a3c (rounded signed divide); ctor 0x1abe0c..0x1abe0e stores 1world units per map pixel

Watches

labeladdrtypetransformmarkerevidence
worlds3e+0xF2338 +0x4cworld*world_holder.world
entity_counts3e+0xF2338 +0x4 * +0x88u32cworld.entity_count
entitiess3e+0xF2338 +0x4 * +0x84centity*[entity_count]cworld.entities: the CIwArray data pointer, entity_count elements
entity_classentities[i] * +0x0ptrcentity.vtable; the value is the vtable column of structs.tsv: cchar_guard, cchar_snake, cpickup, csecuritycam
entity_frameentities[i] * +0x14ciwframe*centity.frame
entity_posentities[i] * +0x14 * +0x10 +0x24i32[3]radar_mapdotciwframe.mat_world ciwmat.t; valid when ciwframe.vtable is 0x1eb810 (CIwFrame); a CIwFrameLW frame (0x1e3648) keeps the matrix at +0x40 (0x15d3f2)
snakes3e+0xF2338 +0x4 * +0x1d4cchar_snake*cworld.snake
snake_poss3e+0xF2338 +0x4 * +0x1d4 * +0x14 * +0x10 +0x24i32[3]radar_mapplayercworld.snake centity.frame ciwframe.mat_world ciwmat.t; same frame-class caveat as entity_pos
snake_angles3e+0xF2338 +0x4 * +0x1d4 * +0x754angle12radar_mapheadingcworld.snake cchar.facing_angle
guard_angleentities[i] * :cchar_guard +0x754angle12radar_mapheadingcchar.facing_angle read at 0x189e5a..0x189e62 after the guard-family compare 0x189e40..0x189e52
guard_cone_half_angleentities[i] * :cchar_guard +0x770 +0x2cangle12radar_mapconecchar.view_cone cviewcone.half_angle
guard_cone_rangeentities[i] * :cchar_guard +0x770 +0xc * +0x4i32radar_mapconecchar.view_cone cviewcone.type cviewcone_type.range
guard_cone_activeentities[i] * :cchar_guard +0x770 +0x64u8cchar.view_cone cviewcone.active
guard_cone_visibleentities[i] * :cchar_guard +0x770 +0x65u8cchar.view_cone cviewcone.visible
guard_cone_modeentities[i] * :cchar_guard +0x770 +0x68u32cchar.view_cone cviewcone.mode
seccam_angleentities[i] * :csecuritycam +0xc4angle12radar_mapheadingcsecuritycam.facing_angle read at 0x189ecc..0x189ed8 after the camera-family compare 0x189ea4..0x189eb2
seccam_coneentities[i] * :csecuritycam +0x110cviewcone*csecuritycam.view_cone; then the cviewcone fields as for guards
cameras3e+0xF13CCccamera*ccamera (the pointer at 0x1f13cc, stored 0x1aafae)
camera_mats3e+0xF13CC * +0xacciwmatccamera.mat; no 3-D marker transform is declared because the IwGx view/projection source is not pinned

Transforms

transformparamsformulaevidencenote
radar_mapcguimappx = div(x + cguimap.offset_x, cguimap.scale) ; py = div(z + cguimap.offset_z, cguimap.scale)0x1c208c..0x1c20b4: `ldr r1,[r1] ; ldr r2,[r0,#0x78] ; adds r2,r1,r2 ; adds r4,#0x80 ; ldr r1,[r4,#4] ; bl 0x109a3c` then `ldr r0,[r5,#8] ; ldr r1,[r4] ; adds r0,r0,r1 ; ldr r1,[r4,#4] ; bl 0x109a3c ; strh r6,[r3] ; strh r0,[r3,#2]` packs (px, py) as two int16; called from 0x1bface with the CGuiMap in r0 (0x1bfac8 `ldr r0,[sp,#0x30]`, the r3 argument of 0x1bfa5c) and the entity position in r1world (x, z) -> radar pixel; the live CGuiMap is reached only through the GUI tree ([[CGuiMapViewCones+0x24]+0x24] at 0x189dd0..0x189dd4), no static root is pinned, so the overlay cannot evaluate this transform until that root is a watch

What is still open

Named, because these are the interesting parts.

The second bit of CActive's flags word

Three independent sites clear bits 0 and 1 of CActive+8 together — Cancel 0x8019F0F2 and RunIfReady 0x8019F58C with lsrs r0,#2 ; lsls r0,#2, and the ARM run loop 0x8019BA4C with bic ip,ip,#3 — and SetActive ORs bit 0 in rather than storing it, which is what says the word carries more than one flag. No instruction that SETS bit 1 was found anywhere in EUser, so the field is declared as a flags word with bit 0 named and bit 1 left undescribed. What would settle it is a sweep of the derived classes that ship in this ROM, CAsyncOneShot at 0x8019F2AB first, for a store to +8 that is not the SetActive pattern.

Sources
  • emulation/ngage/db/os/struct_field.tsv — cactive.flags

RArrayBase + 0x18 and + 0x1C

All five constructors zero these two words and nothing in EUser reads them: of the 245 Thumb loads and stores at those two offsets anywhere in EUser's code section, the 10 that fall inside the array region are all stores, all inside constructors. They are therefore not declared as fields, on the same rule that leaves TEntry + 0x220 undeclared. A reader that finds them used would most likely find it in a template instantiation inside a client image rather than in EUser, so the sweep worth doing next is over the game's own images for a load at +0x18 relative to a pointer that was passed to ordinal 55.

Sources
  • emulation/ngage/db/os/struct.tsv — rarraybase

The 185 imported EUser ordinals that still have no established name

db/os/euser-export.tsv holds all 212 ordinals the game and the runtime import, each with its export VA, its kind, whether its extent was closed, and the exec it issues where it issues one. 27 carry a name established by disassembly. The remaining 185 carry evidence — the quoted instructions — but no identity, because a name that comes from EKA2L1's epoc9.def is a cross-reference and this relation does not commit cross-references. The cheapest of them to close are the 31 rows that are call-free with a closed extent: for those the quoted body is the whole behaviour and no further reading of the ROM is needed.

The relation is also not yet joined by any generator, so the gate does not validate it. Wiring it into gen-bible.mjs and into the HLE generator's halt message is what would make a wrong row fail rather than merely sit there — and until that is done, its rows are checked by nothing but the eye.

Sources
  • emulation/ngage/db/os/euser-export.tsv
  • emulation/ngage/db/hle/import-target.tsv

!Loader function 2 beyond the handle, and the name of ordinal 1135

The library-load request is answered up to the handle: the uid1 gate, the .DLL rule and the write-back are evidenced. The drive search at 0x801D19FE, which resolves a library the caller did not path, is not disassembled, so a non-empty aPath and a library the VFS cannot find both HALT rather than guess. And whether euser ordinal 1135 at 0x801A78FE is RLibrary::Load or RProcess::Create is settled by shape only — a stripped ROM carries no names.

Sources
  • emulation/ngage/db/os/fs-op.tsv — !Loader 2
  • emulation/ngage/db/frontier.json — history: unimplemented !Loader op 2

!FileServer ops 5, 15, 22 and 87

DriveList, SetSessionPath, Entry and CreatePrivatePath have their client wrappers found in EFSrv.dll (0x801EC808, 0x801EC932, 0x801ECCC6, 0x801ED15C) and their TIpcArgs flags recorded, but the server side is not derived. Entry (22) is the interesting one: answering it needs the TEntry layout above plus a VFS lookup relative to the session path, and the packed directory form advances by EntrySize rounded up to 4 rather than by 0x228.

Sources
  • emulation/ngage/db/os/fs-op.tsv

The eight bytes at TEntry + 0x220

Closed by cross-reference — iSizeHigh and iReserved — and still unestablished from this binary, which reads neither word. They stay undeclared as fields until something in the ROM reads them.

Sources
  • emulation/ngage/db/os/struct.tsv — tentry

The DChunk fields behind slow execs 0x64 and 0x65

The handlers are single-field reads — 0x64 returns DChunk+0x34 and 0x65 returns [+0x34] + [+0x24] — and the meaning of those words is not established; the row that calls 0x64 RChunk::Top rests on the handler family at 0x80083D48 and nothing behavioural. RChunk::Base (slow 0x1, DChunk+0x2C) and RChunk::MaxSize (slow 0x3, +0x28) are established by euser's use of them; these two are not.

Sources
  • emulation/ngage/db/os/exec-impl.tsv — slow 0x64

The direct IPC hand-off and the parts of session creation the boot never exercises

When a server thread is already blocked in Receive, delivery takes the 0x8006DFAC path, which MMU-switches to the receiver; it is not RE'd and HALTs. Also unread because the boot never needs them: the dedicated message pool at 0x8008A2FC for aMsgSlots above 0, the six in-range arms of the TSecurityPolicy validator 0x8009A66C, TDesC::Match beyond its '*' comparison at 0x80099048, the thread-priority mapping table at 0x800928EC, and the cross-process legs of the RProcess execs (0x800820D8), which the model refuses instead of guessing.

Sources
  • emulation/ngage/db/os/exec-impl.tsv — slow 0x4c, slow 0x7e, slow 0x51, slow 0x17, slow 0x28

efile's load request block

The request efile.exe builds around a load carries the tldrinfo at +0 and fields at +0x48, +0x4C, +0x54 and +0x58; the copy direction at 0x801DFC06 is not settled, and tldrinfo's +0x14 and +0x1C are never written on the library path, so their meaning is undetermined and they are deliberately not declared.

Sources
  • emulation/ngage/db/os/struct.tsv — tldrinfo
  • emulation/ngage/db/frontier.json — current

What static RE of the game does not reach

  • The live CGuiMap (transforms.tsv radar_map) is a GUI-tree child reached as [[CGuiMapViewCones+0x24]+0x24] (0x189DD0..0x189DD4); its instance is created by the resource loader from all.dz stage data. No static root, so the overlay cannot evaluate the world-to-radar transform until that root is a watch.
  • all.dz and warp.dz are DTRZ containers whose every entry uses codec 4 — the game's own decoder, not ported; the .group.bin stage data and .luc scripts inside are unread (tools/mgs_re.py dz FILE lists them).
  • The stage name: the state object whose +0x20 C string is passed to the loader 0x1A88E0 (chapters/ + name + /ingame, 0x16E7FC..0x16E806 and 0x16E9B4..0x16E9BA) has no pinned owner; its methods are reached from 0x17E42E..0x17E442 with the object pointer set outside the disassembled window.
  • The IwGx view matrix: ccamera.mat (+0xAC) is reset from the .bss matrix at 0x1F4470; whether the renderer's view and projection are taken from there is not evidenced, so no 3-D marker transform is declared.
  • Nothing has been observed live: the game has not run past its front end under either engine.
Sources
  • emulation/ngage/db/game/mgs/transforms.tsv
  • emulation/ngage/db/game/mgs/fields.tsv — ccamera.mat
  • emulation/ngage/tools/mgs_re.py

The <Game> part's checksums

No CRC-16 parameterisation reproduces the ControllerChecksum or DataChecksum of any <Game> part, while the same algorithm reproduces all six <Metadata> values. Closing it needs an unmodified first-party N-Gage SIS or the makesis/sisxlibrary source; until then serializeSisx drops both fields from a patched <Game> part and recomputes them only on a part whose stored values verified.

Sources
  • emulation/ngage/web/src/loader/sisx.js
  • emulation/ngage/web/test/loader.mjs

A reference trace of the game past its entry, scoped to the game

The raw-path launch of mgs_ngi covers 150 executive calls before dying at entry point 0, and the Game Card route needs an .aif this title never had. Installing the launcher's runtime does get the title started — 271,617 lines on 2026-09-04, screen devices created — but that trace carries no per-line process id, so cutting the game's own calls out of a device running the launcher is a reconstruction, not a reading. Until the scope is cut by something the log actually states, the differential oracle has a trustworthy reference only for the shared _E32Startup prefix.

Sources
  • emulation/ngage/tools/eka2l1-capture.ps1
  • emulation/ngage/tools/oracle-diff.mjs

How the S3E stub reaches libgles_cm

The game's XE3U asks the stub for eglGetDisplay, eglCreateWindowSurface and eglSwapBuffers by name, and the stub imports no graphics library at all. How it gets from those names to libgles_cm's exports is not established. The hypothesis on the table is runtime.dll's feature registry — LookupFeature is runtime ordinal 10, and the stub calls it with the ids 0x701, 0x602, 0x02000101 and 0xC03 at 0x7000C44C, 0x7000CDB6, 0x7000F8CE and 0x70010328 — but nothing has been read that shows the registry handing back a GL entry point, so it stays a hypothesis and no HLE is built on it.

Sources
  • emulation/ngage/db/game/mgs/image.tsv
  • emulation/ngage/samples/launcher/installed/sys/bin/runtime.dll

What the crack changed inside MGSfileaccess.dll

17,226 of the 21,292 code bytes the two modules share differ from ngifileaccess.dll, and the game's copy is 40 bytes longer. The import table says what it redirects — caf.dll reached through MetalGS_Crack.dll on the same nine ordinals — and says nothing about what the changed code does. It has not been disassembled, so no claim is made about it beyond the byte counts.

Sources
  • emulation/ngage/web/src/loader/e32.js — flatImage, parseImports

The S3E stub's frame and event loop — the retired lineage's map, unverified

Recorded so the next pass knows where to look, and here rather than among the entries for the reason that matters: none of it has been re-derived from the binary. It comes from the retired HLE lineage — the deleted emulation/ngage-hexenwerkzeug tree and the workbench lineage's mgs-patches.js, readable in git history before 5a9f0ec and 2b3d280 — and that is the same lineage whose measured reach was 0.3 percent of a frame. Every address below is a place to start a disassembly, not a fact.

All addresses are stub VAs at base 0x8000, and keyN means the stub's service-registry object N from the 0x14108 lookup.

  • The loop: entry 0x25504, body 0x2551C, the per-iteration state[+4] sustainer 0x25584, force-tick 0x25576, 0x25590 beq to playStage, 0x255B8 clears abort and sets engine-phase 5, and 0x255DE is playStage's early return on [stage+0xD8] plus [stage+0xB0].
  • Bail gates 0xE1CC, 0xE1D2, 0xE1DC and 0x2553C; 0x2C2EA is a vtable-null fault on the deeper path.
  • One frame is 0x24BCC(1,0,0,0): 0x24BD2 cmp r0,#1, 0x24BD4 bne 0x24BFE, then bl 0x28EEC.
  • 0x28EEC runs a 200-entity loop gated by bitmask bit 0x20 at key4+0x104, and 0x22B54(0xD,5,1) when that bit is clear.
  • Per entity: 0x29ABE primary and 0x29F98 secondary, gated by key5+0x2AA+idx and key5+0x1D7+idx; 0x28E46 action 2 bails on 0xFFFFFFFF; the character-remap table is 0x124A4, whose live indices [13..21] and [23..47] make 34 entities.
  • The event queue sits at key3+0x26C with 12-byte entries and read and write pointers at +0x200 and +0x204: 0x23118 pushes (unlocked by bitmask bit 0x1000000 plus the gate byte at key4+0x125), 0x2DDE8 allocates, 0x252FE begins a transaction, 0x1BF14 is the push gate where 0 means skipped, and command 0x0D is the event-send that dominates a frame.
  • State: 0x400280 is globalTable[1], the event-loop state pointer, a 1,148-byte buffer allocated at pc 0x14D58; 0x40015C and up are libc data thunks; 0x14108 is the service-registry lookup; 0x226C6 and 0x226C7 are the event-pump trigger (flag key19+0x40, vtable key19+0xB8, skip path 0x2271C, setters 0x226DA and 0x227A0); 0x21F05 is the only state-mutating observer method; 0x19864 is the listener-list anchor, its slot reg(3)+0x38.

What would settle it is the route the stage loader entry above already took: disassemble forward from 0x25504 against mgs_ngi.exe and keep what the instructions say. Where this map and the binary disagree, the binary is right and this entry is the thing to correct.

Sources
  • emulation/ngage/samples/mgs/extracted/game/drive_!/sys/bin/mgs_ngi.exe
  • emulation/ngage/db/game/mgs/image.tsv

Generated by emulation/ngage/tools/gen-bible.mjs from emulation/ngage/db/bible.json, emulation/ngage/db/frontier.json, emulation/ngage/db/frontier-hle.json, emulation/ngage/db/last-run.json, emulation/ngage/db/os/exec-impl.tsv, emulation/ngage/db/os/struct.tsv, emulation/ngage/db/os/struct_field.tsv, emulation/ngage/db/os/hle-server.tsv, emulation/ngage/db/os/fs-op.tsv, emulation/ngage/docs/data/ekern-exec-table.tsv, emulation/ngage/docs/data/euser-svc-map.tsv, emulation/ngage/db/game/mgs/image.tsv, emulation/ngage/db/game/mgs/structs.tsv, emulation/ngage/db/game/mgs/fields.tsv, emulation/ngage/db/game/mgs/watches.tsv, emulation/ngage/db/game/mgs/transforms.tsv. The prose is db/bible.json; everything else is the tables the emulator runs on.