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.

Read, trace, prove

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.

Define the endpoint first

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

BoundaryInspectInvariant / artifact to record
Guest path → host fileEmulator::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 objectRuntimeLinker::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 programLoadProgramToMemory, 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 bytesPT_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 mappingPatchProgram, 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 structuresPT_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.

QuantityFixture
Load base0x100000000 = 4,294,967,296
Segment p_vaddr / p_offset0x4000 / 0x1000
Segment p_filesz / p_memsz0x2000 = 8,192 / 0x4000 = 16,384
ELF e_entry0x4100
Mapped segment start0x100004000 = base + p_vaddr
Runtime entry PC0x100004100 = base + e_entry
Entry byte's file offset0x1100 = 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.

  1. Break at LoadProgram. Write down the resolved path and program identity.
  2. At the end of mapping, compute entry from the actual base/header. Identify its owning segment, permissions and any applicable patching.
  3. At RelocateAll, choose one import and inspect its slot before/after. Classify the destination by its owning region.
  4. Break at StartModule and RunEntry. Record which occurs first and which thread hits it. Step to the actual target instruction rather than counting a host wrapper as guest entry.
  5. At executable entry, verify PC, RSP alignment and argument pointer contents. Continue only as far as the scope of your investigation permits.
  6. 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 and LoadSegment.

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.