Step 23 · Loading and execution · D2
ELF to the first guest instruction
Follow one boot path across file offsets, mapped addresses, relocation and the native guest-entry boundary.
Before this page: Building, linking & debugging · NIDs and the runtime linker · Patching guest instructions · Threads and TLS · HLE libraries · The system around the GPU. View the learning route.
Bring the codebase map and the loading, patching, thread and HLE explanations together here. This is a source-reading trace at d2413fc2, with an optional debugger worksheet. It is not a captured run of a particular game.
First guest instruction may be a shared module initializer called by StartModule. Main executable entry is a later transfer through RunEntry. Neither necessarily means the game's C/C++ main(). A failure before the “Execute: Main” log can therefore already be executing guest code.
The selected boot chain
Host startup: Emulator::Run
initialize subsystems and mount guest paths
Libs::InitAll -> registered HLE symbol database
LoadElf -> RuntimeLinker::LoadProgram
Elf64::Open -> LoadProgramToMemory
ParseProgramDynamicInfo -> CreateSymbolDatabase
Execute starts guest thread; host main continues WindowRun
Guest thread: RuntimeLinker::Execute
initialize main guest thread and allocate its guest stack
LoadProgram("/app0/sce_module/libc.prx" resolved through the mount, if present)
RelocateAll -> optional GamePatch::Apply
StartModule(libc) -> guest libc initializer, if present
GetEntry -> RunEntry -> guest executable entry -> guest startup code
The ordering comes from src/emulator.cpp and src/loader/runtimeLinker.cpp. A source trace tells you where to look; a live run establishes which branches and modules were actually used.
1. Establish the file, mapping and symbols
| Boundary | Inspect | Invariant / artifact to record |
|---|---|---|
| Guest path → host file | Emulator::Run, LoadElf; FileSystem::GetRealFilename. | Record the resolved ELF and mounted app directory. A path inside the guest namespace is not automatically a host filesystem path. |
| File → parsed object | RuntimeLinker::LoadProgram calls Elf64::Open and checks validity. | Record ELF type, entry, program headers and whether this is a shared module. Do not equate a filename extension with a valid executable. |
| Object → mapped program | LoadProgramToMemory, CalcBaseSize, Memory::AllocateProgramMemory. | Record base_vaddr, base size, aligned size and mapped size separately. Allocation/rounding checks are part of loading, not a reason to trust arbitrary ELF sizes. |
| Segments → host bytes | PT_LOAD/PT_OS_RELRO loop, Elf64::LoadSegment. | Destination is base + segment virtual address; source bytes start at the file offset. File size and memory extent need not match. Inspect any zero-fill tail and actual allocation behavior rather than reading beyond the file payload. |
| Code → executable mapping | PatchProgram, protection changes, instruction-cache flush and platform-specific guest instruction patching. | Record the relevant flags and any generated trampoline range. Loaded bytes can differ from the original ELF after patching. |
| Metadata → runtime structures | PT_TLS, PT_OS_PROCPARAM, ParseProgramDynamicInfo, CreateSymbolDatabase. | Identify the TLS template, dynamic imports/exports and owning module. A mapped address alone is not a resolved import. |
The guest page constant used by this loader is 0x4000 = 16,384 bytes. Do not reuse it blindly as a host page size, allocation granularity or GPU alignment. See Lab A for a concrete page-boundary failure.
2. Work an address through every coordinate system
This is an invented arithmetic fixture, not an observed game mapping. Use hexadecimal for addresses and decimal for byte counts; never silently interchange a virtual address and file offset.
| Quantity | Fixture |
|---|---|
| Load base | 0x100000000 = 4,294,967,296 |
| Segment p_vaddr / p_offset | 0x4000 / 0x1000 |
| Segment p_filesz / p_memsz | 0x2000 = 8,192 / 0x4000 = 16,384 |
| ELF e_entry | 0x4100 |
| Mapped segment start | 0x100004000 = base + p_vaddr |
| Runtime entry PC | 0x100004100 = base + e_entry |
| Entry byte's file offset | 0x1100 = p_offset + (e_entry − p_vaddr) |
The file-offset conversion is valid only if the relative virtual address lies inside this segment's file-backed portion. An address in its remaining memory tail has no corresponding byte in this segment's file payload. A generated stub has no original ELF file offset at all.
Self-check: where is runtime PC 0x100006100?
Its relative address is 0x6100. It is inside the fixture's memory extent [0x4000,0x8000), but beyond its file-backed extent [0x4000,0x6000). The formula would produce a number, but not a valid byte of this segment's file payload. First identify the region, then do the subtraction.
3. Resolve before executing
On the guest thread, RuntimeLinker::Execute establishes main-thread state and allocates the main guest stack, loads libc if present, then calls RelocateAll. Inspect the relocation kind, target slot, addend, imported identity and resolved destination as a unit. HLE registrations were installed earlier; module exports are another possible resolution source.
Dynamic imports are eagerly relocated in this snapshot. Successful dynamic relocation slots retain resolved state; unresolved callable imports can receive a 34-byte diagnostic thunk that logs and returns zero in RAX/XMM0. This is not lazy binding that retries and forwards on first call, and returning zero does not establish correct guest behavior. Different relocation kinds and resolution-failure policies must be checked in their own code paths.
At a selected import, record the slot's runtime address, its value after relocation and the owner of that destination: host HLE, guest module, generated diagnostic code, or unresolved failure. Breaking on a thunk proves only that a fallback was reached. For a real implementation, follow the contract checks.
4. Locate the first transfer into guest code
Execute automatically starts the loaded libc when it has an initializer; other modules are requested by the application. StartModule invokes the initializer at program->base_vaddr + program->dynamic_info->init_vaddr through the module initializer function type. If such a module is started, this guest call happens before the executable-entry transfer. Trace application-requested load/start calls separately from this automatic libc startup.
After module startup, GetEntry supplies the main ELF entry plus its load base. Execute writes a zero-initialized entry-parameter block near the top of the guest stack, sets argc = 1 and argv[0] = "KytyEmu", then calls RunEntry with an exit handler and a stack top below the parameter block.
RunEntry is the native boundary, not a CPU interpreter loop. Its stack-switch path aligns the supplied stack, places root frame words, sets RSP/RBP and performs a native call with the parameter pointer and exit callback in the required ABI argument registers. Read the platform-specific assembly; Windows also handles host stack-bound metadata. Do not assume that the earlier module-initializer call uses this same stack-switch path.
For the selected stack-switch path, an aligned top minus two eight-byte root words is still 16-byte aligned before the call; the call pushes an eight-byte return address. Thus the callee sees RSP % 16 == 8 at entry. This is an ABI check, not evidence that TLS, imports or the game's own startup logic are correct.
5. Debugger worksheet: expected evidence, not guessed addresses
Optional runtime work needs your own lawful executable and a matching build with symbols. Use the debugging topic to prepare a debugger. On Windows include --redzone when launching a game. Do not stop on every expected memory-tracking exception; that changes normal emulator operation. No game run is required for the arithmetic exercise.
- Break at
LoadProgram. Write down the resolved path and program identity. - At the end of mapping, compute entry from the actual base/header. Identify its owning segment, permissions and any applicable patching.
- At
RelocateAll, choose one import and inspect its slot before/after. Classify the destination by its owning region. - Break at
StartModuleandRunEntry. Record which occurs first and which thread hits it. Step to the actual target instruction rather than counting a host wrapper as guest entry. - At executable entry, verify PC, RSP alignment and argument pointer contents. Continue only as far as the scope of your investigation permits.
- If a boundary is never reached, report the last observed boundary. Do not infer that mapping, relocation or initialization succeeded from a window appearing.
Module / resolved file: Base / entry RVA / runtime PC / owning segment: Original file offset (or why none): Import identity / slot / destination owner: First initializer observed (or none): Executable entry PC / thread / RSP alignment: Argument block / exit callback: Last verified boundary / unresolved question:
Pass the boot gate
Checkpoint
Source anchors and next step
src/emulator.cpp:Run,LoadElf,Execute.src/loader/runtimeLinker.cpp:LoadProgram,LoadProgramToMemory,RelocateAll,StartModule,GetEntry,RunEntry.src/loader/elf.cpp: file parsing andLoadSegment.
Continue with GPU basics. The graphics lessons build toward one draw through the renderer. Dynamic loading/unloading, every relocation type and every exception/TLS path are outside this selected trace.