x86-64 と x86 のスレッドローカルストレージ
スレッドローカル変数はスレッドごとに独自のコピーを持ち、コードはそれを実行しているスレッドのコピーを見つけなければなりません。x86 ではセグメントレジスターがその仕事の大部分を担いますが、どのレジスターを使い、それが何を指すかはシステムによって異なります。
C11 の _Thread_local、C++ の thread_local、GCC の __thread、MSVC の __declspec(thread) は、いずれもスレッドごとに 1 つのコピーを持つ変数を宣言します。通常のグローバル変数にはリンカーがコードに埋め込める 1 つのアドレスがありますが、スレッドローカル変数にはスレッドの数だけアドレスがあるため、コードは実行中のスレッドのアドレスを見つけなければなりません。
x86 には、そのためのレジスターがあります。64 ビットモードではセグメントはフラットですが、FS と GS だけは例外で、オペレーティングシステムがスレッドごとにそのベースアドレスを設定します。FS または GS プレフィックスを持つ命令はそのベースをアドレスに加えるため、同じ命令がスレッドごとに異なるコピーにアクセスします。
| システム | レジスター | ベースが指すもの |
|---|---|---|
| Linux、FreeBSD、その他の psABI システム (x86-64 と x32) | FS | スレッド制御ブロック (TCB) |
| 同じシステムの i386 | GS | TCB |
| macOS x86-64 | GS | スレッドの pthread のスレッド固有データ |
| Windows x64 | GS | スレッド環境ブロック (TEB) |
| Windows x86 | FS | TEB |
一部のコンパイラーは、スレッドローカル変数にこのレジスターを使わず、代わりにエミュレートします。Windows の MinGW-w64 GCC と OpenBSD 向けの Clang です。
各カーネルは、もう一方のレジスターを自分のために使います。64 ビット Linux は GS を通じて CPU ごとのデータにアクセスし、カーネルへの入口でユーザーの値と入れ替えます (SWAPGS)。32 ビット Linux は FS を使います。
スレッドポインター
x86-64 psABI は FS をスレッドポインターと定め、i386 では Drepper の ELF TLS に関する文書が GS について同じことを定めています。TCB は自分自身のアドレスで始まるため、レジスターを通じてオフセット 0 を読むと、スレッドポインターが通常のアドレスとして得られます。コードがスレッドローカル変数のアドレスを取るときには、すぐにこれが必要になります。
__builtin_thread_pointer() は TCB の最初のワードを読みます。Linux でも FreeBSD でも、x86-64 と x32 では FS を、i386 では GS を通じて読みます。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 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S tls-pointer.c
thread_pointer:
movq %fs:0, %rax
retIntel 構文 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 構文 gcc-14 -mx32 -O2 -fno-asynchronous-unwind-tables -S tls-pointer.c
thread_pointer:
movl %fs:0, %eax
retIntel 構文 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 構文 gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -S tls-pointer.c
thread_pointer:
movl %gs:0, %eax
retIntel 構文 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 構文 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 構文 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 構文 clang --target=i686-unknown-freebsd14 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -S tls-pointer.c
thread_pointer:
pushl %ebp
movl %esp, %ebp
movl %gs:0, %eax
popl %ebp
retlIntel 構文 clang --target=i686-unknown-freebsd14 -march=i686 -mtune=generic -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
retC ライブラリは、作成する各スレッドのベースを設定します。64 ビット Linux では、プログラムは arch_prctl() (ARCH_GET_FS、ARCH_SET_FS) で、またはカーネルが FSGSBASE 命令を有効にし、補助ベクトル (auxiliary vector) の HWCAP2_FSGSBASE でそれを示している場合は、RDFSBASE や WRFSBASE などの FSGSBASE 命令で、ベースを読み書きできます。新しいスレッドは、フラグ CLONE_SETTLS を付けた clone の tls 引数からベースを受け取ります。64 ビットのプログラムでは FS のベースそのもの、32 ビットのプログラムでは set_thread_area も受け取るセグメントディスクリプターである struct user_desc です。
glibc が TCB に保持するもの
TCB は C ライブラリのものです。glibc は TCB をヘッダーで始め、そこにはオフセット 0 の TCB 自身のアドレス、動的スレッドベクトル (dynamic thread vector) へのポインター、スタックプロテクターのカナリア値などが入っています。カナリア値は x86-64 ではオフセット 0x28、i386 では 0x14 にあります。新しいスレッドは、それを作成したスレッドからカナリア値をコピーします。
%fs:40 または %gs:20 からカナリア値を読みます。Windows 向けの Clang はグローバル変数 __security_cookie と RSP の XOR を取り、MinGW-w64 GCC と macOS 向けの Clang はグローバル変数 __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 構文 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 構文 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 構文 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 構文 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 構文 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 構文 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 構文 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 構文 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 構文 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 構文 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_failELF のアクセスモデル
FS と GS を使う ELF システムは、x86 ではスレッドローカルデータを同じ方法で配置します。Drepper の文書のバリアント II です。各モジュール、つまりプログラムと各共有ライブラリは、スレッドごとに TLS ブロックを持ちます。起動時に読み込まれたモジュールのブロックは TCB のすぐ下にあり、スレッドポインターからのオフセットはプログラムの実行開始後は固定です。プログラム自身のブロックは TCB の直下にあるため、そのオフセットはプログラムのリンク時にわかります。後から dlopen で読み込まれたライブラリのブロックは別の場所に置かれ、その場所は各スレッドの動的スレッドベクトルを通じて C ライブラリだけが知っています。
そのため、コードが事前にどれだけ知ることができるかは、変数が定義されている場所とコードが実行される場所によって決まり、アクセスモデルは 4 つあります。
| モデル | コードの場所 | 変数の場所 | コードが変数を見つける方法 |
|---|---|---|---|
| Local exec | プログラム | プログラム | リンク時に固定されたスレッドポインターからのオフセット |
| Initial exec | 起動時に読み込まれる任意のモジュール | 起動時に読み込まれる任意のモジュール | 動的リンカーが GOT に格納するオフセット |
| General dynamic | 任意のモジュール | 任意のモジュール | モジュールとオフセットを渡して __tls_get_addr を呼び出す |
| Local dynamic | 任意のモジュール | 同じモジュール | モジュールのブロックを 1 回の呼び出しで求め、その後は固定オフセット |
プログラム内: local exec と initial exec
プログラムのコードは 2 つのことを知っています。自分の変数は自分のブロックにあること、そしてリンクするすべてのライブラリが起動時に読み込まれることです。サイトのビルドの GCC と Clang はデフォルトで位置独立実行ファイルを生成しますが、それによって変わるのは i386 のコードが GOT にアクセスする方法だけです。
counter はプログラムが定義しているため、アクセスは固定オフセット (@tpoff、i386 では @ntpoff) を使う 1 つの命令で済み、ブロックが TCB の下にあるためオフセットは負の数です。errcode はライブラリにあるかもしれないため、そのオフセットは GOT から取ります (@gottpoff)。counter のアドレスについては、コードはオフセット 0 からスレッドポインターを読み、オフセットを加えます。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 構文 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 構文 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 構文 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 構文 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 構文 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 構文 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]
ret共有ライブラリ内: general dynamic と local dynamic
-fPIC でコンパイルされた共有ライブラリのコードは、自分が起動時に読み込まれたと仮定できません。どこで定義されているかわからない変数については、2 つの GOT エントリ (モジュールの番号と、ブロック内での変数のオフセット) へのポインターを渡して __tls_get_addr を呼び出します。これらのエントリは動的リンカーが埋めます。この関数は変数のアドレスを返し、dlopen で読み込まれたモジュールのブロックは、スレッドが初めて使うときに確保します。ライブラリ自身の変数については、1 回の呼び出しでブロックを求め、各変数はその中の固定オフセット (@dtpoff) にあります。
next は general dynamic モデルを、total は local dynamic モデルを使います。x86-64 では data16 と rex64 プレフィックス (GCC は一部を .value 0x6666 と書きます) によって、この命令列は 16 バイトになります。i386 は EAX で引数をアンダースコア 3 つの ___tls_get_addr に渡します。record が必要とする 1 つの変数について、i386 向けの GCC は general dynamic の呼び出しを生成します。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 構文 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 構文 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 構文 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 構文 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 構文 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 構文 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]
retこのパディングはリンカーのためのものです。リンカーは、コードを実行ファイルにリンクするとき、コンパイラーよりも多くのことを知っているため、general dynamic の命令列を同じ長さの initial exec または local exec の命令列に、initial exec の命令列を local exec に書き換えます。コンパイラーは可能な場合はデフォルトより安価なモデルを選びます。ファイル全体には -ftls-model、1 つの変数には tls_model 属性でモデルを明示的に選べます。dlopen で読み込まれることのないライブラリは initial exec を使えます。
TLS ディスクリプター
TLS ディスクリプターは、__tls_get_addr の呼び出しをより安価な呼び出しに置き換えます。GOT は各変数のディスクリプターを保持し、それは 2 ワード、つまり関数とその引数で、どちらも動的リンカーが設定します。コードはディスクリプターのアドレスを RAX で渡してその関数を呼び出し、関数はスレッドポインターからの変数のオフセットを RAX で返し、フラグ以外のレジスターは変更しません。静的ブロックにある変数については、関数は定数のオフセットを返すだけで、dlopen で読み込まれたライブラリの変数については __tls_get_addr の処理を行います。この呼び出しはすべてのレジスターを保存するため、呼び出し元はその前後で何も保存しません。
GCC と Clang は -mtls-dialect=gnu2 でディスクリプターを生成します。上のリストが示すとおり、x86-64 ではどちらもデフォルトでは従来の呼び出しを使います。
@TLSDESC は GOT 内のディスクリプターを指し、@TLSCALL はリンカーのために呼び出しに印を付けます。自分の 2 つの変数について、total はモジュールのブロックのオフセット _TLS_MODULE_BASE_ を 1 回だけ求め、各変数の @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 構文 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 構文 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 構文 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 構文 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: TEB と TLS 配列
Windows には 4 つのモデルの代わりに 1 つの仕組みがあります。x64 では GS、x86 では FS がスレッド環境ブロック (TEB) を指し、TEB はスレッドの TLS 配列へのポインターを保持します。スレッドローカルデータを持つ各モジュール (プログラムまたは DLL) には .tls セクションと、ローダーが割り当ててモジュールの _tls_index に格納するインデックスがあります。そのインデックスの位置にある配列のエントリは、このスレッド用のモジュールのブロックを指し、ローダーはスレッドの開始時に .tls セクションからそのブロックを埋めます。Microsoft の PE のドキュメントは 32 ビットの TEB での配列ポインターのオフセットを 0x2C としており、LLVM のコードジェネレーターは 64 ビットの TEB について 0x58 を挙げています。
%gs:88、つまり 0x58、x86 では 0x2C を表すシンボル %fs:__tls_array)、_tls_index の位置にあるモジュールのエントリを取り、.tls セクション内のオフセット (@SECREL32) にある変数にアクセスします。MinGW-w64 向けの Clang は 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 構文 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 構文 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 構文 clang --target=i686-pc-windows-msvc -march=pentium4 -mtune=generic -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 構文 clang --target=i686-pc-windows-msvc -march=pentium4 -mtune=generic -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 構文 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 構文 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 0Windows Vista より前は、LoadLibrary で読み込まれた DLL はこの仕組みに頼れませんでしたが、それ以降はローダーがそこでもサポートしています。Windows API は、任意の量のデータについて同じことを実行時に行う TlsAlloc、TlsGetValue、TlsSetValue、TlsFree を提供しています。
エミュレートされた TLS
サイトのビルドの MinGW-w64 GCC 14.2 は TEB をまったく使わず、OpenBSD 向けの Clang は FS を使いません。どちらもスレッドローカルストレージをエミュレートします。各変数は制御オブジェクト (ここでは __emutls_v.counter) を持ち、すべてのアクセスは制御オブジェクトのアドレスを渡してコンパイラーのランタイムライブラリの __emutls_get_address を呼び出し、この関数が実行中のスレッドのコピーのアドレスを返します。
counter そのものを参照するため、そこでは 2 つのコンパイラーのオブジェクトがスレッドローカル変数を共有できません。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 構文 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 構文 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 構文 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 構文 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: スレッドローカル変数ディスクリプター
macOS は、すべてのスレッドローカル変数に、セクション __DATA,__thread_vars 内の 3 ワードのディスクリプターを与えます。関数、キー、変数のオフセットです。コードはディスクリプターのアドレスを RDI に読み込み (@TLVP)、その最初のワードにある関数を呼び出します。この関数は変数のアドレスを RAX で返し、ELF の TLS ディスクリプターと同じく、ほかのすべてのレジスターを保存します。
counter のディスクリプター。最初のワードには __tlv_bootstrap があり、dyld がプログラムを読み込むときにこれを置き換えます。3 番目のワードには初期値へのポインターがあり、リンカーがこれをオフセットに変えます。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 構文 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 構文 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 は、モジュールのスレッドローカルデータを、スレッドが初めて使うときにだけそのスレッドのために確保し、そのアドレスを pthread のキーに保持します。カーネルは GS がスレッドの pthread 固有データを指すようにしているため、関数の高速な経路は GS を通じてそのキーのスロットを読み、オフセットを加えて戻ります。
まとめ
| Linux x86-64 | Linux i386 | macOS x86-64 | Windows x64 | Windows x86 | |
|---|---|---|---|---|---|
| レジスター | FS | GS | GS | GS | FS |
| 指す先 | TCB | TCB | pthread のスレッド固有データ | TEB | TEB |
| プログラムの変数 | %fs:x@tpoff | %gs:x@ntpoff | ディスクリプターを通じた呼び出し | TLS 配列、_tls_index、オフセット | TLS 配列、_tls_index、オフセット |
| ライブラリの変数 | __tls_get_addr または TLS ディスクリプター | ___tls_get_addr または TLS ディスクリプター | ディスクリプターを通じた呼び出し | プログラムと同じ | プログラムと同じ |
出典
- System V Application Binary Interface, AMD64 Architecture Processor Supplement, version 1.0: コミット
e1ce098331da、2025-03-12 - Ulrich Drepper: ELF Handling For Thread-Local Storage, version 0.21: 2026-09-25 の公開版
- Alexandre Oliva: Thread-Local Storage Descriptors for IA32 and AMD64/EM64T, version 0.9.6: 2026-09-25 の公開版
- glibc 2.44: sysdeps/x86_64/nptl/tls.h: コミット
c3a3a9808ad3、2026-07-24 - glibc 2.44: sysdeps/i386/nptl/tls.h: コミット
c3a3a9808ad3、2026-07-24 - Linux 7.3: Documentation/arch/x86/x86_64/fsgs.rst: コミット
c03c1a14507d、2026-10-18 - Linux 7.3: arch/x86/include/asm/percpu.h: コミット
c03c1a14507d、2026-10-18 - Linux 7.3: arch/x86/kernel/process.c: コミット
c03c1a14507d、2026-10-18 - GCC 14.2 manual: Options for Code Generation Conventions: 2026-09-25 の公開版
- GCC 14.2 manual: Common Variable Attributes: 2026-09-25 の公開版
- Microsoft Learn: PE Format: コミット
e103fa4e8810、2026-09-15 - Microsoft Learn: Thread Local Storage (TLS): コミット
f70d88cd5da7、2026-09-24 - LLVM 23.1.2: llvm/lib/Target/X86/X86ISelLowering.cpp: コミット
85ac56026243、2026-09-20 - GCC 14.2 internals manual: Emulating TLS: 2026-09-25 の公開版
- XNU xnu-12377.121.6: osfmk/i386/pcb_native.c: コミット
ac9718fb1af6、2026-06-17 - dyld-1378: libdyld/ThreadLocalVariables.h: コミット
fd8d0c4d5232、2026-06-15