Thread-local storage gives each thread its own copy of some variables, those declared
__declspec(thread) in Visual C++ or thread_local in C++. The
image holds a template of their starting values, and the loader makes a copy for each
thread. Data directory 9 points at the TLS directory, which describes the template and
lists the TLS callbacks, functions that run whenever a thread starts or ends. 152 of
the PE files examined for this site had a TLS directory, and 48 had callbacks.
The TLS directory
IMAGE_TLS_DIRECTORY32, 24 bytes. All but the last two fields are virtual addresses, the image base included, not RVAs; they are covered by base relocations like any other address.
| Offset | Size | Type | Field | Meaning |
|---|---|---|---|---|
| 0 | 4 | u32 | StartAddressOfRawData | The address of the start of the template. |
| 4 | 4 | u32 | EndAddressOfRawData | The address of its end, not counting the zero fill. |
| 8 | 4 | u32 | AddressOfIndex | Where the loader writes the TLS index: the slot the module’s data has in each thread’s array of TLS blocks. |
| 12 | 4 | u32 | AddressOfCallBacks | The address of the callback array. |
| 16 | 4 | u32 | SizeOfZeroFill | How many zero bytes follow the template in each thread’s copy. |
| 20 | 4 | u32 | Characteristics | Bits 20–23 give the alignment of the block, as IMAGE_SCN_ALIGN_* values; the other bits are reserved. |
To turn one of these addresses into a file offset, take away ImageBase to get an RVA, then map the RVA through the section table.
The template
The bytes from StartAddressOfRawData to EndAddressOfRawData are the starting values
of the thread-local variables, usually in a .tls section. For each new
thread the loader allocates a block of the template’s size plus SizeOfZeroFill, copies
the template into it and clears the rest. Code reaches a thread’s block through the
thread environment block: on x86, fs:[0x2C] holds the pointer to the
thread’s array of TLS blocks, and the TLS index picks this module’s block from it.
TLS callbacks
AddressOfCallBacks points at an array of 4-byte addresses of functions, ending with 0. Each callback has the prototype of a DLL entry point:
void NTAPI callback(PVOID DllHandle, DWORD Reason, PVOID Reserved)
The loader calls each one when the process starts and ends and when each thread
starts and ends. For the process start, that is before the program’s entry point, which
is why debuggers and analysts look for them: code can run there before
main is reached.
The explorer names the callbacks “TLS callback 0”, “TLS callback 1” and so on in the disassembly, and lists them in the Program tab. Borland’s linkers often point AddressOfCallBacks at a zero inside the TLS directory itself, which is an empty list.