A global variable has one copy for the whole process. The moment two threads write it, your program is guessing. Thread-local storage gives every thread its own copy of a variable, declared once and used like a normal static: __declspec (thread) int tlsFlag = 1; — the exact example from the PE/COFF specification.
The format supports this with a dedicated structure. The loader reads it, hands each thread a private block of memory, seeds it from a template, and (optionally) runs your callback functions at thread start and exit. Compilers, runtimes, and even the operating system lean on it: exception state, locale, random seeds, and C run-time per-thread data all live here.
Data directory index 9 (TLS Table) points at the TLS directory. An image with no static TLS simply leaves that slot empty — our cli-64.exe does exactly that — while conpty.dll carries a full one.
Think of TLS as a photocopy machine with a master sheet:
The model's missing piece: the TLS directory does not use relative addresses. Its pointers are full virtual addresses, which ties the structure to the load address and forces the file to carry base relocations for it.
The specification chapter "The .tls Section" gives the six fields (PE32+ sizes):
| Offset | Size | Field | Spec rule |
|---|---|---|---|
| 0 | 8 | Raw Data Start VA | Start of the template — a VA, not an RVA; there should be a base relocation for it in .reloc |
| 8 | 8 | Raw Data End VA | Last byte of the template, also a VA |
| 16 | 8 | Address of Index | Location in an ordinary data section where the loader assigns the TLS index |
| 24 | 8 | Address of Callbacks | Pointer to a null-terminated array of callback functions; an empty list is valid |
| 32 | 4 | Size of Zero Fill | Extra bytes beyond the template, zero-filled for each thread |
| 36 | 4 | Characteristics | Bits [23:20] hold IMAGE_SCN_ALIGN_* alignment; the other 28 bits are reserved |
The spec also spells out how code reaches its per-thread data: the linker places the Address of Index so the runtime can read the index the loader writes there; the loader publishes each thread TLS array through the thread environment block (the spec notes the FS register and the 0x2C offset as Intel x86-specific details); code then indexes that array to find this module's data area for the current thread, and every variable is a fixed offset into that area.
Callback functions take the same three parameters as a DLL entry-point function — DLL handle, reason, reserved — and the reason values are DLL_PROCESS_DETACH (0), DLL_PROCESS_ATTACH (1), DLL_THREAD_ATTACH (2), and DLL_THREAD_DETACH (3). Multiple callbacks run in array order; a null pointer ends the list, and the spec states it is perfectly valid to have an empty list whose only member is that null pointer.
conpty.dll declares TLS directory index 9 at RVA 0x14180, size 0x28, which lands at file offset 0x12D80 in .rdata. Decoding all six fields:
00012d80: 404b 0180 0100 0000 484b 0180 0100 0000 @K......HK...... 00012d90: 888a 0180 0100 0000 7814 0180 0100 0000 ........x....... 00012da0: 0000 0000 0000 3000 ......0.
Walk-through:
00 00 00 00 00 00 00 80 (file 0x13740), the initial value this module gives each thread block..data. That section has VirtualSize 0xC30 but SizeOfRawData only 0x400, and the index sits at offset 0xA88 — beyond the raw bytes. Per the spec, the remainder is zero-filled in memory, so the slot exists at runtime but not on disk, waiting for the loader to write the assigned index.00010078: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00013740: 0000 0000 0000 0080 ........
The spec also carries a practical warning: before Windows Vista, static TLS was unreliable in DLLs loaded with LoadLibrary; later loaders can allocate TLS slots for such DLLs at load time. If you ship static TLS in a DLL, that history is part of your compatibility story.
On Windows, dumpbin shows the whole structure and the callback addresses (Microsoft, "dumpbin /TLS"):
dumpbin /TLS conpty.dllOn Linux or macOS, dump the directory bytes and decode six little-endian fields (the last two are 32-bit):
$ xxd -s 0x12d80 -l 40 conpty.dll
00012d80: 404b 0180 0100 0000 484b 0180 0100 0000 @K......HK......
00012d90: 888a 0180 0100 0000 7814 0180 0100 0000 ........x.......
00012da0: 0000 0000 0000 3000 ......0.What to look for: the top byte of each VA is 0x01 — the images live at 0x180000000 and 0x140000000 style bases, so the high byte of a 64-bit little-endian pointer shows up at the far right of its group. And if the last two bytes decode to 0x00300000, you have just read the alignment field.
Without looking back: whose job is the value stored at Address of Index, and where does it live?
A TLS directory has Characteristics = 0x00300000. What do bits [23:20] say, according to the spec?
The pattern: mask the four bits, shift them down 20, and match the enum — the same bit-reading habit every PE flag needs.
TLS describes per-thread data and startup hooks. The next table describes something much less friendly: how the processor is supposed to rebuild a stack that has already crashed.
Next: Exception Tables (.pdata) — RUNTIME_FUNCTION entries that make x64 unwinding work.