How SharpEmu Manages Thread-Local Storage (TLS) for Guest Threads

SharpEmu isolates guest thread-local storage from the host by maintaining a process-wide registry of TLS templates and per-thread Dynamic Thread Vectors (DTV) that emulate the ELF TLS Variant II model used on AMD64 systems.

SharpEmu is an AMD64 emulator that faithfully reproduces the Linux ELF TLS Variant II layout for emulated processes. According to the par274/sharpemu source code, the emulator manages thread-local storage for guest threads through a dedicated GuestTlsTemplate class that maintains static reservations below the thread pointer while keeping the host's own TLS completely separate.

Guest TLS Registry Architecture

All TLS-related data lives in src/SharpEmu.HLE/GuestTlsTemplate.cs【15-30】, which implements a registry that tracks both static template information and per-thread runtime state.

Process-Wide Template Registry

The GuestTlsTemplate class maintains a SortedDictionary<ulong, ModuleTemplate> _modules that stores each loaded module's PT_TLS image, size, alignment, and calculated static offset【22-23】. A StartupStaticTlsReservation of 0x10000 bytes guarantees address space below the thread pointer for all modules loaded before thread creation【20-21】.

The registry tracks global changes through a _generation counter that increments on every module registration or reset, forcing stale DTVs to rebuild when a thread's view becomes outdated【26-27】.

Per-Thread Dynamic Thread Vectors

For runtime resolution, the template maintains Dictionary<ulong, ThreadDtv> _threadDtvs, which maps a guest thread-pointer (the FS base) to a per-thread DTV containing the actual TLS block addresses for that specific thread【23-48】. This design allows each guest thread to hold independent views of dynamically allocated TLS blocks while sharing static reservations.

Registering TLS Modules at Load Time

When the ELF loader discovers a PT_TLS segment, it calls GuestTlsTemplate.RegisterModule【30-36】. The method normalizes alignment to a power-of-two【42-46】, calculates the static offset using CalculateStaticOffset for Variant II layouts【68-78】, and fails fast if the required block exceeds the startup reservation【74-78】.

The registration process updates _staticTlsSize, _maximumAlignment, and bumps _generation before storing the module template【80-92】. The static offset represents the distance below the thread pointer where the module's TLS starts.

// Example: registering the main executable's TLS (normally done by the loader)
GuestTlsTemplate.RegisterModule(
    moduleId: 1,
    initImage: initBytes,
    memorySize: 0x200,
    alignment: 0x10);

Initializing Guest Thread Contexts

When a new guest thread is created, the runtime calls GuestTlsTemplate.SeedThreadBlock【34-36】. This method locks the global registry, clears any previous DTV for the same thread pointer (handling thread reuse)【44-47】, and creates a fresh ThreadDtv populated with static or dynamic entries for every registered module【49-55】.

The method then calls RebuildGuestDtv to allocate the DTV structure in host memory and writes its address into the guest's Thread Control Block (TCB) at threadPointer + sizeof(ulong)【55-56】【89-93】. After seeding, the guest thread possesses a fully populated DTV that the emulated __tls_get_addr can query.

// After creating a host thread that will run guest code:
ulong guestThreadPointer = /* value set as FS base in the guest CPU context */;
GuestTlsTemplate.SeedThreadBlock(cpuContext, guestThreadPointer);

Resolving TLS Addresses at Runtime

The emulated implementation of __tls_get_addr forwards to GuestTlsTemplate.ResolveAddress【62-66】. The resolution logic:

  1. Returns 0 if the thread's FS base is unset or the module ID is 0.
  2. Validates the module template and rejects out-of-range offsets.
  3. Retrieves or lazily creates the thread's ThreadDtv; if the module is missing, it allocates a dynamic entry via CreateDynamicEntry and adds it to the DTV【89-98】.
  4. Rebuilds the DTV if the global generation has changed or new entries were added【100-103】.
  5. Returns entry.Address + offset as the absolute guest address【105-106】.
