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.
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:
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
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.
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_protectexists because some PS5 executables declarePT_LOADsegments with no access flags at all. ApplyingNoAccessliterally would make them unreadable; the loader leaves them mapped instead.- The TLS image is a template.
PT_TLSdescribes the initial contents; each thread gets its own copy, allocated lazily byTlsGetAddr.
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.
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.
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.
Trace a real load. The log will tell you exactly where each segment went.
Checkpoint