TLS是每個線程特有的數據區域,每個線程可以把自己需要的數據存儲在這里。一個著名的例子是C標準的全局變量errno。多個線程可以同時使用errno獲取返回的錯誤碼,如果是全局變量它是無法在多線程環境下正常工作的。因此errno必須保存在TLS。
C++11標準里面新添加了一個thread_local修飾符,標明每個線程都屬于自己版本的變量。它可以被初始化并位于TLS中。
Listing 65.1: C++11
#!c
#include <iostream>
#include <thread>
thread_local int tmp=3;
int main()
{
std::cout << tmp << std::endl;
};
使用MinGW GCC 4.8.1而不是MSVC2012編譯。
如果我們查看它的PE文件,可以看到tmp變量被放到TLS section。
前面第20章的純隨機數生成器有一個缺陷:它不是線程安全的,因為它的內部狀態變量可以被不同的線程同時讀取或修改。
一個全局變量如果添加了_declspec(thread)修飾符,那么它會被分配在TLS。
#!c
#include <stdint.h>
#include <windows.h>
#include <winnt.h>
// from the Numerical Recipes book
#define RNG_a 1664525
#define RNG_c 1013904223
__declspec( thread ) uint32_t rand_state;
void my_srand (uint32_t init)
{
rand_state=init;
}
int my_rand ()
{
rand_state=rand_state*RNG_a;
rand_state=rand_state+RNG_c;
return rand_state & 0x7fff;
}
int main()
{
my_srand(0x12345678);
printf ("%d\n", my_rand());
};
使用Hiew可以看到PE文件多了一個section:.tls。
Listing 65.2: Optimizing MSVC 2013 x86
_TLS SEGMENT
_rand_state DD 01H DUP (?)
_TLS ENDS
_DATA SEGMENT
$SG84851 DB '%d', 0aH, 00H
_DATA ENDS
_TEXT SEGMENT
_init$ = 8 ; size = 4
_my_srand PROC
; FS:0=address of TIB
mov eax, DWORD PTR fs:__tls_array ; displayed in IDA as FS:2Ch
; EAX=address of TLS of process
mov ecx, DWORD PTR __tls_index
mov ecx, DWORD PTR [eax+ecx*4]
; ECX=current TLS segment
mov eax, DWORD PTR _init$[esp-4]
mov DWORD PTR _rand_state[ecx], eax
ret 0
_my_srand ENDP
_my_rand PROC
; FS:0=address of TIB
mov eax, DWORD PTR fs:__tls_array ; displayed in IDA as FS:2Ch
; EAX=address of TLS of process
mov ecx, DWORD PTR __tls_index
mov ecx, DWORD PTR [eax+ecx*4]
; ECX=current TLS segment
imul eax, DWORD PTR _rand_state[ecx], 1664525
add eax, 1013904223 ; 3c6ef35fH
mov DWORD PTR _rand_state[ecx], eax
and eax, 32767 ; 00007fffH
ret 0
_my_rand ENDP
_TEXT ENDS
rand_state現在處于TLS段,而且這個變量每個線程都擁有屬于自己版本。它是這么訪問的:從FS:2Ch加載TIB(Thread Information Block)的地址,然后添加一個額外的索引(如果需要的話),接著計算出在TLS段的地址。
最后可以通過ECX寄存器來訪問rand_state變量,它指向每個線程特定的數據區域。
FS:這是每個逆向工程師都很熟悉的選擇子了。它專門用于指向TIB,因此訪問線程特定數據可以很快完成。
GS: 該選擇子用于Win64,0x58的地址是TLS。
Listing 65.3: Optimizing MSVC 2013 x64
_TLS SEGMENT
rand_state DD 01H DUP (?)
_TLS ENDS
_DATA SEGMENT
$SG85451 DB '%d', 0aH, 00H
_DATA ENDS
_TEXT SEGMENT
init$ = 8
my_srand PROC
mov edx, DWORD PTR _tls_index
mov rax, QWORD PTR gs:88 ; 58h
mov r8d, OFFSET FLAT:rand_state
mov rax, QWORD PTR [rax+rdx*8]
mov DWORD PTR [r8+rax], ecx
ret 0
my_srand ENDP
my_rand PROC
mov rax, QWORD PTR gs:88 ; 58h
mov ecx, DWORD PTR _tls_index
mov edx, OFFSET FLAT:rand_state
mov rcx, QWORD PTR [rax+rcx*8]
imul eax, DWORD PTR [rcx+rdx], 1664525 ;0019660dH
add eax, 1013904223 ; 3c6ef35fH
mov DWORD PTR [rcx+rdx], eax
and eax, 32767 ; 00007fffH
ret 0
my_rand ENDP
_TEXT ENDS
比方說,我們想為rand_state設置一些固定的值以避免程序員忘記初始化。
#!c
#include <stdint.h>
#include <windows.h>
#include <winnt.h>
// from the Numerical Recipes book
#define RNG_a 1664525
#define RNG_c 1013904223
__declspec( thread ) uint32_t rand_state=1234;
void my_srand (uint32_t init)
{
rand_state=init;
}
int my_rand ()
{
rand_state=rand_state*RNG_a;
rand_state=rand_state+RNG_c;
return rand_state & 0x7fff;
}
int main()
{
printf ("%d\n", my_rand());
};
代碼除了給rand_state設定初始值外與之前的并沒有什么不同,但在IDA我們看到:
.tls:00404000 ; Segment type: Pure data
.tls:00404000 ; Segment permissions: Read/Write
.tls:00404000 _tls segment para public 'DATA' use32
.tls:00404000 assume cs:_tls
.tls:00404000 ;org 404000h
.tls:00404000 TlsStart db 0 ; DATA XREF: .rdata:TlsDirectory
.tls:00404001 db 0
.tls:00404002 db 0
.tls:00404003 db 0
.tls:00404004 dd 1234
.tls:00404008 TlsEnd db 0 ; DATA XREF: .rdata:TlsEnd_pt
...
每次一個新的線程運行的時候,會分配新的TLS給它,然后包括1234所有數據將被拷貝過去。
這是一個典型的場景:
線程A開始運行,然后分配給它一個TLS,并把1234拷貝到rand_state。
線程A里面多次調用my_rand()函數,rand_state已經不是1234。
線程B開始運行,然后分配給它一個TLS,并把1234拷貝到rand_state,這時候可以觀察到兩個線程使用同一個變量,但它們的值是不一樣的。
如果我們想給TLS賦一個變量值呢?比方說:程序員忘記調用my_srand()函數來初始化PRNG,但是隨機數生成器在開始的時候必須使用一個真正的隨機數值而不是1234。這種情況下則可以使用TLS callbaks。
下面的代碼的可移植性很差,原因你應該明白。我們定義了一個函數(tls_callback()),它在進程/線程開始執行前調用,該函數使用GetTickCount()函數的返回值來初始化PRNG。
#!c
#include <stdint.h>
#include <windows.h>
#include <winnt.h>
// from the Numerical Recipes book
#define RNG_a 1664525
#define RNG_c 1013904223
__declspec( thread ) uint32_t rand_state;
void my_srand (uint32_t init)
{
rand_state=init;
}
void NTAPI tls_callback(PVOID a, DWORD dwReason, PVOID b)
{
my_srand (GetTickCount());
}
#pragma data_seg(".CRT$XLB")
PIMAGE_TLS_CALLBACK p_thread_callback = tls_callback;
#pragma data_seg()
int my_rand ()
{
rand_state=rand_state*RNG_a;
rand_state=rand_state+RNG_c;
return rand_state & 0x7fff;
}
int main()
{
// rand_state is already initialized at the moment (using GetTickCount())
printf ("%d\n", my_rand());
};
用IDA看一下:
Listing 65.4: Optimizing MSVC 2013
.text:00401020 TlsCallback_0 proc near ; DATA XREF: .rdata:TlsCallbacks
.text:00401020 call ds:GetTickCount
.text:00401026 push eax
.text:00401027 call my_srand
.text:0040102C pop ecx
.text:0040102D retn 0Ch
.text:0040102D TlsCallback_0 endp
...
.rdata:004020C0 TlsCallbacks dd offset TlsCallback_0 ; DATA XREF: .rdata:TlsCallbacks_ptr
...
.rdata:00402118 TlsDirectory dd offset TlsStart
.rdata:0040211C TlsEnd_ptr dd offset TlsEnd
.rdata:00402120 TlsIndex_ptr dd offset TlsIndex
.rdata:00402124 TlsCallbacks_ptr dd offset TlsCallbacks
.rdata:00402128 TlsSizeOfZeroFill dd 0
.rdata:0040212C TlsCharacteristics dd 300000h
TLS callbacks函數時常用于隱藏解包處理過程。為此有些人可能會困惑,為什么一些代碼可以偷偷地在OEP(Original Entry Point)之前執行。
下面是GCC聲明線程局部存儲的方式:
#!c
__thread uint32_t rand_state=1234;
這不是標準C/C++的修飾符,但是是GCC的一個擴展特性。
GS:該選擇子同樣用于訪問TLS,但稍微有點區別:
Listing 65.5: Optimizing GCC 4.8.1 x86
.text:08048460 my_srand proc near
.text:08048460
.text:08048460 arg_0 = dword ptr 4
.text:08048460
.text:08048460 mov eax, [esp+arg_0]
.text:08048464 mov gs:0FFFFFFFCh, eax
.text:0804846A retn
.text:0804846A my_srand endp
.text:08048470 my_rand proc near
.text:08048470 imul eax, gs:0FFFFFFFCh, 19660Dh
.text:0804847B add eax, 3C6EF35Fh
.text:08048480 mov gs:0FFFFFFFCh, eax
.text:08048486 and eax, 7FFFh
.text:0804848B retn
.text:0804848B my_rand endp
更多例子:ELF Handling For Thread-Local Storage