Step 20 · Loading and execution
Threads and TLS
There is no scheduler here. A guest thread is a host thread, and the host OS runs it. The work is in setting up the world that thread expects before its first instruction — thread-local storage, a stack, and a correct ABI transition.
Before this page: Submitted is not completed · Virtual memory, briefly. View the learning route.
Guest threads are real host threads — but with two pieces of state Kyty has to maintain itself, and a set of FreeBSD semantics that do not come free.
What pthread.cpp implements
The file covers: threads with names, priorities and CPU affinity masks; mutexes in all four types with protocol attributes; condition variables; read-write locks (including try/timed variants); umtx wait/wake; thread-specific keys with destructors; cancellation in both deferred and asynchronous modes; per-thread signal queues; and the timing calls (KernelUsleep, KernelNanosleep, clock queries). On Linux and macOS every guest thread also installs a 64 KB alternate signal stack so a GPU-tracking fault can still be delivered when the guest stack itself is write-protected.
Both the Sony-prefixed and POSIX-named entry points are registered, because games use both:
LIB_FUNC("7H0iTOciTLo", Posix::pthread_mutex_lock);
LIB_FUNC("2Z+PpY6CaJg", Posix::pthread_mutex_unlock);
LIB_FUNC("IafI2PxcPnQ", LibKernel::PthreadMutexTimedlock);
LIB_FUNC("Op8TBGY5KHg", Posix::pthread_cond_wait);
Guest stacks
A guest thread cannot simply use the host thread's stack. Guest code computes addresses relative to its own stack, and those addresses may be handed to the GPU or stored in structures Kyty inspects — so they must lie inside the guest address bands:
void PthreadInitSelfForMainThread(); void* PthreadCreateMainGuestStack(); bool PthreadGetGuestStack(Pthread thread, uint64_t* stack_addr, uint64_t* stack_size);
The guest stack is allocated with AllocateGuestStackMemory, and RunEntry switches to it before calling the entry point. The entry parameters are placed at the top of that stack, 16-byte aligned as the ABI requires:
auto* params = reinterpret_cast<EntryParams*>(
(reinterpret_cast<uintptr_t>(main_stack_top) - 0x100u) & ~0x0fu);
params->argc = 1;
params->argv[0] = "KytyEmu";
Per-thread TLS blocks
Each loaded module has a TLS template from its PT_TLS segment. Every thread needs its own copy of every module's template. Kyty allocates them lazily:
struct ThreadLocalStorage {
struct Block {
uint8_t* ptr = nullptr;
application_heap_free_func_t free_func = nullptr; // the game's own free()
bool vm_alloc = false;
uint64_t alloc_size = 0;
};
uint64_t image_vaddr; // the template
uint64_t init_size; // bytes to copy from it
uint64_t image_size; // total per-thread size
uint64_t tcb_offset; // where the control block sits within the block
uint64_t handler_vaddr; // the generated stub from Patching guest instructions
std::vector<uint8_t> init_image;
std::unordered_map<int, Block> tlss; // thread id → block
Common::Mutex mutex;
};
Note free_func. If the game has installed its own heap through SetApplicationHeapApi, TLS blocks are allocated from that heap and must be released through it — freeing them with delete[] would corrupt the game's allocator. FreeTlsBlock handles all three cases (game heap, guest VM allocation, plain new[]).
Blocks are released per thread on exit via DeleteTlss(thread_id), and per module on unload via DeleteTls(program, thread_id).
The application heap bridge
Games can install their own allocator, which the runtime is then expected to use:
void SetApplicationHeapApi(void* const api[10]); void* ApplicationHeapMalloc(uint64_t size); void* ApplicationHeapMemalign(uint64_t alignment, uint64_t size);
An array of ten function pointers, of which Kyty uses malloc, free and posix_memalign. This matters because memory Kyty allocates on the game's behalf may be freed by the game, and vice versa.
Timed _umtx_op waits use shared SyncOnAddress deadlines and pending-signal dispatch. Pthread key values and wakeup state are maintained per thread. Fiber context uses native TLS when execution migrates between threads; small guest stacks are preserved.
Short sleeps block too
KernelUsleep() and KernelNanosleep() use signal-polling sleep helpers. Nonzero short waits use blocking waits with signal polling. Windows uses its high-resolution waitable timer path; POSIX uses an absolute monotonic deadline that survives signal interruptions. Zero-duration requests still have their explicit signal-dispatch behavior.
Which threads exist in a running emulator and what each is responsible for. Note that the GPU work does not happen on the guest's threads.
Step through what each new thread actually receives, and where the zero-filled tail comes from.
The ABI wall shows up here in reverse — host code calling into guest code — which is the harder direction.
Checkpoint