MemoryModule阅读与PE文件解析(四)---深入理解TLS

来源:互联网 发布:mac奶瓶粉底液 编辑:程序博客网 时间:2024/06/09 23:58

下面的操作是关于TLS 和 PE 入口点函数的,首先,了解什么是TLS。

TLS 是线程局部存储的简称,这种机制的特殊之处就在于,线程相关。对于普通的变量来说,如果它是全局或静态的,那么如果多线程程序同时访问可能会造成逻辑问题,TLS提供了一种简便的方法来实现线程访问与该线程相关联的全局或静态变量的方法

TLS 分为静态和动态

动态TLS

如下图所示:


PEB 中有一个DWORD 类型的标志位,其某个标志位代表了该进程所有的线程的TEB 中对应的一个存储单元是否在使用,线程所访问的线程相关的变量就是存储在线程自己的存储单元中。详情请参考:http://m.2cto.com/kf/201604/503658.html

这个图向我们展示了线程局部存储的核心思想,即:根据PEB 中的标志位对应于TEB 中(也就是线程相关的由来)相关的内存位置,读写线程相关内存。

另外:当Tls变量的个数小于 64,使用TEB 中的成员TlsSlots存储TLS 变量:


否则,需要使用一个指针成员TlsExpansionSlots指向新申请的内存以存储TLS变量:


静态TLS:

TLS 变量

静态TLS 其实使用的依然是上述的TEB 的思想,只不过此时TLS 变量的定义和声明是静态的,不需要通过显式的函数调用即可实现TLS 的目的。下面我们将会看到,静态TLS 的实现依赖于操作系统加载器的支持;另一方面,静态TLS 必须要保证静态加载:如果DLL 中使用TLS,必须保证DLL 的静态加载,否则,程序逻辑错误,可能会出错。

 

首先我们来看对于声明为TLS的变量的操作在汇编级别是怎样的过程:

静态链接的DLL 代码如下:

// dllmain.cpp : 定义 DLL应用程序的入口点。

#include "stdafx.h"

__declspec (thread)unsignedint test =0x61626364;

BOOL APIENTRY DllMain( HMODULEhModule,

                      DWORD ul_reason_for_call,

                      LPVOIDlpReserved

                     )

{

    return TRUE;

}

 

EXTERN_C __declspec(dllexport)unsignedint Test()

{

    test++;

    //(*(DWORD*)(TEB->ThreadLocalStoragePointer[__tls_index(504C3348h)]+4))++;

    return test;

}

 

主程序代码如下:

#include <stdio.h>

#include <windows.h>

#include <WinNT.h>

#pragma comment(lib,"TLSDllTest.lib")

EXTERN_C __declspec(dllimport)unsignedint Test();

#define THREADCOUNT 5

 

//静态TLS数据

__declspec(thread)DWORDdwTlsData1=0x61626364;

__declspec(thread)DWORDdwTlsData2=0x65666768;

//为了查看内存数据时正常,采用临界区

CRITICAL_SECTION gDisplayTLS_CS;

 

DWORD WINAPI ThreadFunc(LPVOIDlpThreadParameter)

{

    EnterCriticalSection(&gDisplayTLS_CS);

    dwTlsData1 = (DWORD)lpThreadParameter;

    dwTlsData1 ++;

    dwTlsData2 = dwTlsData1;

    dwTlsData2 ++;

 

    printf("%d\t",Test());

    printf("%d\n\n",dwTlsData1);

    LeaveCriticalSection(&gDisplayTLS_CS);

    return 0;

}

 

DWORD main(VOID)

{

    DWORD IDThread;

    HANDLE hThread[THREADCOUNT];

    DWORD g_ThreadData[THREADCOUNT];

    for(inti = 0;i < THREADCOUNT ;i++)

    {

        g_ThreadData[i] =i;

    }

 

    InitializeCriticalSection( &gDisplayTLS_CS );

 

    for (inti = 0; i < THREADCOUNT; i++)

    {

        hThread[i] =CreateThread(NULL,

            0,                       

            (LPTHREAD_START_ROUTINE)ThreadFunc,

            (LPVOID)g_ThreadData[i],                 

            0,                    

            &IDThread);

 

        if (hThread[i] ==NULL)

            fprintf(stderr,"CreateThread %d error\n",i);

    }

 

    for (inti = 0; i < THREADCOUNT; i++)

        WaitForSingleObject(hThread[i],INFINITE);

 

    DeleteCriticalSection( &gDisplayTLS_CS );

    return 0;

}

 

