线程局部存储,Part 6:Windows Server 2003中隐式TLS支持方法设计中的问题

来源:互联网 发布:改图软件 编辑:程序博客网 时间:2024/06/03 11:25

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

上周,我描述了在WindowsServer 2003中加载器如何处理隐式TLS支持。尽管TLS支持对于最初的要求支持的挺好,但是仍然存在一些让人不悦的地方。如果你一直看到这里,你可能已经注意到隐式TLS支持中设计方面的问题。这些缺陷最终鞭策微软在vista版本中对隐私TLS进行了重要的修正。

WindowsServer 2003及其早期版本中,隐式TLS实现的主要问题是对动态载入的DLL文件将完全不起作用(使用LoadLibrary和LdrLoadDll动态载入)。事实上,动态加载使用TLS的DLL将会产生巨大的灾难。

最终发生的事情是,对于动态载入的DLL其TLS将不会被处理。就目前所了解的TLS内部机制,这样很明显会产生不幸的后果。

当一个使用了隐式TLS的DLL被动态载入时,由于加载器不会处理TLS目录,其_tls_index的值没有被初始化,模块对应当前线程的TLS存储空间和ThreadLocalStoragePointer数组都没有分配。但是DLL会被加载成功,看起来也可以工作,直到你第一次访问__declspec(thread)变量。

具有代表性的,编译器默认将_tls_index初始化为0,因此在进程初始化之后动态载入的DLL其_tls_index将保持为0.当访问__declspec(thread)变量时,会进行隐式TLS变量解析过程。换言之,ThreadLocalStoragePointer的值将会被获取并用_tls_index进行索引(该值对动态载入的dll来说始终为0),索引得到的结果指针被认为是当前线程中对应本模块中的TLS变量的地址。不幸的是,加载器并没有设置该模块的_tls_index值为一个有效的值,因此该模块将会引用那个_tls_index值被赋予0的模块的线程局部存储变量。一般该模块将会是主模块,如果主模块没有使用tls,那么将是某一个静态链接的使用了TLS的DLL模块。

在调试时,这将导致一个非常难以被发现的问题。现在你有了一个任意蹂躏其它模块状态的模块,而导致问题的模块将会认为其修改是属于自己的线程状态。如果幸运,应用程序完全没有使用隐式TLS功能(进程初始化时),ThreadLocalStoragePointer数组将不存在,在第一次访问__declspec(thread)变量时将会引发一个NULL指针解析。但是更多的是进程中存在其它模块使用隐式TLS,这种情况下,TLS索引为0的模块的线程数据将会被新载入的模块破坏。

这种情况下,程序崩溃将会延迟到模块发现数据出现了问题。也可能你足够幸运,新载入的模块的TLS内存空间比TLS索引为0的模块占用的TLS内存空间大很多,这样在访问__declspec(thread)变量时如果超出了堆分配的界限,也会立即出现访问错误。当然,如果访问的数据刚好位于堆内存中记录堆分配的内存区,那么会导致堆溢出。(加载器使用进程堆来分配TLS存储空间)

也许加载器关于隐式TLS和按需载入DLL的限制的一个补救措施就是由于加载器对这两种情况的不支持,大多数的程序员都知道在与DLL进行合作时何时需要远离使用隐式TLS。

这些可怕的按需载入使用了__declspec(thread)变量的dll所造成的后果大概是MSDN中关于在按需载入dll中使用隐式TLS的警告出现的原因吧。

很明显,从调试角度来看,按需载入使用了隐式TLS的DLL所出现的错误很难发现。这个问题严重限制了__declspec(thread)变量的使用。

幸运的是,Vista的加载器使用了一些办法解决这个问题,这样就可以安全的使用__declspec(thread)变量了。新的加载器支持按需加载DLL使用隐式TLS,但是实现相对复杂(由于考虑兼容性的原因)

下一次,我将会进一步对vista的加载器如何支持这一特性进行分析,同时包含一些新实现中需要注意的问题。
原创粉丝点击