线程局部存储,Part 5:加载器对__declspec(thread)变量的支持(进程初始化阶段)

原文网址:http://www.nynaeve.net/?p=186

上次,我描述了编译器和链接器为访问__declspec(thread)扩展类变量所使用的生成代码的机制。尽管此时它们已经为隐式TLS布置了舞台,但为了使整体能够工作,仍然需要加载器这个组件来提供必需的运行时支持。

具体的,加载器将负责为每个模块分配TLS索引值,为每个线程的TEB中的ThreadLocalStoragePointer分配内存空间。此外加载器还需要为每个模块分配TLS存储空间。

概念上,加载器中和TLS相关的分配和管理职责可以被划分为四个方面:(注意这是在Windows Server 2003版本和比其早的版本;之后将分析下Vista中所做的修改)

1.    进程初始化阶段,为变量_tls_index分配索引值,确定每个模块所需的TLS空间内存的大小,然后调用TLS和DLL初始化函数(同一模块,先调用TLS初始化函数,后调用DllMain初始化函数)。

2.    在线程初始化阶段,为每一个使用了TLS的模块分配TLS内存并初始化,根据使用TLS的模块数目为当前线程分配ThreadLocalStoragePointer数组,然后将各个模块的TLS内存和ThreadLocalStoragePointer数组中的对应项相关联。然后为当前线程调用TLS初始化函数和DLLMain初始化函数。

3.    在线程终止的时候,调用TLS初始化函数和DLLMain函数(根据参数确定是线程终止),释放当前线程中每个模块对应的TLS内存,然后释放ThreadLocalStoragePointer数组。

4.    在进程终止时,调用TLS和DLlmain初始化函数。

当然,加载器在完成上面所列工作的同时也做了其他事情;以上所列的只是TLS支持中的关键部分。

除了进程初始化以外,其它大部分操作都非常直观。进程初始化主要是由ntdll中的LdrpInitializeTls和LdrpAllocateTls两个例程来完成的。

当所有静态连接的dll文件被载入之后,所有其它初始化例程被调用之前,LdrpInitializeTls被调用(说明优先级比较高,是关键的部分)。基本上,该函数要遍历所有加载模块,为每一个具有有效TLS目录的模块统计出它使用的TLS内存的大小。对每一个使用了TLS的模块,会分配一个数据结构来记录该模块所使用的TLS内存大小并为其分配的索引号(_tls_used)。(早在Xp系统中,LDR_DATA_TABLE_ENTRY结构中的TlsIndex域貌似就没有使用了。而在WINME系统中将该值误用为模块的TLS索引,因此假定该值为-1在WINME系统中是不可靠的)

使用了TLS的模块在调用LdrpInitializeProcess的过程中将被标记为始终位于内存当中(这种模块的LoadCount值为0xFFFF)。实际中,这个不是什么问题,因为这种模块必须是静态链接的或是被主模块隐式依赖,不可能中途退场。

在函数LdrpInitializeTls为模块分类了TLS索引之后,将调用LdrpAllocateTls为初始线程初始化TLS值。

这时,进程继续初始化,最后每个模块的TLS初始化和DLLmain初始化函数会被调用。(注意应用程序主模块可以有多个TLS回调函数,但是没有DLLmain函数)

一个有意思的事情是同一个DLL模块的TLS初始化函数始终在DLL初始化函数之前调用。(这个过程按顺序进行,例如先A.dll的TLS初始化,A.dll的DLLmain初始化,B.dll的TLS初始化,B.dll的Dllmain初始化,以此类推)。这意味着在TLS初始化函数中要慎重使用CRT的函数((as the C runtime is initialized before the user’s DllMain routineis called, by the actual DLL initializer entrypoint, such that the CRT will notbe initialized when a TLS initializer for the module is invoked).)。这将非常危险,因为全局数据还没有被创建;除非导入被跳过,否则模块将处于一个完全未初始化的状态。

另一个值得一提的有关加载器对TLS支持的方面是PE文件格式标准中,IMAGE_TLS_DIRECTORY结构中的SizeOfZeroFill域并没有被链接器和加载器使用。这意味着在现实中,所有TLS模板数据都将初始化,TLS内存块的大小不像PE文件格式标准所陈述的的那样包含域SizeOfZeroFill。

一些软件滥用TLS回调来用于反调试的目的(通过创建一个TLS回调项来在入口函数获得执行权之前执行代码),虽然可以,但是实际中这点将非常明显,因为大部分PE文件都不会使用TSL回调。

直到Windows Server 2003,上述就是加载器对__declspec(thread)存储类的所有支持。这个方法看起来工作的很好,但事实上存在些问题(如果你一直看到现在,你也许会发现是什么问题)。更多关于以上方法的限制的讨论将在下一周为大家讲述。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值