Step 15 · Loading and execution

ELF, SELF and Sony's format

A game module looks like an ELF and is rejected by every normal ELF loader. The type is ET_SCE_*, and the machinery a linker needs — symbols, strings, relocations — has been moved into a proprietary segment.

Before this page: Virtual memory, briefly. View the learning route.

Read

A game is a file. Something has to turn that file into a running program. On Linux that job belongs to ld.so; in KytyPS5 it belongs to RuntimeLinker. To read that code you need to know what a dynamic linker does.

The ELF layout

ELF — Executable and Linkable Format — is the Unix executable format, and the PS5 uses it. An ELF file has two overlapping views:

ViewDescribed byUsed by
Sections (.text, .data, .rodata…)Section headersThe linker at build time. Often stripped from shipped binaries.
Segments (PT_LOAD, PT_DYNAMIC, PT_TLS…)Program headersThe loader at run time. This is the view Kyty cares about.

A program header entry says: "take p_filesz bytes at file offset p_offset, place them at virtual address p_vaddr, zero-fill up to p_memsz, and apply these permissions." Loading is essentially a loop over those entries.

src/loader/elf.h — the actual struct Kyty parses
struct Elf64_Phdr {
    Elf64_Word  p_type;    PT_LOAD, PT_DYNAMIC, PT_TLS, …
    Elf64_Word  p_flags;   PF_R | PF_W | PF_X
    Elf64_Off   p_offset;  where in the file
    Elf64_Addr  p_vaddr;   where in memory
    Elf64_Addr  p_paddr;   reserved
    Elf64_Xword p_filesz;  bytes present in the file
    Elf64_Xword p_memsz;   bytes needed in memory (remainder is zeroed)
    Elf64_Xword p_align;
};

Why relocation exists

A shared library does not know where it will be loaded. It is compiled position-independently, and every absolute address inside it is left as a placeholder. The loader must go back and fix them. Each fix is a relocation: a record saying "at offset X, write value Y."

The principal relocation types are:

TypeMeaningComputation
R_X86_64_RELATIVEAn internal pointer that just needs the load bias added.*where = base + addend
R_X86_64_64A 64-bit absolute reference to a symbol.*where = symbol + addend
R_X86_64_GLOB_DATA Global Offset Table entry for a data symbol.*where = symbol
R_X86_64_JUMP_SLOTA Procedure Linkage Table entry for a function.*where = symbol
R_X86_64_DTPMOD64Thread-local storage: which module owns this variable.module ID
Jargon · GOT and PLT

The Global Offset Table is an array of pointers. Instead of embedding an absolute address in code, the compiler emits "load the pointer from GOT slot 12, then use it." The loader fills the GOT, so the code itself never needs patching.

The Procedure Linkage Table is the same idea for calls. A call to an external function jumps to a small PLT stub, which jumps to whatever address the GOT holds.

This is the hook KytyPS5 uses. When the loader writes the address of its own C++ function into a GOT slot, every subsequent guest call to that "console function" lands in the emulator instead. No patching of the game's code is needed.

Symbols, and why the PS5's are unreadable

Normally a dynamic symbol has a name: printf, malloc. PS5 modules instead export NIDs — an 11-character encoding of a hash of the original name — qualified by the library and module they belong to:

b4fpgH5ZXxQ#Graphics5Driver#Graphics5Driver
└─ symbol ─┘ └─── library ───┘ └──── module ────┘

Kyty stores its HLE implementations under the same key, so resolution is a dictionary lookup. You will meet the exact mechanism in NIDs and the runtime linker and HLE libraries.

Thread-local storage in one paragraph

A variable declared thread_local needs a different copy per thread. The usual implementation gives each thread a block of memory and a pointer to it, reachable through a segment register — fs on x86-64 Linux and FreeBSD. Code that reads a thread-local reads fs:[offset].

On x86-64 Windows the host TEB uses gs; a guest fs access does not address the guest TLS block. Patching guest instructions is entirely about the fix.

Learn more · linking, PIC, GOT and PLT
  • Article
    Eli Bendersky — Position Independent Code in shared librariesThe clearest explanation anywhere of why the GOT and PLT exist and how they work, built up from disassembly. If one link in this book is worth reading before continuing, it is this one — the GOT is the exact hook KytyPS5's entire HLE design hangs on.
  • Article
    …and the x64 follow-upThe same material for x86-64, which is the architecture that actually matters here — RIP-relative addressing changes the picture noticeably.
  • Spec
    ELF specification (generic ABI)The section and segment structures of this topic, minus Sony's additions. Worth having open when reading src/loader/elf.h.

Sony's executable format is standard ELF64 wrapped in a container, with a set of custom type and tag values bolted on. Here is the complete set Kyty understands.

Directories and ZArchive content

--game accepts a directory, ELF or .zar ZArchive with eboot.bin at its root. common/archive.cpp exposes virtual paths such as game.zar!/sce_sys/param.json through Common::File. A retained ArchiveReader shares the index and block cache while a mount or launcher scan is active. This is read-only game content; saves and temporary files use writable host locations.

