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.
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:
| View | Described by | Used by |
|---|---|---|
Sections (.text, .data, .rodata…) | Section headers | The linker at build time. Often stripped from shipped binaries. |
Segments (PT_LOAD, PT_DYNAMIC, PT_TLS…) | Program headers | The 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.
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:
| Type | Meaning | Computation |
|---|---|---|
R_X86_64_RELATIVE | An internal pointer that just needs the load bias added. | *where = base + addend |
R_X86_64_64 | A 64-bit absolute reference to a symbol. | *where = symbol + addend |
R_X86_64_GLOB_DAT | A Global Offset Table entry for a data symbol. | *where = symbol |
R_X86_64_JUMP_SLOT | A Procedure Linkage Table entry for a function. | *where = symbol |
R_X86_64_DTPMOD64 | Thread-local storage: which module owns this variable. | module ID |
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.
- ArticleEli 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.
- SpecELF 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:
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
| Field | Value | Meaning |
|---|---|---|
e_ident[EI_OSABI] | ELFOSABI_FREEBSD = 9 | Confirms the FreeBSD heritage. |
e_machine | EM_X86_64 = 62 | x86-64, as expected. |
e_type | ET_DYNEXEC = 0xfe10 | Sony-specific: a main executable that is nevertheless position-independent. |
e_type | ET_DYNAMIC = 0xfe18 | Sony-specific: a shared module. |
Custom segment types
p_type | Value | Contents |
|---|---|---|
PT_LOAD | 1 | Standard: map this into memory. |
PT_DYNAMIC | 2 | Standard: the dynamic tag array. |
PT_TLS | 7 | Standard: the thread-local storage template image. |
PT_OS_DYNLIBDATA | 0x61000000 | Sony: 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_PROCPARAM | 0x61000001 | Sony: the process-parameter block the runtime reads at start-up. |
PT_OS_RELRO | 0x61000010 | Sony: 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:
| Tag | Value | Purpose |
|---|---|---|
DT_OS_SYMTAB / SYMTABSZ | 0x61000039 / 0x6100003f | Dynamic symbol table and its size. |
DT_OS_STRTAB / STRSZ | 0x61000035 / 0x61000037 | String table holding NIDs and library names. |
DT_OS_RELA / RELASZ / RELAENT | 0x6100002f / 31 / 33 | General relocations. |
DT_OS_JMPREL / PLTRELSZ | 0x61000029 / 0x6100002d | PLT relocations — the function imports. |
DT_OS_PLTGOT | 0x61000027 | Address of the Global Offset Table. |
DT_OS_MODULE_INFO | 0x6100000d | Name and version of a module this file exports. |
DT_OS_NEEDED_MODULE | 0x6100000f | Name and version of a module this file imports. |
DT_OS_EXPORT_LIB / IMPORT_LIB | 0x61000013 / 0x61000015 | Library names and versions on each side. |
DT_OS_FINGERPRINT | 0x61000007 | Build 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:
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.
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.
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 this one with a real file in front of you. It is far more convincing than a diagram.
Checkpoint