// This is the stub emitted for __tls_get_addr
ulong address = GuestTlsTemplate.ResolveAddress(cpuContext, moduleId, offset);
// Returns the absolute address inside the guest's address space

Static vs. Dynamic TLS Allocation

Static TLS follows the Variant II model where blocks are allocated once below the thread pointer and shared across all threads. The CalculateStaticOffset method determines these positions at load time【10-14】【16-18】.

Dynamic TLS handles modules loaded after thread creation (e.g., via dlopen). These blocks are allocated on the host heap using Marshal.AllocHGlobal, zero-initialized, and copied with the module's init image before their addresses are stored in the DTV entry【28-44】. This separation ensures that late-loading does not corrupt existing thread contexts.

Host TLS Isolation

SharpEmu strictly separates guest TLS from the host's own thread-local storage through platform-specific IHostThreading implementations. Windows uses native TlsAlloc and TlsFree operations found in src/SharpEmu.HLE/Host/Windows/WindowsHostThreading.cs【51-57】, while POSIX systems use pthread-based stubs located in src/SharpEmu.HLE/Host/Posix/PosixHostThreading.cs【8-14】.

These host-side slots are completely independent of the guest TLS managed by GuestTlsTemplate, preventing collisions between emulator internals and emulated program state.

// Allocate a host-side TLS slot for SharpEmu internal use
uint slot = hostThreading.AllocateTlsSlot();
hostThreading.SetTlsValue(slot, (nint)somePointer);
nint value = hostThreading.GetTlsValue(slot);

Summary

  • Process-wide registry: GuestTlsTemplate in src/SharpEmu.HLE/GuestTlsTemplate.cs maintains a global view of all TLS modules with a 64KB static reservation.
  • Per-thread DTVs: Each guest thread receives a dedicated Dynamic Thread Vector stored in host memory and referenced from the guest TCB.
  • Variant II compliance: Static offsets are calculated below the thread pointer, while dynamic allocations use host heap memory for late-loaded modules.
  • Runtime resolution: ResolveAddress handles __tls_get_addr calls with generation tracking to ensure consistency across module loading events.
  • Host isolation: Platform-specific IHostThreading implementations in WindowsHostThreading.cs and PosixHostThreading.cs keep emulator TLS separate from guest memory.

Frequently Asked Questions

How does SharpEmu prevent conflicts between host and guest TLS?

SharpEmu uses completely separate mechanisms for host and guest thread-local storage. The guest uses the GuestTlsTemplate registry and DTV system in src/SharpEmu.HLE/GuestTlsTemplate.cs, while the host uses native platform APIs like TlsAlloc on Windows or pthread TLS on POSIX systems. These host implementations live in WindowsHostThreading.cs and PosixHostThreading.cs and never interact with the guest's FS base or thread pointer.

What is the Dynamic Thread Vector (DTV) in SharpEmu?

The DTV is a per-thread structure that maps module IDs to their specific TLS block addresses for that thread. Stored in Dictionary<ulong, ThreadDtv> _threadDtvs and keyed by the thread pointer (FS base), it contains entries for both static allocations (below the thread pointer) and dynamic allocations (host heap). The DTV is rebuilt when the global generation counter changes, ensuring threads see consistent TLS layouts after new modules are loaded.

How does SharpEmu handle TLS for modules loaded after thread creation?

For modules loaded via dlopen after a thread has started, SharpEmu allocates dynamic TLS blocks on the host heap using Marshal.AllocHGlobal. When ResolveAddress encounters a missing module in a thread's DTV, it calls CreateDynamicEntry to allocate and initialize the block, then rebuilds the DTV to include the new entry. This complies with the ELF TLS Variant II requirement that late-loaded modules receive per-thread storage without moving existing static allocations.

What happens when the static TLS reservation is exhausted?

The RegisterModule method calls CalculateStaticOffset and explicitly checks if the required static block exceeds StartupStaticTlsReservation (0x10000 bytes)【74-78】. If the calculation would overflow this reservation, the method fails fast, preventing memory corruption. Modules that would exceed this limit must use dynamic TLS allocation instead, which is not subject to the static reservation size.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →