Thread-local storage on x86-64 and x86
Every thread has its own copy of a thread-local variable, and the code must find the copy of the thread that runs it. On x86 a segment register does most of the work, but which register, and what it points to, differ from system to system.
C11’s _Thread_local, C++’s thread_local, GCC’s __thread and
MSVC’s __declspec(thread) all declare a variable with one copy per
thread. An ordinary global has one address that the linker can put into
the code; a thread-local variable has as many addresses as there are
threads, so the code has to find the one of the running thread.
x86 has a register made for that. In 64-bit mode the segments are flat, all but FS and GS, whose base address the operating system sets for each thread. An instruction with an FS or GS prefix adds that base to its address, so the same instruction reaches a different copy in each thread.
| System | Register | Its base points to |
|---|---|---|
| Linux, FreeBSD and the other psABI systems, x86-64 and x32 | FS | the thread control block (TCB) |
| The same systems on i386 | GS | the TCB |
| macOS x86-64 | GS | the thread-specific data of the thread’s pthread |
| Windows x64 | GS | the thread environment block (TEB) |
| Windows x86 | FS | the TEB |
Some compilers leave the register aside for thread-local variables and emulate them instead: MinGW-w64 GCC on Windows and Clang for OpenBSD.
Each kernel uses the other register for itself: 64-bit Linux reaches its per-CPU data through GS, which it swaps with the user value on entry (SWAPGS), and 32-bit Linux through FS.
The thread pointer
The x86-64 psABI makes FS the thread pointer, and on i386 Drepper’s document on ELF TLS does the same with GS. The TCB starts with its own address, so reading offset 0 through the register gives the thread pointer as an ordinary address, which code needs as soon as it takes the address of a thread-local variable.
__builtin_thread_pointer() reads the first word of the TCB: through FS for x86-64 and x32, through GS for i386, on Linux and FreeBSD alike.tls-pointer.c
/* The thread pointer: the address of the running thread's control
block, read through the segment register of thread data. */
void *thread_pointer(void)
{
return __builtin_thread_pointer();
}
GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu
AT&T syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -S tls-pointer.c
thread_pointer:
movq %fs:0, %rax
retIntel syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S tls-pointer.c
thread_pointer:
mov rax, QWORD PTR fs:0
retGCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnux32
AT&T syntax gcc-14 -mx32 -O2 -fno-asynchronous-unwind-tables -S tls-pointer.c
thread_pointer:
movl %fs:0, %eax
retIntel syntax gcc-14 -mx32 -O2 -fno-asynchronous-unwind-tables -masm=intel -S tls-pointer.c
thread_pointer:
mov eax, DWORD PTR fs:0
retGCC 14.2.0 (Debian 14.2.0-19) i386-linux-gnu
AT&T syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -S tls-pointer.c
thread_pointer:
movl %gs:0, %eax
retIntel syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -masm=intel -S tls-pointer.c
thread_pointer:
mov eax, DWORD PTR gs:0
retClang 23.1.2 x86_64-unknown-freebsd14
AT&T syntax clang --target=x86_64-unknown-freebsd14 -O2 -fno-asynchronous-unwind-tables -S tls-pointer.c
thread_pointer:
pushq %rbp
movq %rsp, %rbp
movq %fs:0, %rax
popq %rbp
retqIntel syntax clang --target=x86_64-unknown-freebsd14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S tls-pointer.c
thread_pointer:
push rbp
mov rbp, rsp
mov rax, qword ptr fs:[0]
pop rbp
retClang 23.1.2 i686-unknown-freebsd14
AT&T syntax clang --target=i686-unknown-freebsd14 -march=i686 -O2 -fno-asynchronous-unwind-tables -S tls-pointer.c
thread_pointer:
pushl %ebp
movl %esp, %ebp
movl %gs:0, %eax
popl %ebp
retlIntel syntax clang --target=i686-unknown-freebsd14 -march=i686 -O2 -fno-asynchronous-unwind-tables -masm=intel -S tls-pointer.c
thread_pointer:
push ebp
mov ebp, esp
mov eax, dword ptr gs:[0]
pop ebp
retThe C library sets the base for each thread it creates. On 64-bit
Linux a program can read and write it with arch_prctl()
(ARCH_GET_FS, ARCH_SET_FS), or with the FSGSBASE instructions,
such as RDFSBASE and WRFSBASE, where the kernel has enabled them and
says so with HWCAP2_FSGSBASE in the auxiliary vector. A new thread
gets its base from the tls argument of
clone, with the flag
CLONE_SETTLS: the FS base itself for a 64-bit program, and for a
32-bit one a struct user_desc, the segment descriptor that
set_thread_area also takes.
What glibc keeps in the TCB
The TCB belongs to the C library. glibc starts it with a header that holds, among other things, the TCB’s own address at offset 0, a pointer to the dynamic thread vector, and the canary of the stack protector: at offset 0x28 on x86-64 and 0x14 on i386. Each new thread copies the canary from the thread that creates it.
%fs:40 or %gs:20. Clang for Windows XORs the global __security_cookie with RSP; MinGW-w64 GCC and Clang for macOS load a global __stack_chk_guard.tls-guard.c
/* With -fstack-protector-strong, a function with an array on the
stack checks a canary before it returns. */
extern void fill(char *buf);
int first(void)
{
char buf[64];
fill(buf);
return buf[0];
}
GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu
AT&T syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -fstack-protector-strong -S tls-guard.c
first:
subq $88, %rsp
movq %fs:40, %rdi
movq %rdi, 72(%rsp)
movq %rsp, %rdi
call fill@PLT
movsbl (%rsp), %eax
movq 72(%rsp), %rdx
subq %fs:40, %rdx
jne .L5
addq $88, %rsp
ret
.L5:
call __stack_chk_fail@PLTIntel syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -fstack-protector-strong -masm=intel -S tls-guard.c
first:
sub rsp, 88
mov rdi, QWORD PTR fs:40
mov QWORD PTR 72[rsp], rdi
mov rdi, rsp
call fill@PLT
movsx eax, BYTE PTR [rsp]
mov rdx, QWORD PTR 72[rsp]
sub rdx, QWORD PTR fs:40
jne .L5
add rsp, 88
ret
.L5:
call __stack_chk_fail@PLTGCC 14.2.0 (Debian 14.2.0-19) i386-linux-gnu
AT&T syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -fstack-protector-strong -S tls-guard.c
first:
pushl %ebx
call __x86.get_pc_thunk.bx
addl $_GLOBAL_OFFSET_TABLE_, %ebx
subl $100, %esp
movl %gs:20, %eax
movl %eax, 88(%esp)
leal 24(%esp), %eax
pushl %eax
call fill@PLT
movsbl 28(%esp), %eax
addl $16, %esp
movl 76(%esp), %edx
subl %gs:20, %edx
jne .L5
addl $88, %esp
popl %ebx
ret
.L5:
call __stack_chk_fail_local
__x86.get_pc_thunk.bx:
movl (%esp), %ebx
retIntel syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -fstack-protector-strong -masm=intel -S tls-guard.c
first:
push ebx
call __x86.get_pc_thunk.bx
add ebx, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
sub esp, 100
mov eax, DWORD PTR gs:20
mov DWORD PTR 88[esp], eax
lea eax, 24[esp]
push eax
call fill@PLT
movsx eax, BYTE PTR 28[esp]
add esp, 16
mov edx, DWORD PTR 76[esp]
sub edx, DWORD PTR gs:20
jne .L5
add esp, 88
pop ebx
ret
.L5:
call __stack_chk_fail_local
__x86.get_pc_thunk.bx:
mov ebx, DWORD PTR [esp]
retClang 23.1.2 x86_64-pc-windows-msvc
AT&T syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -fstack-protector-strong -S tls-guard.c
first:
subq $104, %rsp
movq __security_cookie(%rip), %rax
xorq %rsp, %rax
movq %rax, 96(%rsp)
leaq 32(%rsp), %rcx
callq fill
movsbl 32(%rsp), %eax
movq 96(%rsp), %rcx
xorq %rsp, %rcx
movq __security_cookie(%rip), %rdx
cmpq %rcx, %rdx
jne .LBB0_2
addq $104, %rsp
retq
.LBB0_2:
movq 96(%rsp), %rcx
xorq %rsp, %rcx
callq __security_check_cookieIntel syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -fstack-protector-strong -masm=intel -S tls-guard.c
first:
sub rsp, 104
mov rax, qword ptr [rip + __security_cookie]
xor rax, rsp
mov qword ptr [rsp + 96], rax
lea rcx, [rsp + 32]
call fill
movsx eax, byte ptr [rsp + 32]
mov rcx, qword ptr [rsp + 96]
xor rcx, rsp
mov rdx, qword ptr [rip + __security_cookie]
cmp rdx, rcx
jne .LBB0_2
add rsp, 104
ret
.LBB0_2:
mov rcx, qword ptr [rsp + 96]
xor rcx, rsp
call __security_check_cookieMinGW-w64 GCC 14.2.0 (Debian 14.2.0-19+27+b1) x86_64-w64-mingw32
AT&T syntax x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -fstack-protector-strong -S tls-guard.c
first:
pushq %rbp
movq %rsp, %rbp
pushq %rbx
andq $-16, %rsp
subq $112, %rsp
movq .refptr.__stack_chk_guard(%rip), %rbx
movq (%rbx), %rcx
movq %rcx, 104(%rsp)
leaq 32(%rsp), %rcx
call fill
movsbl 32(%rsp), %eax
movq 104(%rsp), %rdx
subq (%rbx), %rdx
jne .L5
movq -8(%rbp), %rbx
leave
ret
.L5:
call __stack_chk_fail
.refptr.__stack_chk_guard:
.quad __stack_chk_guardIntel syntax x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -fstack-protector-strong -masm=intel -S tls-guard.c
first:
push rbp
mov rbp, rsp
push rbx
and rsp, -16
sub rsp, 112
mov rbx, QWORD PTR .refptr.__stack_chk_guard[rip]
mov rcx, QWORD PTR [rbx]
mov QWORD PTR 104[rsp], rcx
lea rcx, 32[rsp]
call fill
movsx eax, BYTE PTR 32[rsp]
mov rdx, QWORD PTR 104[rsp]
sub rdx, QWORD PTR [rbx]
jne .L5
mov rbx, QWORD PTR -8[rbp]
leave
ret
.L5:
call __stack_chk_fail
.refptr.__stack_chk_guard:
.quad __stack_chk_guardClang 23.1.2 x86_64-apple-macos11
AT&T syntax clang --target=x86_64-apple-macos11 -O2 -fno-asynchronous-unwind-tables -fstack-protector-strong -S tls-guard.c
_first:
pushq %rbp
movq %rsp, %rbp
subq $80, %rsp
movq ___stack_chk_guard@GOTPCREL(%rip), %rax
movq (%rax), %rax
movq %rax, -8(%rbp)
leaq -80(%rbp), %rdi
callq _fill
movsbl -80(%rbp), %eax
movq ___stack_chk_guard@GOTPCREL(%rip), %rcx
movq (%rcx), %rcx
cmpq -8(%rbp), %rcx
jne LBB0_2
addq $80, %rsp
popq %rbp
retq
LBB0_2:
callq ___stack_chk_failIntel syntax clang --target=x86_64-apple-macos11 -O2 -fno-asynchronous-unwind-tables -fstack-protector-strong -masm=intel -S tls-guard.c
_first:
push rbp
mov rbp, rsp
sub rsp, 80
mov rax, qword ptr [rip + ___stack_chk_guard@GOTPCREL]
mov rax, qword ptr [rax]
mov qword ptr [rbp - 8], rax
lea rdi, [rbp - 80]
call _fill
movsx eax, byte ptr [rbp - 80]
mov rcx, qword ptr [rip + ___stack_chk_guard@GOTPCREL]
mov rcx, qword ptr [rcx]
cmp rcx, qword ptr [rbp - 8]
jne LBB0_2
add rsp, 80
pop rbp
ret
LBB0_2:
call ___stack_chk_failThe ELF access models
The ELF systems that use FS and GS lay thread-local data out the same
way on x86, variant II of Drepper’s document. Every module, the
program and each shared library, has a TLS block for each thread. The
blocks of the modules loaded at startup sit just below the TCB, at
offsets from the thread pointer that are fixed once the program runs;
the program’s own block is right under the TCB, so its offset is known
when the program is linked. A library loaded later with dlopen gets
its blocks elsewhere, and only the C library knows where, through the
dynamic thread vector of each thread.
How much the code can know in advance therefore depends on where the variable is defined and where the code runs, and there are four access models:
| Model | Code in | Variable in | The code finds the variable |
|---|---|---|---|
| Local exec | the program | the program | at an offset from the thread pointer fixed at link time |
| Initial exec | any module loaded at startup | any module loaded at startup | at an offset that the dynamic linker stores in the GOT |
| General dynamic | any module | any module | by calling __tls_get_addr with the module and the offset |
| Local dynamic | any module | the same module | by one call for the module’s block, then fixed offsets |
In a program: local exec and initial exec
Code of a program knows two things: its own variables are in its own block, and every library it links against is loaded at startup. GCC and Clang in the image build position-independent executables by default, which only changes how i386 code reaches the GOT.
counter is defined by the program: one instruction with a fixed offset (@tpoff, @ntpoff on i386), a negative number since the block lies below the TCB. errcode may be in a library, so its offset comes from the GOT (@gottpoff). For the address of counter, the code reads the thread pointer from offset 0 and adds the offset.tls-models.c
/* A thread-local variable of the program itself, and one that a
shared library may define. Each thread has its own copies. */
_Thread_local int counter;
extern _Thread_local int errcode;
int next(void)
{
return ++counter;
}
int last_error(void)
{
return errcode;
}
int *counter_addr(void)
{
return &counter;
}
GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu
AT&T syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -S tls-models.c
next:
movl %fs:counter@tpoff, %eax
addl $1, %eax
movl %eax, %fs:counter@tpoff
ret
last_error:
movq errcode@gottpoff(%rip), %rax
movl %fs:(%rax), %eax
ret
counter_addr:
movq %fs:0, %rax
addq $counter@tpoff, %rax
ret
counter:
.zero 4Intel syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S tls-models.c
next:
mov eax, DWORD PTR fs:counter@tpoff
add eax, 1
mov DWORD PTR fs:counter@tpoff, eax
ret
last_error:
mov rax, QWORD PTR errcode@gottpoff[rip]
mov eax, DWORD PTR fs:[rax]
ret
counter_addr:
mov rax, QWORD PTR fs:0
add rax, OFFSET FLAT:counter@tpoff
ret
counter:
.zero 4Clang 23.1.2 x86_64-linux-gnu
AT&T syntax clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -S tls-models.c
next:
movl %fs:counter@TPOFF, %eax
incl %eax
movl %eax, %fs:counter@TPOFF
retq
last_error:
movq errcode@GOTTPOFF(%rip), %rax
movl %fs:(%rax), %eax
retq
counter_addr:
movq %fs:0, %rax
leaq counter@TPOFF(%rax), %rax
retq
counter:
.long 0Intel syntax clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -masm=intel -S tls-models.c
next:
mov eax, dword ptr fs:[counter@TPOFF]
inc eax
mov dword ptr fs:[counter@TPOFF], eax
ret
last_error:
mov rax, qword ptr [rip + errcode@GOTTPOFF]
mov eax, dword ptr fs:[rax]
ret
counter_addr:
mov rax, qword ptr fs:[0]
lea rax, [rax + counter@TPOFF]
ret
counter:
.long 0GCC 14.2.0 (Debian 14.2.0-19) i386-linux-gnu
AT&T syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -S tls-models.c
next:
movl %gs:counter@ntpoff, %eax
addl $1, %eax
movl %eax, %gs:counter@ntpoff
ret
last_error:
call __x86.get_pc_thunk.ax
addl $_GLOBAL_OFFSET_TABLE_, %eax
movl errcode@gotntpoff(%eax), %eax
movl %gs:(%eax), %eax
ret
counter_addr:
leal counter@ntpoff, %eax
addl %gs:0, %eax
ret
counter:
.zero 4
__x86.get_pc_thunk.ax:
movl (%esp), %eax
retIntel syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -masm=intel -S tls-models.c
next:
mov eax, DWORD PTR gs:counter@ntpoff
add eax, 1
mov DWORD PTR gs:counter@ntpoff, eax
ret
last_error:
call __x86.get_pc_thunk.ax
add eax, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
mov eax, DWORD PTR errcode@gotntpoff[eax]
mov eax, DWORD PTR gs:[eax]
ret
counter_addr:
lea eax, counter@ntpoff
add eax, DWORD PTR gs:0
ret
counter:
.zero 4
__x86.get_pc_thunk.ax:
mov eax, DWORD PTR [esp]
retIn a shared library: general and local dynamic
Code of a shared library, compiled with -fPIC, cannot assume that
it was loaded at startup. For a variable that may be defined anywhere
it calls __tls_get_addr with a pointer to two GOT entries, the
module’s number and the variable’s offset in its block, which the
dynamic linker fills in. The function returns the variable’s address,
and allocates the block of a module loaded with dlopen the first time
a thread uses it. For the library’s own variables one call finds the
block, and each variable is at a fixed offset in it (@dtpoff).
next uses the general dynamic model and total the local dynamic one. On x86-64 the data16 and rex64 prefixes, which GCC partly writes as .value 0x6666, make the sequence 16 bytes long. i386 passes the argument in EAX to ___tls_get_addr, with three underscores; for the one variable that record needs, GCC for i386 makes a general dynamic call.tls-dynamic.c
/* Code of a shared library, compiled with -fPIC: counter may be
defined by another module, while hits and misses are this
library's own. */
_Thread_local int counter;
static _Thread_local int hits, misses;
int next(void)
{
return ++counter;
}
int total(void)
{
return hits + misses;
}
void record(int hit)
{
if (hit)
hits++;
else
misses++;
}
GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu
AT&T syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -fPIC -S tls-dynamic.c
next:
subq $8, %rsp
data16 leaq counter@tlsgd(%rip), %rdi
.value 0x6666
rex64
call __tls_get_addr@PLT
movq %rax, %rdx
movl (%rax), %eax
addl $1, %eax
movl %eax, (%rdx)
addq $8, %rsp
ret
total:
subq $8, %rsp
leaq hits@tlsld(%rip), %rdi
call __tls_get_addr@PLT
movl hits@dtpoff(%rax), %edx
addl misses@dtpoff(%rax), %edx
addq $8, %rsp
movl %edx, %eax
ret
record:
subq $8, %rsp
testl %edi, %edi
je .L7
leaq hits@tlsld(%rip), %rdi
call __tls_get_addr@PLT
addl $1, hits@dtpoff(%rax)
addq $8, %rsp
ret
.L7:
leaq hits@tlsld(%rip), %rdi
call __tls_get_addr@PLT
addl $1, misses@dtpoff(%rax)
addq $8, %rsp
ret
misses:
.zero 4
hits:
.zero 4
counter:
.zero 4Intel syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -fPIC -masm=intel -S tls-dynamic.c
next:
sub rsp, 8
data16 lea rdi, counter@tlsgd[rip]
.value 0x6666
rex64
call __tls_get_addr@PLT
mov rdx, rax
mov eax, DWORD PTR [rax]
add eax, 1
mov DWORD PTR [rdx], eax
add rsp, 8
ret
total:
sub rsp, 8
lea rdi, hits@tlsld[rip]
call __tls_get_addr@PLT
mov edx, DWORD PTR hits@dtpoff[rax]
add edx, DWORD PTR misses@dtpoff[rax]
add rsp, 8
mov eax, edx
ret
record:
sub rsp, 8
test edi, edi
je .L7
lea rdi, hits@tlsld[rip]
call __tls_get_addr@PLT
add DWORD PTR hits@dtpoff[rax], 1
add rsp, 8
ret
.L7:
lea rdi, hits@tlsld[rip]
call __tls_get_addr@PLT
add DWORD PTR misses@dtpoff[rax], 1
add rsp, 8
ret
misses:
.zero 4
hits:
.zero 4
counter:
.zero 4Clang 23.1.2 x86_64-linux-gnu
AT&T syntax clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -fPIC -S tls-dynamic.c
next:
pushq %rax
data16
leaq counter@TLSGD(%rip), %rdi
data16
data16
rex64
callq __tls_get_addr@PLT
movl (%rax), %ecx
incl %ecx
movl %ecx, (%rax)
movl %ecx, %eax
popq %rcx
retq
total:
pushq %rax
leaq hits@TLSLD(%rip), %rdi
callq __tls_get_addr@PLT
movq %rax, %rcx
movl misses@DTPOFF(%rax), %eax
addl hits@DTPOFF(%rcx), %eax
popq %rcx
retq
record:
pushq %rbx
movl %edi, %ebx
leaq misses@TLSLD(%rip), %rdi
callq __tls_get_addr@PLT
testl %ebx, %ebx
leaq misses@DTPOFF(%rax), %rcx
leaq hits@DTPOFF(%rax), %rax
cmoveq %rcx, %rax
incl (%rax)
popq %rbx
retq
counter:
.long 0
hits:
.long 0
misses:
.long 0Intel syntax clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -fPIC -masm=intel -S tls-dynamic.c
next:
push rax
data16
lea rdi, [rip + counter@TLSGD]
data16
data16
rex64
call __tls_get_addr@PLT
mov ecx, dword ptr [rax]
inc ecx
mov dword ptr [rax], ecx
mov eax, ecx
pop rcx
ret
total:
push rax
lea rdi, [rip + hits@TLSLD]
call __tls_get_addr@PLT
mov rcx, rax
mov eax, dword ptr [rax + misses@DTPOFF]
add eax, dword ptr [rcx + hits@DTPOFF]
pop rcx
ret
record:
push rbx
mov ebx, edi
lea rdi, [rip + misses@TLSLD]
call __tls_get_addr@PLT
test ebx, ebx
lea rcx, [rax + misses@DTPOFF]
lea rax, [rax + hits@DTPOFF]
cmove rax, rcx
inc dword ptr [rax]
pop rbx
ret
counter:
.long 0
hits:
.long 0
misses:
.long 0GCC 14.2.0 (Debian 14.2.0-19) i386-linux-gnu
AT&T syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -fPIC -S tls-dynamic.c
next:
pushl %ebx
call __x86.get_pc_thunk.bx
addl $_GLOBAL_OFFSET_TABLE_, %ebx
subl $8, %esp
leal counter@tlsgd(,%ebx,1), %eax
call ___tls_get_addr@PLT
movl (%eax), %ecx
leal 1(%ecx), %edx
movl %edx, (%eax)
addl $8, %esp
movl %edx, %eax
popl %ebx
ret
total:
pushl %ebx
call __x86.get_pc_thunk.bx
addl $_GLOBAL_OFFSET_TABLE_, %ebx
subl $8, %esp
leal misses@tlsldm(%ebx), %eax
call ___tls_get_addr@PLT
movl misses@dtpoff(%eax), %edx
addl hits@dtpoff(%eax), %edx
addl $8, %esp
movl %edx, %eax
popl %ebx
ret
record:
pushl %ebx
call __x86.get_pc_thunk.bx
addl $_GLOBAL_OFFSET_TABLE_, %ebx
subl $8, %esp
movl 16(%esp), %eax
testl %eax, %eax
je .L7
leal hits@tlsgd(,%ebx,1), %eax
call ___tls_get_addr@PLT
addl $1, (%eax)
addl $8, %esp
popl %ebx
ret
.L7:
leal misses@tlsgd(,%ebx,1), %eax
call ___tls_get_addr@PLT
addl $1, (%eax)
addl $8, %esp
popl %ebx
ret
misses:
.zero 4
hits:
.zero 4
counter:
.zero 4
__x86.get_pc_thunk.bx:
movl (%esp), %ebx
retIntel syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -fPIC -masm=intel -S tls-dynamic.c
next:
push ebx
call __x86.get_pc_thunk.bx
add ebx, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
sub esp, 8
lea eax, counter@tlsgd[ebx*1]
call ___tls_get_addr@PLT
mov ecx, DWORD PTR [eax]
lea edx, 1[ecx]
mov DWORD PTR [eax], edx
add esp, 8
mov eax, edx
pop ebx
ret
total:
push ebx
call __x86.get_pc_thunk.bx
add ebx, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
sub esp, 8
lea eax, misses@tlsldm[ebx]
call ___tls_get_addr@PLT
mov edx, DWORD PTR misses@dtpoff[eax]
add edx, DWORD PTR hits@dtpoff[eax]
add esp, 8
mov eax, edx
pop ebx
ret
record:
push ebx
call __x86.get_pc_thunk.bx
add ebx, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
sub esp, 8
mov eax, DWORD PTR 16[esp]
test eax, eax
je .L7
lea eax, hits@tlsgd[ebx*1]
call ___tls_get_addr@PLT
add DWORD PTR [eax], 1
add esp, 8
pop ebx
ret
.L7:
lea eax, misses@tlsgd[ebx*1]
call ___tls_get_addr@PLT
add DWORD PTR [eax], 1
add esp, 8
pop ebx
ret
misses:
.zero 4
hits:
.zero 4
counter:
.zero 4
__x86.get_pc_thunk.bx:
mov ebx, DWORD PTR [esp]
retThe padding is for the linker. When it links code into an executable,
it knows more than the compiler did and rewrites a general dynamic
sequence into an initial exec or local exec one of the same length,
and an initial exec one into local exec. The compiler picks a cheaper
model than the default when it can, and -ftls-model for a whole file
or the tls_model attribute for one variable choose a model
explicitly: a library that is never loaded with dlopen may use
initial exec.
TLS descriptors
TLS descriptors replace the call of __tls_get_addr with a cheaper
one. The GOT holds a descriptor for each variable, two words: a
function and its argument, both set by the dynamic linker. The code
passes the descriptor’s address in RAX and calls the function, which
returns the variable’s offset from the thread pointer in RAX and
changes no other register except the flags. For a variable in the
static blocks the function just returns a constant offset, and for one
of a library loaded with dlopen it does the work of
__tls_get_addr. Since the call preserves every register, the caller
saves nothing around it.
GCC and Clang emit descriptors with -mtls-dialect=gnu2; on x86-64
both default to the traditional calls, as the listings above show.
@TLSDESC names the descriptor in the GOT, @TLSCALL marks the call for the linker. For two variables of its own, total asks once for the offset of the module’s block, _TLS_MODULE_BASE_, and adds each variable’s @dtpoff.tls-dynamic.c
/* Code of a shared library, compiled with -fPIC: counter may be
defined by another module, while hits and misses are this
library's own. */
_Thread_local int counter;
static _Thread_local int hits, misses;
int next(void)
{
return ++counter;
}
int total(void)
{
return hits + misses;
}
void record(int hit)
{
if (hit)
hits++;
else
misses++;
}
GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu
AT&T syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -fPIC -mtls-dialect=gnu2 -S tls-dynamic.c
next:
subq $8, %rsp
leaq counter@TLSDESC(%rip), %rax
call *counter@TLSCALL(%rax)
addq %fs:0, %rax
movl (%rax), %ecx
leal 1(%rcx), %edx
movl %edx, (%rax)
movl %edx, %eax
addq $8, %rsp
ret
total:
subq $8, %rsp
leaq _TLS_MODULE_BASE_@TLSDESC(%rip), %rax
call *_TLS_MODULE_BASE_@TLSCALL(%rax)
movl %fs:misses@dtpoff(%rax), %edx
addl %fs:hits@dtpoff(%rax), %edx
addq $8, %rsp
movl %edx, %eax
ret
record:
subq $8, %rsp
testl %edi, %edi
je .L7
leaq hits@TLSDESC(%rip), %rax
call *hits@TLSCALL(%rax)
addl $1, %fs:(%rax)
addq $8, %rsp
ret
.L7:
leaq misses@TLSDESC(%rip), %rax
call *misses@TLSCALL(%rax)
addl $1, %fs:(%rax)
addq $8, %rsp
ret
misses:
.zero 4
hits:
.zero 4
counter:
.zero 4Intel syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -fPIC -mtls-dialect=gnu2 -masm=intel -S tls-dynamic.c
next:
sub rsp, 8
lea rax, counter@TLSDESC[rip]
call [QWORD PTR [rax+counter@TLSCALL]]
add rax, QWORD PTR fs:0
mov ecx, DWORD PTR [rax]
lea edx, 1[rcx]
mov DWORD PTR [rax], edx
mov eax, edx
add rsp, 8
ret
total:
sub rsp, 8
lea rax, _TLS_MODULE_BASE_@TLSDESC[rip]
call [QWORD PTR [rax+_TLS_MODULE_BASE_@TLSCALL]]
mov edx, DWORD PTR fs:misses@dtpoff[rax]
add edx, DWORD PTR fs:hits@dtpoff[rax]
add rsp, 8
mov eax, edx
ret
record:
sub rsp, 8
test edi, edi
je .L7
lea rax, hits@TLSDESC[rip]
call [QWORD PTR [rax+hits@TLSCALL]]
add DWORD PTR fs:[rax], 1
add rsp, 8
ret
.L7:
lea rax, misses@TLSDESC[rip]
call [QWORD PTR [rax+misses@TLSCALL]]
add DWORD PTR fs:[rax], 1
add rsp, 8
ret
misses:
.zero 4
hits:
.zero 4
counter:
.zero 4Clang 23.1.2 x86_64-linux-gnu
AT&T syntax clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -fPIC -mtls-dialect=gnu2 -S tls-dynamic.c
next:
pushq %rax
leaq counter@tlsdesc(%rip), %rax
callq *counter@tlscall(%rax)
movl %fs:(%rax), %ecx
incl %ecx
movl %ecx, %fs:(%rax)
movl %ecx, %eax
popq %rcx
retq
total:
pushq %rax
leaq _TLS_MODULE_BASE_@tlsdesc(%rip), %rax
callq *_TLS_MODULE_BASE_@tlscall(%rax)
movl %fs:misses@DTPOFF(%rax), %ecx
addl %fs:hits@DTPOFF(%rax), %ecx
movl %ecx, %eax
popq %rcx
retq
record:
pushq %rax
leaq _TLS_MODULE_BASE_@tlsdesc(%rip), %rax
callq *_TLS_MODULE_BASE_@tlscall(%rax)
testl %edi, %edi
movq %fs:0, %rcx
leaq misses@DTPOFF(%rax,%rcx), %rdx
leaq hits@DTPOFF(%rax,%rcx), %rax
cmoveq %rdx, %rax
incl (%rax)
popq %rax
retq
counter:
.long 0
hits:
.long 0
misses:
.long 0Intel syntax clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -fPIC -mtls-dialect=gnu2 -masm=intel -S tls-dynamic.c
next:
push rax
lea rax, [rip + counter@tlsdesc]
call qword ptr [rax + counter@tlscall]
mov ecx, dword ptr fs:[rax]
inc ecx
mov dword ptr fs:[rax], ecx
mov eax, ecx
pop rcx
ret
total:
push rax
lea rax, [rip + _TLS_MODULE_BASE_@tlsdesc]
call qword ptr [rax + _TLS_MODULE_BASE_@tlscall]
mov ecx, dword ptr fs:[rax + misses@DTPOFF]
add ecx, dword ptr fs:[rax + hits@DTPOFF]
mov eax, ecx
pop rcx
ret
record:
push rax
lea rax, [rip + _TLS_MODULE_BASE_@tlsdesc]
call qword ptr [rax + _TLS_MODULE_BASE_@tlscall]
test edi, edi
mov rcx, qword ptr fs:[0]
lea rdx, [rax + rcx + misses@DTPOFF]
lea rax, [rax + rcx + hits@DTPOFF]
cmove rax, rdx
inc dword ptr [rax]
pop rax
ret
counter:
.long 0
hits:
.long 0
misses:
.long 0Windows: the TEB and the TLS array
Windows has one mechanism instead of four models. GS on x64 and FS on
x86 point to the thread environment block, the TEB, which holds a
pointer to the thread’s TLS array. Each module with thread-local data,
the program or a DLL, has a .tls section and an index that the
loader assigns and stores in the module’s _tls_index; the array entry
at that index points to the module’s block for this thread, which the
loader fills from the .tls section when the thread starts. Microsoft’s
PE documentation gives the offset of the array pointer in the 32-bit
TEB, 0x2C; LLVM’s code generator names 0x58 for the 64-bit one.
%gs:88, 0x58, on x64; %fs:__tls_array, a symbol for 0x2C, on x86), the module’s entry at _tls_index, and the variable at its offset in the .tls section (@SECREL32). Clang for MinGW-w64 emits the same code as for MSVC.tls-models.c
/* A thread-local variable of the program itself, and one that a
shared library may define. Each thread has its own copies. */
_Thread_local int counter;
extern _Thread_local int errcode;
int next(void)
{
return ++counter;
}
int last_error(void)
{
return errcode;
}
int *counter_addr(void)
{
return &counter;
}
Clang 23.1.2 x86_64-pc-windows-msvc
AT&T syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -S tls-models.c
next:
movl _tls_index(%rip), %eax
movq %gs:88, %rcx
movq (%rcx,%rax,8), %rcx
movl counter@SECREL32(%rcx), %eax
incl %eax
movl %eax, counter@SECREL32(%rcx)
retq
last_error:
movl _tls_index(%rip), %eax
movq %gs:88, %rcx
movq (%rcx,%rax,8), %rax
movl errcode@SECREL32(%rax), %eax
retq
counter_addr:
movl _tls_index(%rip), %eax
movq %gs:88, %rcx
movq (%rcx,%rax,8), %rax
leaq counter@SECREL32(%rax), %rax
retq
counter:
.long 0Intel syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -masm=intel -S tls-models.c
next:
mov eax, dword ptr [rip + _tls_index]
mov rcx, qword ptr gs:[88]
mov rcx, qword ptr [rcx + 8*rax]
mov eax, dword ptr [rcx + counter@SECREL32]
inc eax
mov dword ptr [rcx + counter@SECREL32], eax
ret
last_error:
mov eax, dword ptr [rip + _tls_index]
mov rcx, qword ptr gs:[88]
mov rax, qword ptr [rcx + 8*rax]
mov eax, dword ptr [rax + errcode@SECREL32]
ret
counter_addr:
mov eax, dword ptr [rip + _tls_index]
mov rcx, qword ptr gs:[88]
mov rax, qword ptr [rcx + 8*rax]
lea rax, [rax + counter@SECREL32]
ret
counter:
.long 0Clang 23.1.2 i686-pc-windows-msvc
AT&T syntax clang --target=i686-pc-windows-msvc -march=pentium4 -O2 -fno-asynchronous-unwind-tables -S tls-models.c
_next:
movl __tls_index, %eax
movl %fs:__tls_array, %ecx
movl (%ecx,%eax,4), %ecx
movl _counter@SECREL32(%ecx), %eax
incl %eax
movl %eax, _counter@SECREL32(%ecx)
retl
_last_error:
movl __tls_index, %eax
movl %fs:__tls_array, %ecx
movl (%ecx,%eax,4), %eax
movl _errcode@SECREL32(%eax), %eax
retl
_counter_addr:
movl __tls_index, %eax
movl %fs:__tls_array, %ecx
movl (%ecx,%eax,4), %eax
leal _counter@SECREL32(%eax), %eax
retl
_counter:
.long 0Intel syntax clang --target=i686-pc-windows-msvc -march=pentium4 -O2 -fno-asynchronous-unwind-tables -masm=intel -S tls-models.c
_next:
mov eax, dword ptr [__tls_index]
mov ecx, dword ptr fs:[__tls_array]
mov ecx, dword ptr [ecx + 4*eax]
mov eax, dword ptr [ecx + _counter@SECREL32]
inc eax
mov dword ptr [ecx + _counter@SECREL32], eax
ret
_last_error:
mov eax, dword ptr [__tls_index]
mov ecx, dword ptr fs:[__tls_array]
mov eax, dword ptr [ecx + 4*eax]
mov eax, dword ptr [eax + _errcode@SECREL32]
ret
_counter_addr:
mov eax, dword ptr [__tls_index]
mov ecx, dword ptr fs:[__tls_array]
mov eax, dword ptr [ecx + 4*eax]
lea eax, [eax + _counter@SECREL32]
ret
_counter:
.long 0Clang 23.1.2 x86_64-w64-windows-gnu
AT&T syntax clang --target=x86_64-w64-windows-gnu -O2 -fno-asynchronous-unwind-tables -S tls-models.c
next:
movl _tls_index(%rip), %eax
movq %gs:88, %rcx
movq (%rcx,%rax,8), %rcx
movl counter@SECREL32(%rcx), %eax
incl %eax
movl %eax, counter@SECREL32(%rcx)
retq
last_error:
movl _tls_index(%rip), %eax
movq %gs:88, %rcx
movq (%rcx,%rax,8), %rax
movl errcode@SECREL32(%rax), %eax
retq
counter_addr:
movl _tls_index(%rip), %eax
movq %gs:88, %rcx
movq (%rcx,%rax,8), %rax
leaq counter@SECREL32(%rax), %rax
retq
counter:
.long 0Intel syntax clang --target=x86_64-w64-windows-gnu -O2 -fno-asynchronous-unwind-tables -masm=intel -S tls-models.c
next:
mov eax, dword ptr [rip + _tls_index]
mov rcx, qword ptr gs:[88]
mov rcx, qword ptr [rcx + 8*rax]
mov eax, dword ptr [rcx + counter@SECREL32]
inc eax
mov dword ptr [rcx + counter@SECREL32], eax
ret
last_error:
mov eax, dword ptr [rip + _tls_index]
mov rcx, qword ptr gs:[88]
mov rax, qword ptr [rcx + 8*rax]
mov eax, dword ptr [rax + errcode@SECREL32]
ret
counter_addr:
mov eax, dword ptr [rip + _tls_index]
mov rcx, qword ptr gs:[88]
mov rax, qword ptr [rcx + 8*rax]
lea rax, [rax + counter@SECREL32]
ret
counter:
.long 0Before Windows Vista, a DLL loaded with LoadLibrary could not rely
on this mechanism; since then the loader supports it there too. The
Windows API offers the same at run time, for any amount of data:
TlsAlloc, TlsGetValue, TlsSetValue and TlsFree.
Emulated TLS
The MinGW-w64 GCC of the image, 14.2, does not use the TEB at all, and
Clang for OpenBSD does not use FS. Both emulate thread-local storage:
each variable gets a control object, here __emutls_v.counter, and
every access calls __emutls_get_address from the compiler’s runtime
library with the control object’s address, which returns the address
of the running thread’s copy.
counter itself, so objects of the two compilers cannot share a thread-local variable there.tls-models.c
/* A thread-local variable of the program itself, and one that a
shared library may define. Each thread has its own copies. */
_Thread_local int counter;
extern _Thread_local int errcode;
int next(void)
{
return ++counter;
}
int last_error(void)
{
return errcode;
}
int *counter_addr(void)
{
return &counter;
}
MinGW-w64 GCC 14.2.0 (Debian 14.2.0-19+27+b1) x86_64-w64-mingw32
AT&T syntax x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -S tls-models.c
next:
subq $40, %rsp
leaq __emutls_v.counter(%rip), %rcx
call __emutls_get_address
movq %rax, %rdx
movl (%rax), %eax
addl $1, %eax
movl %eax, (%rdx)
addq $40, %rsp
ret
last_error:
subq $40, %rsp
movq .refptr.__emutls_v.errcode(%rip), %rcx
call __emutls_get_address
movl (%rax), %eax
addq $40, %rsp
ret
counter_addr:
leaq __emutls_v.counter(%rip), %rcx
jmp __emutls_get_address
__emutls_v.counter:
.quad 4
.quad 4
.quad 0
.quad 0
.refptr.__emutls_v.errcode:
.quad __emutls_v.errcodeIntel syntax x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -masm=intel -S tls-models.c
next:
sub rsp, 40
lea rcx, __emutls_v.counter[rip]
call __emutls_get_address
mov rdx, rax
mov eax, DWORD PTR [rax]
add eax, 1
mov DWORD PTR [rdx], eax
add rsp, 40
ret
last_error:
sub rsp, 40
mov rcx, QWORD PTR .refptr.__emutls_v.errcode[rip]
call __emutls_get_address
mov eax, DWORD PTR [rax]
add rsp, 40
ret
counter_addr:
lea rcx, __emutls_v.counter[rip]
jmp __emutls_get_address
__emutls_v.counter:
.quad 4
.quad 4
.quad 0
.quad 0
.refptr.__emutls_v.errcode:
.quad __emutls_v.errcodeClang 23.1.2 x86_64-unknown-openbsd7
AT&T syntax clang --target=x86_64-unknown-openbsd7 -O2 -fno-asynchronous-unwind-tables -S tls-models.c
next:
endbr64
pushq %rbp
movq %rsp, %rbp
leaq __emutls_v.counter(%rip), %rdi
callq __emutls_get_address@PLT
movl (%rax), %ecx
incl %ecx
movl %ecx, (%rax)
movl %ecx, %eax
popq %rbp
retq
last_error:
endbr64
pushq %rbp
movq %rsp, %rbp
movq __emutls_v.errcode@GOTPCREL(%rip), %rdi
callq __emutls_get_address@PLT
movl (%rax), %eax
popq %rbp
retq
counter_addr:
endbr64
pushq %rbp
movq %rsp, %rbp
leaq __emutls_v.counter(%rip), %rdi
callq __emutls_get_address@PLT
popq %rbp
retq
__emutls_v.counter:
.quad 4
.quad 4
.quad 0
.quad 0Intel syntax clang --target=x86_64-unknown-openbsd7 -O2 -fno-asynchronous-unwind-tables -masm=intel -S tls-models.c
next:
endbr64
push rbp
mov rbp, rsp
lea rdi, [rip + __emutls_v.counter]
call __emutls_get_address@PLT
mov ecx, dword ptr [rax]
inc ecx
mov dword ptr [rax], ecx
mov eax, ecx
pop rbp
ret
last_error:
endbr64
push rbp
mov rbp, rsp
mov rdi, qword ptr [rip + __emutls_v.errcode@GOTPCREL]
call __emutls_get_address@PLT
mov eax, dword ptr [rax]
pop rbp
ret
counter_addr:
endbr64
push rbp
mov rbp, rsp
lea rdi, [rip + __emutls_v.counter]
call __emutls_get_address@PLT
pop rbp
ret
__emutls_v.counter:
.quad 4
.quad 4
.quad 0
.quad 0macOS: thread-local variable descriptors
macOS gives every thread-local variable a descriptor of three words in
the section __DATA,__thread_vars: a function, a key and the
variable’s offset. The code loads the descriptor’s address into RDI
(@TLVP) and calls the function in its first word, which returns the
variable’s address in RAX and, like a TLS descriptor of ELF, preserves
every other register.
counter as the compiler writes it: __tlv_bootstrap in the first word, which dyld replaces when it loads the program, and a pointer to the initial value in the third, which the linker turns into an offset.tls-models.c
/* A thread-local variable of the program itself, and one that a
shared library may define. Each thread has its own copies. */
_Thread_local int counter;
extern _Thread_local int errcode;
int next(void)
{
return ++counter;
}
int last_error(void)
{
return errcode;
}
int *counter_addr(void)
{
return &counter;
}
Clang 23.1.2 x86_64-apple-macos11
AT&T syntax clang --target=x86_64-apple-macos11 -O2 -fno-asynchronous-unwind-tables -S tls-models.c
_next:
pushq %rbp
movq %rsp, %rbp
movq _counter@TLVP(%rip), %rdi
callq *(%rdi)
movl (%rax), %ecx
incl %ecx
movl %ecx, (%rax)
movl %ecx, %eax
popq %rbp
retq
_last_error:
pushq %rbp
movq %rsp, %rbp
movq _errcode@TLVP(%rip), %rdi
callq *(%rdi)
movl (%rax), %eax
popq %rbp
retq
_counter_addr:
pushq %rbp
movq %rsp, %rbp
movq _counter@TLVP(%rip), %rdi
callq *(%rdi)
popq %rbp
retq
_counter:
.quad __tlv_bootstrap
.quad 0
.quad _counter$tlv$initIntel syntax clang --target=x86_64-apple-macos11 -O2 -fno-asynchronous-unwind-tables -masm=intel -S tls-models.c
_next:
push rbp
mov rbp, rsp
mov rdi, qword ptr [rip + _counter@TLVP]
call qword ptr [rdi]
mov ecx, dword ptr [rax]
inc ecx
mov dword ptr [rax], ecx
mov eax, ecx
pop rbp
ret
_last_error:
push rbp
mov rbp, rsp
mov rdi, qword ptr [rip + _errcode@TLVP]
call qword ptr [rdi]
mov eax, dword ptr [rax]
pop rbp
ret
_counter_addr:
push rbp
mov rbp, rsp
mov rdi, qword ptr [rip + _counter@TLVP]
call qword ptr [rdi]
pop rbp
ret
_counter:
.quad __tlv_bootstrap
.quad 0
.quad _counter$tlv$initdyld allocates a module’s thread-local data for a thread only when the thread first uses it, and keeps its address in a pthread key. The kernel makes GS point to the thread’s pthread-specific data, so the fast path of the function reads the slot of that key through GS, adds the offset and returns.
Summary
| Linux x86-64 | Linux i386 | macOS x86-64 | Windows x64 | Windows x86 | |
|---|---|---|---|---|---|
| Register | FS | GS | GS | GS | FS |
| Points to | TCB | TCB | pthread’s thread-specific data | TEB | TEB |
| Variable of the program | %fs:x@tpoff | %gs:x@ntpoff | call through a descriptor | TLS array, _tls_index, offset | TLS array, _tls_index, offset |
| Variable of a library | __tls_get_addr or a TLS descriptor | ___tls_get_addr or a TLS descriptor | call through a descriptor | the same as in the program | the same as in the program |
Sources
- System V Application Binary Interface, AMD64 Architecture Processor Supplement, version 1.0: commit
e1ce098331da, 2025-03-12 - Ulrich Drepper: ELF Handling For Thread-Local Storage, version 0.21: as published on 2026-09-25
- Alexandre Oliva: Thread-Local Storage Descriptors for IA32 and AMD64/EM64T, version 0.9.6: as published on 2026-09-25
- glibc 2.44: sysdeps/x86_64/nptl/tls.h: commit
c3a3a9808ad3, 2026-07-24 - glibc 2.44: sysdeps/i386/nptl/tls.h: commit
c3a3a9808ad3, 2026-07-24 - Linux 7.2: Documentation/arch/x86/x86_64/fsgs.rst: commit
8d3ae59288f1, 2026-08-16 - Linux 7.2: arch/x86/include/asm/percpu.h: commit
8d3ae59288f1, 2026-08-16 - Linux 7.2: arch/x86/kernel/process.c: commit
8d3ae59288f1, 2026-08-16 - GCC 14.2 manual: Options for Code Generation Conventions: as published on 2026-09-25
- GCC 14.2 manual: Common Variable Attributes: as published on 2026-09-25
- Microsoft Learn: PE Format: commit
e103fa4e8810, 2026-09-15 - Microsoft Learn: Thread Local Storage (TLS): commit
f70d88cd5da7, 2026-09-24 - LLVM 23.1.2: llvm/lib/Target/X86/X86ISelLowering.cpp: commit
85ac56026243, 2026-09-20 - GCC 14.2 internals manual: Emulating TLS: as published on 2026-09-25
- XNU xnu-12377.121.6: osfmk/i386/pcb_native.c: commit
ac9718fb1af6, 2026-06-17 - dyld-1378: libdyld/ThreadLocalVariables.h: commit
fd8d0c4d5232, 2026-06-15