Metal Gear Solid Mobile
Konami shipped this for the N-Gage 2.0 in 2008; the platform is dead and the community kept the game alive with cracked binaries. This emulator runs the game unmodified, by emulating the machine underneath it instead of patching it. Below is exactly how far that gets today — measured, not claimed: every number here is asserted by the gate, in Node, in Chromium and on the tool page, and it moves only when new evidence justifies it.
Where the run stops
There are two ways to run it, and they stop in different places. Without a ROM the game's own code executes while Symbian's calls are answered in JavaScript — each answer derived by reading the real firmware, so a visitor needs nothing but the game. With the device ROM the firmware itself executes, which is slower to get anywhere but is the fidelity oracle that tells the first path what every call must do.
- Without a ROM
- 19,801
- instructions, halted at 0xfffffff8
- With the device ROM
- 68,477
- instructions, halted at 0xf0000000
- Walls crossed
- 37
- 31 with the ROM, 6 without
- Frame drawn
- not yet
Without a ROM, it stops here
mgs_ngi.exe E32Main 0x84d0 calls libc exit(0) after unwinding its cleanup level at 0x84a0
What it is: exit(0) - NOT a missing row and NOT a boot. The game ran its N-Gage Arena initialisation to completion and decided to quit
hle-runtime: 18,847 instructions — runtime.dll 0x8932, inside its own initialisation running as REAL ARM code: CCoeEnv::Static() returned null, so 0x8920 allocated 0x168 bytes through User::AllocZ and 0x892e constructs a CEikonEnv in them
With the device ROM, it stops here
mgs_ngi.exe's Arena initialisation calls ArenaEnv ordinal 1 at 0x249f8 once the file server has answered for Arena/config.xml
What it is: 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.
The latest run
The lle profile, from web/test/run-rom.mjs:
68,477 instructions to 0xf0000000, of which
34 were inside the game's own RAM modules. It carries no
timestamp on purpose — the same code must produce the same record, so this changes when the
emulator does, and the gate refuses a copy that has gone stale.
Halt: HALT: unimplemented import arenaenv@1 called — map it to a ROM export or add an HLE impl (web/src/kernel/eabi.js / registry), or record it in db/os/import-allow.tsv with evidence if it is a genuine no-op
Modules attached, in call-list order: euser → rom@0x82e61340 → rom@0x82e460e0 → rom@0x82e88600 → libc → libpthread → libz → mgs_ngi.exe; MetalGearSlibdl.dll loaded at 0x70100000 with 1310 exports.
| svc | what we implement | calls |
|---|---|---|
| 0x800001 | allocator_get | 27 |
| 0x800008 | trap_handler_get | 14 |
| 0x4c | session_send_sync | 13 |
| 0x800000 | wait_for_any_request | 13 |
| 0x69 | handle_close | 9 |
| 0xa0 | library_detach | 9 |
| 0x6f | sema_create | 8 |
| 0x4e | hal_get | 5 |
| 0x800002 | heap_switch | 5 |
| 0x7e | session_create | 5 |
| 0x6e | mutex_create | 5 |
| 0x11 | mutex_wait | 5 |
| 0x12 | mutex_signal | 5 |
| 0x6d | handle_duplicate | 4 |
| 0x800009 | set_trap_handler | 2 |
| 0x6a | rchunk_create | 2 |
| 0x3 | rchunk_max_size | 2 |
| 0x6b | rchunk_op | 2 |
| 0x1 | rchunk_base | 2 |
| 0x16 | process_filename | 2 |
| 0x4d | dll_tls | 2 |
| 0x26 | thread_id | 2 |
| 0x75 | dll_set_tls | 2 |
| 0x9f | process_static_call_list | 1 |
| 0x27 | session_share | 1 |
| 0x67 | thread_create | 1 |
| 0x28 | thread_resume | 1 |
| 0xd0 | process_get_data_parameter | 1 |
| 0x51 | process_command_line_length | 1 |
| 0x17 | process_command_line | 1 |
| 0xad | process_security_info | 1 |
| 0x9b | dll_lock_wait | 1 |
| 0x9d | library_attach | 1 |
| 0x9e | library_attached | 1 |
| 0xe | library_lookup | 1 |
| 0xa1 | library_detached | 1 |
| server | function | answer |
|---|---|---|
| !FileServer | -1 Connect | 0 |
| !FileServer | 85 SetSessionToPrivate | 0 |
| !FileServer | -1 Connect | 0 |
| !FileServer | 14 SessionPath | C:\Private\20010925\ |
| !FileServer | -1 Connect | 0 |
| !FileServer | 14 SessionPath | C:\Private\20010925\ |
| !FileServer | -1 Connect | 0 |
| !FileServer | 14 SessionPath | C:\Private\20010925\ |
| !Loader | -1 Connect | 0 |
| !Loader | 2 LoadLibrary | hal.dll -> handle 0x40000065 |
| !FileServer | 14 SessionPath | E:\Private\20010925\ |
| !FileServer | 86 PrivatePath | \Private\20010925\ |
| !FileServer | 22 Entry | C:\private\20010925\Arena\config.xml -> 3376 B |
What the machine knows
| what | count |
|---|---|
| kernel calls established | 42 of 269 the ROM dispatches |
| servers answered | 4 (12 of 15 operations) |
| OS structures | 25 structs, 160 fields |
| game object layouts | 15 structs, 32 fields |
| findings written up | 46 (31 established) |
Every one of those is a row with the address it was read at; nothing here is remembered from documentation. The rule is in the bible: a value with no evidence does not exist, so an unknown call halts the run instead of returning a plausible number.
The walls, newest first
| # | wall | how it fell |
|---|---|---|
| HLE 6 | unimplemented-import euser@1360 (UserHeap::SetupThreadHeap) (13 instructions) | the first HLE row: the machine supplies the RAllocator in SStdEpocThreadCreateInfo+0x30 and the sharing branch at euser …the first HLE row: the machine supplies the RAllocator in SStdEpocThreadCreateInfo+0x30 and the sharing branch at euser 0x801b2d24 returns 0 |
| HLE 5 | unimplemented-import libdl@3 (dlopen) (18,410 instructions) | dlopen/dlsym/dlclose/dlerror over the RE map: a dynamically loaded export became an ordinary row of import-impl.tsv, and…dlopen/dlsym/dlclose/dlerror over the RE map: a dynamically loaded export became an ordinary row of import-impl.tsv, and hal.dll's five exports entered db/hle/import-target.tsv through the declared DYNAMIC_LIBS list |
| HLE 4 | unimplemented-import runtime@9 (the N-Gage runtime singleton) for [hle]; euser@1091 CActive::CActive for [hle-runtime] (18,533 instructions) | for [hle-runtime]: the runtime files became an optional dropped input and run as real ARM code; CActive::CActive and CBa…for [hle-runtime]: the runtime files became an optional dropped input and run as real ARM code; CActive::CActive and CBase::~CBase were then read out of the ROM (0x8019f189 is seven straight-line instructions, 0x801a30a4 is a bare bx lr) |
| HLE 3 | unimplemented-import euser@55 (RArrayBase::RArrayBase) for [hle-runtime] (18,638 instructions) | nine leaf rows read out of the ROM - euser 55/481 the two array constructors, 855 UniversalTime, 80 TBufBase16(TDesC16&,…nine leaf rows read out of the ROM - euser 55/481 the two array constructors, 855 UniversalTime, 80 TBufBase16(TDesC16&,TInt), 1849 RProcess::Id, 985 TDes16::AppendNum, 120 RHandleBase::Close, 63/65 the RSemaphore Open/CreateGlobal pair - which carried the runtime through its NGI_SEMAPHORE_<pid> rendezvous to the CONE application framework at 18833 |
| HLE 2 | unimplemented-import runtime@9 (the N-Gage runtime singleton) for [hle] (18,533 instructions) | runtime@9 and runtime@10 answered above the platform, and libc@72 exit - which carried the container-only profile throug…runtime@9 and runtime@10 answered above the platform, and libc@72 exit - which carried the container-only profile through the game's whole Arena init to the game's OWN exit(0) at 19801. The advance is real and so is what it found: the game refuses to run without the N-Gage platform |
| HLE 1 | unimplemented-import cone@182 (CCoeEnv::Static) for [hle-runtime] (18,833 instructions) | CCoeEnv::Static resolved inside 5320.rom (CONE.dll is not one of the 28 extracted DLLs) and shown to be a TLS read throu…CCoeEnv::Static resolved inside 5320.rom (CONE.dll is not one of the 28 extracted DLLs) and shown to be a TLS read through EUser ordinal 2176 = svc 0x4d - three instructions delegating to five. It returns null here and runtime.dll has a first-class branch for that, allocating its own 0x168-byte CEikonEnv instead |
| 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-…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. |
| 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…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. |
| 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 0x…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. |
| 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…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). |
| 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…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. |
| 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 (0x801A795…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. |
| 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 …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. |
| 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 vta…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. |
| 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 th…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. |
| 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…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. |
| 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 c…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. |
| 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 …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`. |
| 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…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. |
| 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. …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. |
| 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 hidi…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. |
| 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 s…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. |
| 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: moni…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. |
| 14 | slow exec 0x67 RThread::Create (4848 instrs) | ekern 0x800839fc copies a 0x30B SThreadCreateInfo (eka2l1 thread_create); alloc a suspended 'thread' object (TObjectType…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. |
| 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' ha…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. |
| 12 | fast exec 0x2 User::Critical (1852 instrs) | ekern 0x8006a67c returns thread's TCritical field; impl returns tracked KState.thread.critical (default ENotCritical=0).…ekern 0x8006a67c returns thread's TCritical field; impl returns tracked KState.thread.critical (default ENotCritical=0). Boot advanced to 1885. |
| 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.…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. |
| 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…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. |
| 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…returns TRomHeader.rom_root_dir_list (read live from ROM header @+0x94). Evidence: eka2l1 svc.cpp + rom.h. Boot advanced to 1536 instrs. |
| 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-…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. |
| 7 | Thumb cond() HI/LS/VS/VC missing -> every bhi/bls always taken | cpu.js cond() explicit cases 6..9 (real CPU bug)…cpu.js cond() explicit cases 6..9 (real CPU bug) |
| 6 | cleanup-stack panic 66/63 (hand-modeled CCleanup) | let real euser CTrapCleanup::New build it; artefact of the cond bug…let real euser CTrapCleanup::New build it; artefact of the cond bug |
| 5 | exec 0x4c (676 instrs) | WAS FABRICATION-INFLATED: only reached because libc@2 silently returned 0 at ~600 instrs. Making import stubs HALT expos…WAS FABRICATION-INFLATED: only reached because libc@2 silently returned 0 at ~600 instrs. Making import stubs HALT exposed the real frontier. |
| 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.…Mapped the REAL libc/pthread/zlib/dl from firmware (run pristine code, not HLE stubs) — resolved 95 imports honestly. |
| 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 …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. |
| 2 | null-branch from euser 0x8019fb36 (1014 instrs) — the honest frontier | 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. |
| 1 | unimplemented !FileServer op 22 (RFs::Entry) at 68,015 instrs — the game's Arena runtime asking for the metadata of C:\private10925\Arena\config.xml | By answering it from the VFS the container installed, with the two fields this filesystem cannot source DECLARED rather …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:\private10925\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. |
Generated from emulation/ngage/tools/gen-bible.mjs; the ledger and the evidence tables are the source, and the gate refuses a stale copy.