Windows starts through wmain, converts arguments to UTF-8 and preserves native std::filesystem::path values through file operations. Guest paths without a matching mount stay outside the host namespace. The launcher uses gameContent.cpp to inspect folders and archives uniformly, including icons, title metadata and game size.

libSaveData.cpp persists SaveDataMemory2 data and allocation metadata on disk, including search/mount capacity and timestamps. Save-data dialog locking and CP932 conversion have dedicated regression tests. Treat these as observable guest behavior when debugging missing progress or stuck dialogs.

The SELF container

eboot.bin starts with a small header describing compressed or encrypted segments, followed by the ELF proper:

src/loader/elf.h
struct SelfHeader {
    uint8_t  ident[12];
    uint16_t size1;
    uint16_t size2;
    uint64_t file_size;
    uint16_t segments_num;
    uint16_t unknown;
    uint32_t pad;
};

struct SelfSegment {
    uint64_t type;
    uint64_t offset;
    uint64_t compressed_size;
    uint64_t decompressed_size;
};

Elf64::IsSelf() tests for the container; IsNextGen() distinguishes PS5-era files from PS4-era ones. Retail files are encrypted, which is why the project cannot and does not distribute games — you supply files you already have the right to use.

The ELF header values that are not standard

FieldValueMeaning
e_ident[EI_OSABI]ELFOSABI_FREEBSD = 9Confirms the FreeBSD heritage.
e_machineEM_X86_64 = 62x86-64, as expected.
e_typeET_DYNEXEC = 0xfe10Sony-specific: a main executable that is nevertheless position-independent.
e_typeET_DYNAMIC = 0xfe18Sony-specific: a shared module.

Custom segment types

p_typeValueContents
PT_LOAD1Standard: map this into memory.
PT_DYNAMIC2Standard: the dynamic tag array.
PT_TLS7Standard: the thread-local storage template image.
PT_OS_DYNLIBDATA0x61000000Sony: a blob holding the symbol table, string table and relocation tables. The dynamic tags below index into this blob, not into the file.
PT_OS_PROCPARAM0x61000001Sony: the process-parameter block the runtime reads at start-up.
PT_OS_RELRO0x61000010Sony: data that becomes read-only after relocation. Kyty maps it like PT_LOAD.

Custom dynamic tags

Standard ELF has DT_SYMTAB, DT_STRTAB, DT_RELA and so on. Sony duplicated the whole set in the OS-specific range so they can refer into PT_OS_DYNLIBDATA:

TagValuePurpose
DT_OS_SYMTAB / SYMTABSZ0x61000039 / 0x6100003fDynamic symbol table and its size.
DT_OS_STRTAB / STRSZ0x61000035 / 0x61000037String table holding NIDs and library names.
DT_OS_RELA / RELASZ / RELAENT0x6100002f / 31 / 33General relocations.
DT_OS_JMPREL / PLTRELSZ0x61000029 / 0x6100002dPLT relocations — the function imports.
DT_OS_PLTGOT0x61000027Address of the Global Offset Table.
DT_OS_MODULE_INFO0x6100000dName and version of a module this file exports.
DT_OS_NEEDED_MODULE0x6100000fName and version of a module this file imports.
DT_OS_EXPORT_LIB / IMPORT_LIB0x61000013 / 0x61000015Library names and versions on each side.
DT_OS_FINGERPRINT0x61000007Build identifier.

The module and library tables matter more than they look. A NID is only unique within a library, so resolution needs all three parts. GetDynModules and GetDynLibs unpack these into DynamicInfo:

src/loader/runtimeLinker.h — abbreviated
struct DynamicInfo {
    char*       str_table;      uint64_t str_table_size;
    Elf64_Sym*  symbol_table;   uint64_t symbol_table_total_size;
    Elf64_Rela* rela_table;     uint64_t rela_table_total_size;
    Elf64_Rela* jmprela_table;  uint64_t jmprela_table_size;
    uint64_t    init_vaddr, fini_vaddr;
    uint64_t    init_array_vaddr, init_array_size;
    std::vector<ModuleId>    export_modules, import_modules;
    std::vector<LibraryId>   export_libs,    import_libs;
};

The encoded identifiers

Module and library identifiers in a NID string are not names — they are small integers encoded in a base64-like alphabet. EncodeId64 converts an index into the one- or two-character form, and resolution then looks that up in the file's own import tables to find the real name and version. This is why the same NID string can mean different things in two different modules.

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

A module laid out segment by segment. Watch which parts a stock loader understands and which parts only Sony's loader would know what to do with.

Peel the container apart layer by layer. Watch for the last one — that is where the imports went.

Try
Use one of your own

The module inspector in the Playground parses a real module in your browser — drop in an eboot.bin or a .sprx from a game you have dumped and it will name the Sony segments and dynamic tags for you. Nothing is uploaded.

Do

Do this one with a real file in front of you. It is far more convincing than a diagram.

Checkpoint