线程本地存储(Thread Local Storage,TLS)是一种在线程内部共享数据的数据交换区域。每个线程都可以在这个区域保存它们要在内部共享的数据。一个比较知名的例子是C语言的全局变量errno。对于errno这类的全局变量来说,如果多线程进程的某一个线程对其进行了修改,那么这个变量就会影响到其他所有的线程。这显然和实际需求相悖,因此全局变量errno必须保存在TLS中。
为解决这个矛盾,C++ 11标准新增了一个限定符thread_local。它能将指定变量和特定的线程联系起来。由它限定的变量能被初始化,并且会被保存在TLS中。
指令清单65.1 C++11
#include <iostream>
#include <thread>
thread_local int tmp=3;
int main()
{
std::cout << tmp << std::endl;
};
接下来,我们使用MinGW GCC 4.8.1编译它,不要用MSVC 2012进行编译。
在分析可执行文件的PE头之后就会发现,变量tmp被分配到TLS专用的数据保存区域了。
本书第20章展示的随机数生成函数其实有一个瑕疵:在多线程并发运行时,它是不安全的。原因在于:它有一个内部的变量,它可能会同时被不同的线程读取或者修改。
未初始化的TLS数据
我们可以给这种全局变量增加限定符__declspec(thread),这样它就能被分配到TLS中。请注意下述代码的第9行。
1 #include <stdint.h>
2 #include <windows.h>
3 #include <winnt.h>
4
5 // from the Numerical Recipes book:
6 #define RNG_a 1664525
7 #define RNG_c 1013904223
8
9 __declspec( thread ) uint32_t rand_state;
10
11 void my_srand (uint32_t init)
12 {
13 rand_state=init;
14 }
15
16 int my_rand ()
17 {
18 rand_state=rand_state*RNG_a;
19 rand_state=rand_state+RNG_c;
20 return rand_state & 0x7fff;
21 }
22
23 int main()
24 {
25 my_srand(0x12345678);
26 printf ("%d\n", my_rand());
27 };
用MSVC 2013编译上述程序,再用Hiew打开最后生成的可执行文件。我们可以看到这种文件的PE部分出现了全新的TLS段:
指令清单65.2 优化的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段中。此后每个线程都会拥有各自的rand_state 。这里表示的是如何寻址:从FS:2Ch调用线程信息块(Thread Information Block,TIB)的地址。如果需要,再增加一个额外的索引,最后再计算TLS段的地址。
这种程序的线程可以通过ECX寄存器访问各自的rand_state变量,因为该变量在各个线程的地址不再相同。
FS段选择器并不陌生。它其实就是TIB的专用指针,用于提高线程数据的加载速度。
GS段选择器是Win64程序使用的额外的索引寄存器。
在下面这个程序里,TLS的地址是0x58。
指令清单65.3 优化的MSVC 2013(64位)
_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
初始化的TLS数据
编程人员通常会想给变量rand_state设置一个固定的初始值,以防后期忘记对它进行初始化。如下述代码第9行所示,我们对它进行初始化赋值。
1 #include <stdint.h>
2 #include <windows.h>
3 #include <winnt.h>
4
5 // from the Numerical Recipes book:
6 #define RNG_a 1664525
7 #define RNG_c 1013904223
8
9 __declspec( thread ) uint32_t rand_state=1234;
10
11 void my_srand (uint32_t init)
12 {
13 rand_state=init;
14 }
15
16 int my_rand ()
17 {
18 rand_state=rand_state*RNG_a;
19 rand_state=rand_state+RNG_c;
20 return rand_state & 0x7fff;
21 }
22
23 int main()
24 {
25 printf ("%d\n", my_rand());
26 };
以上的代码看起来并没有什么不同,但是在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_ptr
...
我们要关注的是这里显示的数1234。每当启动新的线程时,它都会分配一个新的TLS段。此后,包括1234在内的所有数据都会被复制到新建都TLS段。
一个典型的应用场景是:
启动线程A,系统同期创建该线程专用的TLS,并将变量rand_state赋值为1234。
此后,线程A多次调用my_rand()函数,rand_state变量的值不再会是初始值1234。
另行启动线程B,系统同期创建该线程专用的TLS,变量rand_state也会被赋值为1234。也就是说,在线程A和线程B里,同名变量会有不同的值。
TLS回调
如果TLS中的变量必须填充为某些数据,并且是以某些不寻常的方式进行的话,该怎么办呢?比如说,我们有以下的这个任务:编程人员忘记调用my_srand()函数来初始化随机数发生器(PRNG),而随机数发生器只有在正确初始化之后才会生成真实意义上的随机数,而不是1234这样的固定值。在这种情况下,可以采用TLS回调。
在采用这种hack之后,本例代码的可移植性(通用性)就变差了。毕竟,本例只是一个演示性质的敲门砖而已。它只是构造一个在进程/线程启动前就被系统调用的回调函数(tls_callback())。这个回调函数用GetTickCount()函数的返回值来初始化随机数发生器(PRNG)。
#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中查看一下,代码如下所示。
指令清单65.4 优化的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回调函数,可起到混淆视听的作用。经验不足的分析人员通常会感到晕头转向,无法在分析原始入口/OEP之前就已经运行的回调函数。
我们来看看在GCC下是如何定义线程本地的全局变量的:
__thread uint32_t rand_state=1234;
当然,这不是标准的C/C++修饰符,而是GCC的专用修饰符。
GS段选择器也常用于TLS寻址,但是Linux的实现方法和Windows略有不同。
指令清单65.5 x86下的优化GCC 4.8.1
.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
更多的信息可以查看参考书目Dre13。