首先我们应该理解,x86 fs:[2ch]代表的是TEB中的成员


该变量指向存放静态TLS数据的地址的指针的地址

将上述汇编翻译以下如下:

(*(DWORD*)(TEB->ThreadLocalStoragePointer[__tls_index(504C3348h)]+4))++;

我们可以看到,这里静态TLS 的使用同样使用的TEB 中的成员变量,这已经保证了线程相关,而这里出现的内存地址0x504C3348 是什么含义呢?我们可以看到,它代表的是ThreadLocalStoragePointer数组的一个索引,即静态TLS 的一个索引,而且它是DLL 中的变量,我们首先计算其相对偏移然后得到其在DLL 中的位置查看其值还有含义:

 

静态TLS 预先将变量定义在PE 文件内部,一般使用“.tls”节存储,对相关API 的调用由操作系统来完成。这种方式使得TLS 的使用更加简单。

静态TLS 变量的定义:

_declspec(thread) int tlsFlag = 1;

“.tls”节将包含以下信息:

初始化数据

用于每个线程初始化和终止的回调函数

TLS 索引

通过静态方式定义的TLS 数据对象只能用于静态加载的映像文件。


0x502C3348 – 0x502c000 = 0x3348

 

通过上面的截图我们可以看出,该内存地址刚好对应于DLL 的 AddressOfIndex指向的内存地址。


根据上图我们可以发现,静态PE 中并没有该地址对应的内容,因为此时VirtualSize大于RawSize,0x3348刚好在多出来的地址范围内,在程序加载该段之后申请内存空间,然后自动将多余的部分填充为0。后面这个地址处的内容是由Windows加载器填充的,当程序静态链接的时候,不同的模块分别获得不同的值,这里之所以为1,是因为exe模块中已经声明过静态TLS 变量,默认情况下,exe 中对于静态TLS变量的引用使用的是索引为0的内容。因此我们可以看到如下内容:

 

查看exe 文件的TLS 并查看内存如下

exe中的索引值为0,而在release 程序中,直接省略了对于TLS中AddressOfIndex内存的引用。

 

理解上述内容之后,我们来整体介绍静态TLS:

静态线程局部存储预先将变量定义在PE文件内部,一般使用“.tls”节存储。

为了支持像上述的方式访问TLS 变量,PE的”.tls”节包含以下信息:

1.      初始化数据

2.      用于每个线程初始化和终止的回调函数

3.      TLS 索引

 

 

首先了解TLS 结构体

codebase+(module)->headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS].VirtualAddress指向了该结构体:

typedef struct_IMAGE_TLS_DIRECTORY32 {

    DWORD   StartAddressOfRawData;

    DWORD   EndAddressOfRawData;

    DWORD   AddressOfIndex;             //PDWORD

    DWORD   AddressOfCallBacks;         //PIMAGE_TLS_CALLBACK *

    DWORD   SizeOfZeroFill;

    union {

        DWORDCharacteristics;

        struct {

            DWORDReserved0 : 20;

            DWORDAlignment : 4;

            DWORDReserved1 : 8;

        } DUMMYSTRUCTNAME;

    } DUMMYUNIONNAME;

 

} IMAGE_TLS_DIRECTORY32;

 

StartAddressOfRawData

双字。表示TLS模板的起始地址。用于对TLS数据进行初始化。每当创建线程时,系统都要复制所有这些数据

如图所示:

DLL

EXE


我们发现,该地址指向的内容的开始4个字节是不使用的。因此我们直接忽略。

EndAddressOfRawData

双字。表示TLS模板的结束地址。

AddressOfIndex

双字。用于保存TLS索引的位置,索引的具体值由加载器确定

AddressOfCallBacks

双字。这是一个指针,指向由TLS回调函数组成的数组。这个数组是以NULL结尾的,因此,有回调函数的话,这个字段指向的位置处应该是4个字节的0.在线程建立或退出时,其中的每个函数都会被调用,包括主线程和其他线程。TLS 变量初始化和TLS 回掉函数的执行均在主程序的入口点(EntryPoint)之前执行,在主程序退出之前,TLS 回调函数再被执行一次。

SizeOfZeroFill

双字。TLS模板的大小应该与映像文件中的TLS数据的大小一致。用0填充的数据就是已初始化的非零数据后面的那些数据。

Characteristics

双字。保留未用。

 

 

 

其实看到这里,我们已经发现,这个程序对于静态TLS的支持仅仅局限在了TLS 回调函数的调用上,其实并没有意义,甚至是错误的

