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.

Read

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:

src/loader/runtimeLinker.h
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.

Current wait and fiber behavior

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.

Watch

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.

Try

Step through what each new thread actually receives, and where the zero-filled tail comes from.

Do

The ABI wall shows up here in reverse — host code calling into guest code — which is the harder direction.

Checkpoint