Step 16 · Loading and execution

Mapping a program into memory

The module wants to live at particular addresses and will store raw pointers assuming it got them. So the emulator reserves the guest's whole address space up front and carves it up, rather than letting the host decide where things land.

Before this page: ELF, SELF and Sony's format · The guest address space. View the learning route.

Read

One function does the whole job. It is worth reading line by line because almost every design decision in the loader is visible in it.

Deciding how much space to reserve

The mapped size is the highest address any loadable segment reaches, rounded up to a guest page, plus optional instruction-trampoline space and room for the main module’s generated TLS handler:

src/loader/runtimeLinker.cpp
static uint64_t CalcBaseSize(const Elf64_Ehdr* ehdr, const Elf64_Phdr* phdr) {
    uint64_t base_size = 0;
    for (Elf64_Half i = 0; i < ehdr->e_phnum; i++) {
        if (phdr[i].p_memsz != 0 &&
            (phdr[i].p_type == PT_LOAD || phdr[i].p_type == PT_OS_RELRO)) {
            uint64_t last_addr = phdr[i].p_vaddr + GetAlignedSize(phdr + i);
            if (last_addr > base_size) base_size = last_addr;
        }
    }
    return base_size;
}
program->base_size         = CalcBaseSize(ehdr, phdr);
program->base_size_aligned = AlignUp(program->base_size, 0x4000);   // 16 KB guest page

uint64_t tls_handler_size  = is_shared ? 0 : Jit::SafeCall::GetSize();
program->mapped_size       = program->base_size_aligned + tls_handler_size;

program->base_vaddr = Libs::LibKernel::Memory::AllocateProgramMemory(
    g_desired_base_addr, program->mapped_size,
    Common::VirtualMemory::Mode::ExecuteReadWrite,
    Common::PathToString(program->file_name.filename()).c_str());

g_desired_base_addr += CODE_BASE_INCR * (1 + program->mapped_size / CODE_BASE_INCR);

Note that the region is initially mapped ExecuteReadWrite. Correct protection is applied per segment afterwards — the loader needs write access to copy data in and to patch instructions, before locking things down.

The layout constants

src/loader/runtimeLinker.cpp
constexpr uint64_t SYSTEM_RESERVED  = 0x800000000u;
constexpr uint64_t CODE_BASE_INCR   = 0x010000000u;   // 256 MB between modules
constexpr uint64_t CODE_BASE_OFFSET = 0x100000000u;

static uint64_t g_desired_base_addr = SYSTEM_RESERVED + CODE_BASE_OFFSET;   // 0x900000000

The preferred main-module base is 0x900000000. The next preferred base advances by enough 256 MiB steps for the mapped size. Check the actual load log and module segments when resolving a fault address; a numeric address band alone does not identify a caller.

File on disk → guest memorywhat LoadProgramToMemory does
eboot.bin (SELF → ELF64) guest memory base_vaddr = 0x900000000 ELF header + phdrs PT_LOAD R-X .text PT_LOAD R-- .rodata PT_LOAD RW- .data (filesz < memsz) PT_TLS (template) PT_OS_DYNLIBDATA PT_OS_PROCPARAM +0x00000000 ExecuteRead code ← PatchProgram runs here +0x… Read read-only data +0x… ReadWrite data tail zero-filled to memsz per-thread TLS copies (lazy) proc_param_vaddr mapped_size

Solid arrows are direct copies: file bytes land at p_vaddr + base_vaddr. Dashed arrows are recorded rather than copied — the TLS segment is a template each thread copies from later, and the process-parameter block is just an address the runtime reads. The .data segment shows the filesz < memsz case: the difference is zero-filled, which is how .bss arrives.

The segment loop

for (Elf64_Half i = 0; i < ehdr->e_phnum; i++) {
    if (phdr[i].p_memsz != 0 &&
        (phdr[i].p_type == PT_LOAD || phdr[i].p_type == PT_OS_RELRO)) {

        uint64_t segment_addr        = phdr[i].p_vaddr + program->base_vaddr;
        uint64_t segment_file_size   = phdr[i].p_filesz;
        uint64_t segment_memory_size = GetAlignedSize(phdr + i);
        auto     mode                = GetMode(phdr[i].p_flags);

        program->elf->LoadSegment(segment_addr, phdr[i].p_offset, segment_file_size);

        bool skip_protect = (phdr[i].p_type == PT_LOAD && is_next_gen &&
                             mode == Common::VirtualMemory::Mode::NoAccess);

        if (Common::VirtualMemory::IsExecute(mode)) {
            PatchProgram(program, segment_addr, segment_memory_size);   // Patching guest instructions
        }

        if (!skip_protect) {
            Memory::SetProgramMemoryProtection(segment_addr, segment_memory_size, mode);
            if (Common::VirtualMemory::IsExecute(mode)) {
                Common::VirtualMemory::FlushInstructionCache(segment_addr, segment_memory_size);
            }
        }
    }

    if (phdr[i].p_type == PT_TLS) {
        program->tls.image_vaddr = phdr[i].p_vaddr + program->base_vaddr;
        program->tls.init_size   = std::min(phdr[i].p_filesz, GetAlignedSize(phdr + i));
        program->tls.image_size  = GetAlignedSize(phdr + i);
        program->tls.tcb_offset  = program->tls.image_size;
    }

    if (phdr[i].p_type == PT_OS_PROCPARAM) {
        program->proc_param_vaddr = phdr[i].p_vaddr + program->base_vaddr;
    }
}

Three details worth extracting:

  • Patch before protect. The rewriting in Patching guest instructions needs write access, and the instruction cache flush must happen after the bytes change. Reversing the order would either fault or execute stale instructions.
  • skip_protect exists because some PS5 executables declare PT_LOAD segments with no access flags at all. Applying NoAccess literally would make them unreadable; the loader leaves them mapped instead.
  • The TLS image is a template. PT_TLS describes the initial contents; each thread gets its own copy, allocated lazily by TlsGetAddr.

Loading order across modules

RuntimeLinker::Execute() resolves /app0/sce_module/libc.prx through the mounted filesystem and loads it automatically if present, then calls RelocateAll(). It starts libc’s initializer when available. Other PRXs are loaded and started when the application requests them through the kernel module APIs; there is no directory-wide preload or dependency-sorted startup pass.

src/loader/runtimeLinker.cppsrc/loader/runtimeLinker.cpp
Watch

The guest address space, band by band, with the host reservation underneath it. Note that the layout is not a convenience — the guest depends on these addresses.

Try

Allocate and free until you fragment the space, then try for a large block. This is the failure the placeholder-coalescing logic exists to prevent.

Do

Trace a real load. The log will tell you exactly where each segment went.

Checkpoint