static thread_local 和 thread_local 在块范围内是等价的; thread_local 有一个线程存储时长,不是静态的,也不是自动的;因此,静态和自动说明符,即thread_local,即auto thread_local,和static thread_local 对存储持续时间没有影响;从语义上讲,使用它们是无意义的,由于存在thread_local,它们只是隐含地表示线程存储持续时间; static 甚至也不修改块范围内的链接(因为它始终没有链接),因此除了修改存储持续时间之外没有其他定义。 extern thread_local 也可以在块范围内。文件范围内的static thread_local 提供了thread_local 变量内部链接,这意味着TLS 中的每个翻译单元将有一个副本(每个翻译单元将在.exe 的TLS 索引处解析为自己的变量,因为汇编器会将变量插入.o 文件的rdata$t 部分,并在符号表中将其标记为本地符号,因为符号上缺少.global 指令)。 extern thread_local 在文件范围内是合法的,就像它在块范围内一样,并使用在另一个翻译单元中定义的 thread_local 副本。文件范围内的thread_local 不是隐式静态的,因为它可以为另一个翻译单元提供全局符号定义,这是块范围变量无法完成的。
编译器会将所有已初始化的thread_local 变量存储在.tdata(包括块范围的变量)中以用于ELF,将未初始化的变量存储在.tbss 用于ELF,或者全部存储在.tls 用于PE 格式。我假设线程库在创建线程时将访问.tls 段并执行Windows API 调用(TlsAlloc 和TlsSetValue),它们为堆上的每个.exe 和.dll 分配变量和在 GS 段中线程的 TEB 的 TLS 数组中放置一个指针,并返回分配的索引,以及为动态库调用 DLL_THREAD_ATTACH 例程。据推测,指向_tls_start 和_tls_end 定义的空间中的值的指针是作为值指针传递给TlsSetValue 的。
文件范围static/extern thread_local和块范围(extern) thread_local之间的区别与文件范围static/extern和块范围static/extern之间的一般区别相同,因为块范围thread_local变量将超出范围定义它的函数的末尾,尽管由于线程存储持续时间,它仍然可以通过地址返回和访问。
编译器知道.tls段中数据的索引,所以它可以直接替代访问GS段,如godbolt所示。
MSVC
thread_local int a = 5;
int square(int num) {
thread_local int i = 5;
return a * i;
}
_TLS SEGMENT
int a DD 05H ; a
_TLS ENDS
_TLS SEGMENT
int `int square(int)'::`2'::i DD 05H ; `square'::`2'::i
_TLS ENDS
num$ = 8
int square(int) PROC ; square
mov DWORD PTR [rsp+8], ecx
mov eax, OFFSET FLAT:int a ; a
mov eax, eax
mov ecx, DWORD PTR _tls_index
mov rdx, QWORD PTR gs:88
mov rcx, QWORD PTR [rdx+rcx*8]
mov edx, OFFSET FLAT:int `int square(int)'::`2'::i
mov edx, edx
mov r8d, DWORD PTR _tls_index
mov r9, QWORD PTR gs:88
mov r8, QWORD PTR [r9+r8*8]
mov eax, DWORD PTR [rcx+rax]
imul eax, DWORD PTR [r8+rdx]
ret 0
int square(int) ENDP ; square
这会从gs:88(gs:[0x58],这是线程本地存储数组的线性地址)加载一个64位指针,然后使用TLS array pointer + _tls_index*8加载一个64位指针(这显然是定位索引在数组 * 指针大小)。然后,Int a; 从此指针 + 偏移量加载到 .tls 段中。看到两个变量都使用相同的_tls_index,这表明每个.exe 都有一个索引,即每个.tls 部分,实际上.rdata 中的每个TLS 目录都有一个_tls_index,并且变量被打包在一起TLS 数组指向的地址。 static thread_local 不同翻译单元中的变量将被合并到 .tls 中,并在同一个索引处打包在一起。
我相信mainCRTStartup,链接器总是包含在最终的可执行文件中,如果它作为控制台应用程序被链接,它会使其成为入口点,引用_tls_used变量(因为每个.exe都需要它自己的索引) 并且在libcmt.lib 中定义它的任何目标文件中的.rdata 的T 片段中进行编译(因为mainCRTStartup 引用它,链接器会将它包含在最终的可执行文件中)。如果链接器找到对_tls_used 变量的引用,它将确保包含它并确保PE 标头TLS 目录指向它。
#pragma section(".rdata$T", long, read) //creates a read only section called `.rdata` if not created and a fragment T in the section
#define _CRTALLOC(x) __declspec(allocate(x))
#pragma data_seg() //set the compilers current default data section to `.data`
_CRTALLOC(".rdata$T") //place in the section .rdata, fragment T
const IMAGE_TLS_DIRECTORY _tls_used =
{
(ULONG)(ULONG_PTR) &_tls_start, // start of tls data in the tls section
(ULONG)(ULONG_PTR) &_tls_end, // end of tls data
(ULONG)(ULONG_PTR) &_tls_index, // address of tls_index
(ULONG)(ULONG_PTR) (&__xl_a+1), // pointer to callbacks
(ULONG) 0, // size of tls zero fill
(ULONG) 0 // characteristics
};
http://www.nynaeve.net/?p=183
_tls_used是一个IMAGE_TLS_DIRECTORY结构类型的变量,上面初始化的内容,实际上是在tlssup.c中定义的。在此之前,它定义了_tls_index、_tls_start 和_tls_end,将_tls_start 放在.tls 部分的开头,将_tls_end 放在.tls 部分的末尾,方法是将其放在该部分中fragmentZZZ 使其按字母顺序出现在该部分的末尾:
#pragma data_seg(".tls") //set the compilers current default data section to `.tls`
#if defined (_M_IA64) || defined (_M_AMD64)
_CRTALLOC(".tls") //place the following in the section named `.tls`
#endif
char _tls_start = 0; //if not defined, place in the current default data section, which is also `.tls`
#pragma data_seg(".tls$ZZZ")
#if defined (_M_IA64) || defined (_M_AMD64)
_CRTALLOC(".tls$ZZZ")
#endif
char _tls_end = 0;
这些地址随后被用作_tls_used TLS 目录中的标记。只有当.tls 部分完成并且它具有固定的相对lea 位置时,链接器才会解析该地址。
GCC(TLS 直接在 FS 基础之前;原始数据而不是指针)
mov edx,DWORD PTR fs:0xfffffffffffffff8 //access thread_local int1 inside function
mov eax,DWORD PTR fs:0xfffffffffffffffc //access thread_local int2 inside function
将一个、两个或一个变量都设为本地会产生相同的代码。
当线程执行终止时,windows 上的线程库将使用TlsFree() 调用释放存储空间(它还必须释放指向TlsGetValue() 返回的指针的堆上的内存)。