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 (каждая единица перевода будет разрешаться в свою собственную переменную в индексе TLS для .exe
, потому что ассемблер вставит переменную в раздел rdata$t
файла .o
и пометить его в таблице символов как локальный символ из-за отсутствия директивы .global
для символа). extern thread_local
в области файла допустимо, как и в области блока, и использует копию thread_local
, определенную в другой единице перевода. thread_local
в области файла не является неявно статическим, поскольку может предоставить определение глобального символа для другой единицы перевода, чего нельзя сделать с помощью переменной области блока.
Компилятор сохранит все инициализированные переменные thread_local
в .tdata
(включая переменные блочной области) для формата ELF и неинициализированные в .tbss
для формата ELF, или все в .tls
для формата PE. Я предполагаю, что библиотека потоков при создании потока будет обращаться к сегменту .tls
и выполнять вызовы Windows API (TlsAlloc
и TlsSetValue
), которые выделяют переменные для каждого .exe
и .dll
в куче и помещают указатели в массив TLS потока. TEB в сегменте GS и возвращает выделенный индекс, а также вызывает подпрограммы 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
Это загружает 64-битный указатель из gs:88
(gs:[0x58]
, который является линейным адресом локального массива хранения потока), затем загружает 64-битный указатель, используя TLS array pointer + _tls_index*8
(очевидно, это поиск индекса в массиве * размер указателя). Затем из этого указателя + смещения загружается Int a;
в сегмент .tls. Поскольку обе переменные используют один и тот же _tls_index
, предполагается, что существует индекс для каждого файла .exe, т. е. для каждого раздела .tls, действительно существует один индекс _tls_index
для каждого каталога TLS в .rdata
, и переменные упакованы вместе по адресу, на который указывает TLS-массив. static thread_local
переменные в разных единицах перевода будут объединены в .tls, и все они будут упакованы вместе по одному индексу.
Я считаю, что mainCRTStartup
, который компоновщик всегда включает в окончательный исполняемый файл и делает его точкой входа, если он компонуется как консольное приложение, ссылается на переменную _tls_used
(поскольку каждому .exe нужен свой собственный индекс), и это было прагматично чтобы перейти во фрагмент T .rdata
в любом объектном файле в libcmt.lib
, который определяет его (и поскольку mainCRTStartup
ссылается на него, компоновщик включит его в окончательный исполняемый файл). Если компоновщик найдет ссылку на переменную _tls_used
, он обязательно включит ее и убедится, что каталог TLS заголовка PE указывает на нее.
#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
, помещая его в раздел фрагментZZZ
таким образом, чтобы он в алфавитном порядке заканчивался в конце раздела:
#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()
).
person
Lewis Kelsey
schedule
26.03.2020