TLS 回调函数

接下来介绍TLS 回调函数的相关问题:

程序可以通过PE 文件的方式提供一个或多个TLS 回调函数,以支持对TLS 数据进行附加的初始化和终止操作,类似于面向对象中的构造函数和析构函数。其实现为一个以双子0结束的函数指针数组。

其原型为:

 

typedef VOID (NTAPI *PIMAGE_TLS_CALLBACK)(

PVOIDDllHandle,

DWORDReason,

PVOIDReserved

);

参数解释:

1)  Reserved:预留,为 0

2)  Reason:调用该回调函数的时机。与DLLMain参数相同。

3)  DllHandle: DLL 的句柄。

表9-1 TLS回调函数参数Reason常用值

符号名  值  描述

DLL PROCESS, DETACH 0   进程将要被终止,包括第一个线程

DLL PROCESS_ATTACH  1   启动了一个新进程.包括第一个线程

DLL_THREAD_ATTACH   2   2   创建一个新线程。创建所有线程时都会发送这个通知,除第一个线程外

DLL—THREAD_DETACH  3   线程将要被终止。终止所有线程时都会发送这个通知,除第一个线程外

 

代码示例:

#include <windows.h>

 

// Explained in p. 2 below

void NTAPItls_callback(PVOIDDllHandle, DWORDdwReason, PVOID)

{

    if (dwReason ==DLL_THREAD_ATTACH)

    {

        MessageBox(0, L"DLL_THREAD_ATTACH",L"DLL_THREAD_ATTACH", 0);

    }

 

    if (dwReason ==DLL_PROCESS_ATTACH)

    {

        MessageBox(0, L"DLL_PROCESS_ATTACH",L"DLL_PROCESS_ATTACH", 0);

    }

}

 

#ifdef _WIN64

#pragma comment (linker, "/INCLUDE:_tls_used"// See p. 1 below

#pragma comment (linker, "/INCLUDE:tls_callback_func"// See p. 3 below

#else

#pragma comment (linker, "/INCLUDE:__tls_used"// See p. 1 below

#pragma comment (linker, "/INCLUDE:_tls_callback_func"// See p. 3 below

#endif

 

// Explained in p. 3 below

#ifdef _WIN64

#pragma const_seg(".CRT$XLF")

EXTERN_C const

#else

#pragma data_seg(".CRT$XLF")

EXTERN_C

#endif

    PIMAGE_TLS_CALLBACKtls_callback_func= tls_callback;

#ifdef _WIN64

#pragma const_seg()

#else

#pragma data_seg()

#endif //_WIN64

 

DWORD WINAPI ThreadProc(CONSTLPVOIDlpParam)

{

    ExitThread(0);

}

 

int main(void)

{

    MessageBox(0, L"hellofrom main", L"main",0);

    CreateThread(NULL,0, &ThreadProc, 0, 0,NULL);

    return 0;

}

 

 

首先,如果我们想要使用TLS 回调函数,我们应该告诉编译器

因此我们添加了符号:_tls_used,:

#pragma comment (linker, "/INCLUDE:_tls_used")该变量为一个结构体,即上面介绍的”.tls”节对应的结构体。该结构在tlssup.c中定义:

 

#ifdef _WIN64

 

_CRTALLOC(".rdata$T") constIMAGE_TLS_DIRECTORY64 _tls_used =

{

       (ULONGLONG) &_tls_start,       // start of tls data

       (ULONGLONG) &_tls_end,         // end of tls data

        (ULONGLONG) &_tls_index,        // address of tls_index

       (ULONGLONG) (&__xl_a+1),       // pointer to call back array

        (ULONG)0,                      // size of tlszero fill

        (ULONG)0                       //characteristics

};

 

#else  /*_WIN64 */

 

_CRTALLOC(".rdata$T")

const IMAGE_TLS_DIRECTORY _tls_used =

