6.1 KiB
Reading titles and libraries
Many exports have no source that gives their signature or behaviour. Two places still hold them: the code of a title that calls the export (its call sites) and the code of the library itself. This page covers how to read both, what each one settles, and how to cite it.
Sources, strongest first
- A console measurement: a homebrew payload that calls the function and records what it returns, with the
firmware version (for example
sceVoiceQoSEnd, #1407). - The library's own code: a firmware module, or a copy of an SDK module that a title ships in
sce_module/. It gives behaviour, error codes, layouts and validation order (libSceJobManager, libSceRtc, libSceHttp). A module a title ships is not reimplemented (CONTRIBUTING); it can still be read to learn how a system library behaves. - Other implementations and SDK bindings: KytyPS5, shadPS4, fpPS4, SharpProspero, Orbital. PS4 sources may differ from the PS5 ABI.
- A title's call sites: the arguments, their widths, buffer sizes, which pointers are outputs and which results the title checks. They show what the caller expects, not what the library does.
When sources disagree, say so in the pull request and in the debt entry.
What a call site settles
The PS5 uses the System V AMD64 calling convention: integer and pointer arguments in rdi, rsi, rdx, rcx, r8,
r9, then the stack; floating-point arguments in xmm0-xmm7; the result in rax or xmm0.
| Seen at the call site | Settles |
|---|---|
Registers written right before the call |
The argument count. A register set long before the call, or for another use, is not an argument; check several call sites |
mov %esi / mov %rsi, movzwl, movsbl |
The argument width and signedness |
lea -0x2030(%rbp), %rsi |
A pointer to a caller buffer; the stack layout gives its size |
| A value stored at the pointer before the call and read after it | An in/out argument (capacity in, size out) |
test %eax, %eax / cmp, js after the call |
An int32_t result and which values the title treats as failure |
| The result is never read | Nothing: the return type is not established (libSceAgc) |
A log string that names the function ("sceXxx() failed. Error code %#x") |
Which import the call is, and that failures are SCE error codes |
| A first call with a null buffer, then a second one with a buffer of the returned size | A size query (libSceNpTrophy2) |
A call site doesn't settle what the library writes, which error codes it returns, its side effects, or the values a
title happens to accept. "The title continues if it gets 0" is not "the console returns 0". Everything that the call
site and the other sources leave open throws (NotImplemented_nid_no_patch), as in
libSceAudioIn: the signature came from a call site, the meaning of the result did not, so the
export still throws. A silent stub is allowed only when it affects the UI (CONTRIBUTING): a
read that always reports no data, or a write that drops its data, is not UI.
Finding the call sites
-
Unwrap the title's decrypted
eboot.binwithtools/unwrap_self.py(GAME_TESTING). -
Relink it with
--registry(USAGE).app.registry.jsonlists every import of the executable with itsnid,relocationType,targetOffset(the GOT slot) andcallSites(virtual addresses). Bundled modules' registries have nocallSites.tools/nid_names.py --name sceXxxgives the NID of a name, and--nidthe name of a NID (without the#suffix).R_X86_64_GLOB_DAT: each call site is thecall *slot(%rip)itself.R_X86_64_JUMP_SLOT: the call site is the PLT stub (jmp *slot(%rip)); the callers are thecall <stub>instructions.
-
Disassemble around the site. The ELF has no section headers, so
objdump -dprints nothing; take the executableLOADsegment fromreadelf -lW, cut it out at its file offset and disassemble it at its virtual address:readelf -lW eboot.elf # executable LOAD: offset 0x4000, vaddr 0x0, filesz 0x1f36afc tail -c +$((0x4000 + 1)) eboot.elf | head -c $((0x1f36afc)) > text.bin objdump -D -b binary -m i386:x86-64 --adjust-vma=0x0 text.bin > text.dis grep -nE 'call +0x1f2e480$' text.dis # callers of the JUMP_SLOT stub at 0x1f2e480A disassembler that loads the program headers (Ghidra, IDA) does the same.
-
Log strings near the call confirm which import it is:
strings -t x eboot.elf, then theleathat loads that address.
The same steps work on a module: the code of a firmware module or of one in sce_module/ gives the library's side of
the contract.
Citing it
In the pull request and in the library's debt entry, say:
- Where it came from: the title ID and the call-site addresses (
PPSA03393, 0x76D132), or the module and firmware version (libSceRtc.sprx, firmware 13.60). - What each site showed: "
rsipoints to a 0x2000-byte stack buffer andrdxto its capacity, which the title reads back as the size". - What was not verified: "read from disassembly, not measured on a console", "the return value is not read".
Quote only the instructions that show the claim, not whole functions. The code stays generic: a title ID belongs in the debt entry, never in code or test names.
Rules
- Work only from dumps of titles and firmware that you own. Never add, attach or link game files, firmware, decrypted binaries or keys in the repository, a pull request or an issue.
- Write the implementation from the contract you read (arguments, sizes, values, order, errors), not by translating the disassembly instruction by instruction or copying its structure. Rebuild data tables from a public source when one exists (libSceIme rebuilds its codepage checks from Unicode's tables).
- The goal is interoperability: what a title needs from a system library to run. Nothing here is for bypassing licence, entitlement or online checks.