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)
同じシステムの i386GSTCB
macOS x86-64GSスレッドの pthread のスレッド固有データ
Windows x64GSスレッド環境ブロック (TEB)
Windows x86FSTEB

一部のコンパイラーは、スレッドローカル変数にこのレジスターを使わず、代わりにエミュレートします。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
        ret

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S tls-pointer.c

thread_pointer:
        mov     rax, QWORD PTR fs:0
        ret

GCC 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
        ret

Intel 構文 gcc-14 -mx32 -O2 -fno-asynchronous-unwind-tables -masm=intel -S tls-pointer.c

thread_pointer:
        mov     eax, DWORD PTR fs:0
        ret

GCC 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
        ret

Intel 構文 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
        ret

Clang 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
        retq

Intel 構文 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
        ret

Clang 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
        retl

Intel 構文 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
        ret

C ライブラリは、作成する各スレッドのベースを設定します。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 にあります。新しいスレッドは、それを作成したスレッドからカナリア値をコピーします。

各システムのスタックプロテクター。Linux のコードはスレッドレジスターを通じて %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@PLT

Intel 構文 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@PLT

GCC 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
        ret

Intel 構文 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]
        ret

Clang 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_cookie

Intel 構文 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_cookie

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 -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_guard

Intel 構文 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_guard

Clang 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_fail

Intel 構文 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_fail

ELF のアクセスモデル

FS と GS を使う ELF システムは、x86 ではスレッドローカルデータを同じ方法で配置します。Drepper の文書のバリアント II です。各モジュール、つまりプログラムと各共有ライブラリは、スレッドごとに TLS ブロックを持ちます。起動時に読み込まれたモジュールのブロックは TCB のすぐ下にあり、スレッドポインターからのオフセットはプログラムの実行開始後は固定です。プログラム自身のブロックは TCB の直下にあるため、そのオフセットはプログラムのリンク時にわかります。後から dlopen で読み込まれたライブラリのブロックは別の場所に置かれ、その場所は各スレッドの動的スレッドベクトルを通じて C ライブラリだけが知っています。

そのため、コードが事前にどれだけ知ることができるかは、変数が定義されている場所とコードが実行される場所によって決まり、アクセスモデルは 4 つあります。

ELF の 4 つの TLS アクセスモデル
モデルコードの場所変数の場所コードが変数を見つける方法
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   4

Intel 構文 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   4

Clang 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   0

Intel 構文 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   0

GCC 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
        ret

Intel 構文 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   4

Intel 構文 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   4

Clang 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   0

Intel 構文 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   0

GCC 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
        ret

Intel 構文 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 ではどちらもデフォルトでは従来の呼び出しを使います。

TLS ディスクリプターを使う同じライブラリ。@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   4

Intel 構文 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   4

Clang 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   0

Intel 構文 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   0

Windows: 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 を挙げています。

どのアクセスも 3 つの段階を踏みます。TEB から TLS 配列を取り (x64 では %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   0

Intel 構文 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   0

Clang 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   0

Intel 構文 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   0

Clang 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   0

Intel 構文 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   0

Windows 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 を呼び出し、この関数が実行中のスレッドのコピーのアドレスを返します。

どのアクセスも呼び出しであり、呼び出す名前もそれぞれ異なります。MinGW-w64 向けの Clang のコードは 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.errcode

Intel 構文 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.errcode

Clang 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   0

Intel 構文 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   0

macOS: スレッドローカル変数ディスクリプター

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$init

Intel 構文 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$init

dyld は、モジュールのスレッドローカルデータを、スレッドが初めて使うときにだけそのスレッドのために確保し、そのアドレスを pthread のキーに保持します。カーネルは GS がスレッドの pthread 固有データを指すようにしているため、関数の高速な経路は GS を通じてそのキーのスロットを読み、オフセットを加えて戻ります。

まとめ

各 ABI のスレッドローカル変数
Linux x86-64Linux i386macOS x86-64Windows x64Windows x86
レジスターFSGSGSGSFS
指す先TCBTCBpthread のスレッド固有データTEBTEB
プログラムの変数%fs:x@tpoff%gs:x@ntpoffディスクリプターを通じた呼び出しTLS 配列、_tls_index、オフセットTLS 配列、_tls_index、オフセット
ライブラリの変数__tls_get_addr または TLS ディスクリプター___tls_get_addr または TLS ディスクリプターディスクリプターを通じた呼び出しプログラムと同じプログラムと同じ

出典

  1. System V Application Binary Interface, AMD64 Architecture Processor Supplement, version 1.0: コミット e1ce098331da、2025-03-12
  2. Ulrich Drepper: ELF Handling For Thread-Local Storage, version 0.21: 2026-09-25 の公開版
  3. Alexandre Oliva: Thread-Local Storage Descriptors for IA32 and AMD64/EM64T, version 0.9.6: 2026-09-25 の公開版
  4. glibc 2.44: sysdeps/x86_64/nptl/tls.h: コミット c3a3a9808ad3、2026-07-24
  5. glibc 2.44: sysdeps/i386/nptl/tls.h: コミット c3a3a9808ad3、2026-07-24
  6. Linux 7.3: Documentation/arch/x86/x86_64/fsgs.rst: コミット c03c1a14507d、2026-10-18
  7. Linux 7.3: arch/x86/include/asm/percpu.h: コミット c03c1a14507d、2026-10-18
  8. Linux 7.3: arch/x86/kernel/process.c: コミット c03c1a14507d、2026-10-18
  9. GCC 14.2 manual: Options for Code Generation Conventions: 2026-09-25 の公開版
  10. GCC 14.2 manual: Common Variable Attributes: 2026-09-25 の公開版
  11. Microsoft Learn: PE Format: コミット e103fa4e8810、2026-09-15
  12. Microsoft Learn: Thread Local Storage (TLS): コミット f70d88cd5da7、2026-09-24
  13. LLVM 23.1.2: llvm/lib/Target/X86/X86ISelLowering.cpp: コミット 85ac56026243、2026-09-20
  14. GCC 14.2 internals manual: Emulating TLS: 2026-09-25 の公開版
  15. XNU xnu-12377.121.6: osfmk/i386/pcb_native.c: コミット ac9718fb1af6、2026-06-17
  16. dyld-1378: libdyld/ThreadLocalVariables.h: コミット fd8d0c4d5232、2026-06-15