{

       (ULONG)(ULONG_PTR) &_tls_start, // start of tls data

       (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 call back array

        (ULONG)0,                      // size of tlszero fill

        (ULONG)0                       // characteristics

};

 

#endif  /*_WIN64 */

我们将“_tls_used”符号添加到程序编译过程中,就会引用该结构。

这里的_tls_start 和 _tls_end

分别代表的是.tls段的已初始化的数据开始和结束(即前面提到过的存储TLS 变量初始值的地方),其中.tls段的最开始4个字节为0,我们在前面已经看到过了。

_tls_index 是一个为初始化的变量,由Windows加载器填充,前面也介绍过了。

另外,由于x86 模式下,在链接程序的时候’_’ 被自动加上,因此这里需要修改为:

#pragma comment (linker,"/INCLUDE:__tls_used")

 

然后我们应该注册回调函数

同样,我们来看tlssup.c 中的新的数据段的声明:

 

/* Start section for TLS callback array examined by the OSloader code.

 * If dynamic TLSinitialization is used, then a pointer to __dyn_tls_init

 * will be placed in.CRT$XLC by inclusion of tlsdyn.obj. This will cause

 * the .CRT$XD? array ofindividual TLS variable initialization callbacks

 * to be walked.

 */

 

_CRTALLOC(".CRT$XLA") PIMAGE_TLS_CALLBACK __xl_a =0;

 

/* NULL terminator for TLS callback array.  This symbol, __xl_z, is never

 * actually referencedanywhere, but it must remain.  The OSloader code

 * walks the TLScallback array until it finds a NULL pointer, so this makes

 * sure the array isproperly terminated.

 */

 

_CRTALLOC(".CRT$XLZ") PIMAGE_TLS_CALLBACK __xl_z =0;

 

 

 

.CRT 表明使用的是 C RUN TIME 机制,$后面的 XLX 中,X 表示随机的标识,L 表示TLS callback section.X 可以被换成B 到 Y 中的任意一个字母,但不可 A,Z。A 和Z 用于tlssup.obj 的。

这里的节名为.CRT$XLx

需要如此奇怪的节名是因为TLS回调指针需要进行内存排序的原因。为了理解这种特殊声明的作用,需要首先明白编译器和链接器是如何组织PE文件中的数据的。

PE文件中,除了头部数据,其它均是分不同节存储的,节就是具有相同属性(也保护属性)集合的内存区域。关键字__declspec(allocate(“section-name”))告诉链接器在最终PE文件中其作用域内的内容放在指定的节内。链接器额外支持将相似名字的节合并为一个大节的功能。该功能通过使用 节名前缀+$+任意字符串的形式来激活。链接器将合并具有相同节名前缀的节为一个大节。
链接器对于相似节采用字典顺序进行合并(对$后的字符串进行排序)。这意味着在内存中,位于节.CRT$XLB中的变量将在位于节.CRT$XLA中变量位置的后面,但是在位于节.CRT$XLZ中的变量的前面。C运行时库利用链接器的这一特性来创建一个以NULL结尾的TLS回调数组(将节.CRT$XLZ中放置一个NULL指针)。因此为了保证声明的函数指针位于TLS回调数组内部,必须将它放在节.CRT$XLx

 

(这里是CRT即运行时机制,由tlssup.c可以发现,其声明了一个.tls段,最终的内容是放到.tls段中的)。简单示例:

#include <windows.h>

#include <stdio.h>

#pragma comment(lib,"TLSDllTest")

EXTERN_C __declspec(dllimport)unsignedint Test();

// Explained in p. 2 below

void NTAPItls_callback(PVOIDDllHandle, DWORDdwReason, PVOID)

{

    if (dwReason ==DLL_THREAD_ATTACH)

    {

        MessageBoxW(0, L"DLL_THREAD_ATTACH",L"DLL_THREAD_ATTACH", 0);

    }

 

    if (dwReason ==DLL_PROCESS_ATTACH)

    {

        MessageBoxW(0, L"DLL_PROCESS_ATTACH",L"DLL_PROCESS_ATTACH", 0);

    }

}

void NTAPItls_callback_2(PVOIDDllHandle, DWORDdwReason, PVOID)

{

    if (dwReason ==DLL_THREAD_ATTACH)

    {

        MessageBoxW(0, L"DLL_THREAD_ATTACH",L"2", 0);

    }

 

    if (dwReason ==DLL_PROCESS_ATTACH)

    {

        MessageBoxW(0, L"DLL_PROCESS_ATTACH",L"2", 0);

    }

}

#ifdef _WIN64

#pragma comment (linker, "/INCLUDE:_tls_used"// See p. 1 below

#pragma comment (linker, "/INCLUDE:tls_callback_func"// See p. 3 below

#else

#pragma comment (linker, "/INCLUDE:__tls_used"// See p. 1 below

#pragma comment (linker, "/INCLUDE:_tls_callback_func"// See p. 3 below

#endif

 

// Explained in p. 3 below

#ifdef _WIN64

#pragma const_seg(".CRT$XLD")

EXTERN_C const

#else

#pragma data_seg(".CRT$XLD")

EXTERN_C

#endif

    PIMAGE_TLS_CALLBACKtls_callback_func= tls_callback;

#ifdef _WIN64

#pragma const_seg()

#else

#pragma data_seg()

#endif //_WIN64

 

 

/////////////////////////////////////////////////////////////////////////////////////////////////////////

#ifdef _WIN64

#pragma comment (linker, "/INCLUDE:tls_callback_func_2"// See p. 3 below

#else

#pragma comment (linker, "/INCLUDE:_tls_callback_func_2"// See p. 3 below

#endif

 

// Explained in p. 3 below

#ifdef _WIN64

#pragma const_seg(".CRT$XLC")

EXTERN_C const

#else

#pragma data_seg(".CRT$XLC")

EXTERN_C

#endif

    PIMAGE_TLS_CALLBACKtls_callback_func_2= tls_callback_2;

#ifdef _WIN64

#pragma const_seg()

#else

#pragma data_seg()

#endif //_WIN64

DWORD WINAPI ThreadProc(CONSTLPVOIDlpParam)

{

    ExitThread(0);

}

 

int main(void)

{

    MessageBoxW(0, L"hellofrom main", L"main",0);

    Test();

    CreateThread(NULL,0, &ThreadProc, 0, 0,NULL);

    getchar();

    getchar();

    return 0;

}

先执行后执行

 

我们看到,这里tlssup.c 中已经定义过了 A 和 Z,因此我们程序中只能定义 B 到 Y。

系统会自动执行我们的回调函数,那么执行到什么时候结束呢?答案是上面声明的

_CRTALLOC(".CRT$XLZ") PIMAGE_TLS_CALLBACK __xl_z =0;

它的值为0,系统检测到0时,停止执行回调函数,当我们自己声明的函数数组没有以零结尾的时候,它保证程序的正确性另外,我们声明的函数指针同样可以是一个数组,这样我们就可以一次声明多个回调函数,其依次执行。好好理解这些话

简单示例:

#include <windows.h>

#include <stdio.h>

//#pragmacomment(lib,"TLSDllTest")

//EXTERN_C __declspec(dllimport) unsignedint Test();

// Explained in p. 2 below

void NTAPItls_callback(PVOIDDllHandle, DWORDdwReason, PVOID)

{

    if (dwReason ==DLL_THREAD_ATTACH)

    {

        MessageBoxW(0, L"DLL_THREAD_ATTACH",L"DLL_THREAD_ATTACH", 0);

    }

 

    if (dwReason ==DLL_PROCESS_ATTACH)

    {

        MessageBoxW(0, L"DLL_PROCESS_ATTACH",L"DLL_PROCESS_ATTACH", 0);

    }

}

void NTAPItls_callback_2(PVOIDDllHandle, DWORDdwReason, PVOID)

{

    if (dwReason ==DLL_THREAD_ATTACH)

    {

        MessageBoxW(0, L"DLL_THREAD_ATTACH",L"2", 0);

    }

 

    if (dwReason ==DLL_PROCESS_ATTACH)

    {

        MessageBoxW(0, L"DLL_PROCESS_ATTACH",L"2", 0);

    }

}

#ifdef _WIN64

#pragma comment (linker, "/INCLUDE:_tls_used"// See p. 1 below

#pragma comment (linker, "/INCLUDE:tls_callback_func"// See p. 3 below

#else

#pragma comment (linker, "/INCLUDE:__tls_used"// See p. 1 below

#pragma comment (linker, "/INCLUDE:_tls_callback_func"// See p. 3 below

#endif

 

// Explained in p. 3 below

#ifdef _WIN64

#pragma const_seg(".CRT$XLD")

EXTERN_C const

#else

#pragma data_seg(".CRT$XLD")

EXTERN_C

#endif

    PIMAGE_TLS_CALLBACKtls_callback_func[]= {tls_callback,tls_callback_2,0};

#ifdef _WIN64

#pragma const_seg()

#else

#pragma data_seg()

#endif //_WIN64

 

 

DWORD WINAPI ThreadProc(CONSTLPVOIDlpParam)

{

    ExitThread(0);

}

 

int main(void)

{

    MessageBoxW(0, L"hellofrom main", L"main",0);

    CreateThread(NULL,0, &ThreadProc, 0, 0,NULL);

    getchar();

    getchar();

    return 0;

}

 

参考

http://m.2cto.com/kf/201604/503658.html

http://stackoverflow.com/questions/14538159/about-tls-callback-in-windows

http://www.nynaeve.net/?p=183

 

0 0