Windows PE第九章 线程局部存储(TLS表)

因为书中讲的太过抽象,并且我对msan32编程没有兴趣

把书啃一遍,然后跟网上的技术文章分析比较容易理解消化

什么是线程局部存储?

线程局部存储(Thread Local Storage,TLS)很好的解决了多线程设计中变量同步问题,比如你写一个exe里面有N个线程,你可以放弃使用TLS,因为你对自己设计的程序有比较全面的把握。你清楚自己设计的进程里总共有多少个线程,每个线程使用了哪些数据结构,内存空间申请、释放都在你的掌控之下,全局变量的访问全部都采用了同步技术,那是没问题的。如果你是一个DLL开发者,你无法确定调用这个DLL的素质程序里到底有多少个线程,每个线程的数据是如何定义的,这时,可以考虑使用线程据存储技术。

TLS分为静态和动态两种:

动态TLS

主要是使用这几个API

TlsAlloc 分配线程局部存储空间

TlsFree 释放线程局部存储空间

TlsGetValue 获得线程局部存储空间里面的值

TlsSetValue 设置线程局部存储空间的值

TLSAPI的使用

首先是TlsAlloc的使用

DWORD TlsAlloc(VOID);  函数原型

调用一次TlsAlloc则会分配4个字节的空间,不管你在哪里调用,如果在main里面(主线程)中调用,那么当你创建线程的时候
线程会默认有4个字节的控件
返回值是一个索引, 这个索引是查FS寄存器数组的值当然,这个一会讲解.只需要知道,当我们为每一个线程申请了4个字节的空间
那么索引是一样的,但是索引操作的数据是不一样的
比如 你申请的索引是1
那么在A线程中,操作1索引的时候,那么操作的是A线程的,那么如果在B线程操作索引1的时候,那么操作的是B线程的数据
举例子:
比如有个电话号码是  12345678
中国: 12345678
外国: 12345678 (把电话号码看做是索引)
我们知道,电话号码是一样的,但是你打这个电话的时候,人是不一样的
比如我在中国打123456 那么接听人是张三
我在外国打123456 那么接听人是李四
其中张三李四就是表达了对同一数据的不同操作.看下代码
再比如:
我们使用tlsAlloc申请了4个字节的空间
索引就是nindex (看做是g_dwNumber);
那么访问不同线程的索引,那么索引里面的值是不同的.

Tls的静态使用(真正用法)

其实TLS真正的用法是静态使用,操作系统已经帮你集成了语法了

看下用法,以及语法;

语法:

__declspec(thread) 类型 变量名

然后tls就会自动生成表了,操作系统帮你升成上面动态使用的代码.(所以为啥要理解动态使用)

用的时候还是正常使用.

我们的代码都不用变的.

但其实汇编代码还是会编译为上面的动态使用.

如果变为结构体,那么是一样的,只需要把类型变成结构体的类型即可.

PE中TLS表的设计

了解了上方的原理了,那么如果让你设计表格你要怎么设计?

1.我们全局变量初始化为0了,那么我们肯定有地方存储了这个全局变量的数据 ,所以我会设计一段分为存储这个值.

2.我们常用的nindex索引,那么我觉着也要存储一下

废话不说了,看下真是的结构体

  1. ypedef struct _IMAGE_TLS_DIRECTORY32 {
  2. DWORD StartAddressOfRawData;    TLS初始化数据的起始地址
  3. DWORD EndAddressOfRawData;      TLS初始化数据的结束地址 两个正好定位一个范围,范围放初始化的值
  4. DWORD AddressOfIndex; TLS 索引的位置
  5. DWORD AddressOfCallBacks; Tls回调函数的数组指针
  6. DWORD SizeOfZeroFill;         填充0的个数
  7. union {
  8. DWORD Characteristics;      保留
  9. struct {
  10. DWORD Reserved0 : 20;
  11. DWORD Alignment : 4;
  12. DWORD Reserved1 : 8;
  13. } DUMMYSTRUCTNAME;
  14. } DUMMYUNIONNAME;
  15.  
  16. } IMAGE_TLS_DIRECTORY32;

首先介绍前两个成员,

起始地址 结束地址 定位了一个范围,那么这个范围内存放的就是初始化的值(注意只有静态使用才有TLS表)也就是上方我们定义的g_dwNumber = 0;存放了0,但是因为0不好看,这里我重新赋值为12345678 代码不贴了.

我们查看下PE定位一下Tls的位置.

注意,因为我是VS2015编写的程序,随机基址懒得去了,直接在PE中修改了,把文件头的文件属性修改了即可.

以前是02,现在改成03即可.

首先查看下数据目录的第9项

得出RVA = 000176FC

查看下模块首地址. 首地址是 00400000

看下属于哪个节

命中在.rdata节,RVA = 00016000

上面的RVA减去现在的RVA = 偏移

000176FC – 00016000 = 16FC

节中的文件偏移 + 偏移 = 文件中的位置.

文件偏移是下方的第二个成员

5400 + 16FC = 6AFC

查看6AFC定位Tls表的位置.

前面两个成员分别指向的是

0041B000 0041B208的位置 结束地址 – 起始地址 = 范围.

寻找起始地址的FA

时间关系,这里命中的节是 Rva = 001B000

那么转为文件偏移

FA = 8400h直接计算出来了

起始地址是8400h 那么+208就是8608 ,那么8400h 到8608的位置就存放的初始值,现在已经看到上图画出来的12345678了(小尾方式读取)

第3个成员: 索引的值,这个你可以自己转化查看.

TLS结构体第四个成员,回调函数的数组指针

这个怎么理解,是这样的,还记到动态使用的时候,我们不是在主线程中 TlsAlloc 和TlsFree吗

现在我们可以注册回调函数,操作系统会调用这个回调函数.

怎么注册?

关键字: 加段,必须添加到特定的段中

首先先看下回调的函数原型.

typedef VOID
(NTAPI *PIMAGE_TLS_CALLBACK) (
PVOID DllHandle,
 DWORD Reason,
PVOID Reserved );
PIMAGE_TLS_CALLBACK 其中这个回调是从结构体中第四个成员里面,注释得到的
首先我们自己写一个

请看注释,其实这里才是真正的申请和释放,注意,这个回调函数操作系统会从问价那种读取地址,然后执行一遍,没有申请内存,所以这里面可以藏代码的.

注意,虽然回调我们写了,但是要让操作系统调用,那么我们需要添加一个特定的节.

语法:

#pragma data_seg(“.CRTXLB).CRT

XLB 为什么是这个节,我发下连接看雪论坛的,自己看下吧,很简单了.https://bbs.pediy.com/thread-108015.htm

/*中间写代码,定义函数回调数组*/

PIMAGE_TLS_CALLBACK ary[] = {MyTlsCallBack,0}; //0结尾,那么操作系统就会在文件中找到这个位置,调用一下这个回调.如果多个,里面可以写多个,0结尾即可.

#pragma data_seg();

发现1已经成功弹出来了,那么现在结构体的第四个成员,就是指向这个数组首地址的.PE加载的时候,会默认调用,然后依次执行一遍..

请注意,只会在文件中存储,如果你跑到内存中查看,这个地址是没有的.

作者:IBinary
出处:http://www.cnblogs.com/iBinary/
版权所有,欢迎保留原文链接进行转载:)

坚持两字,简单,轻便,但是真正的执行起来确实需要很长很长时间.当你把坚持两字当做你要走的路,那么你总会成功.

发表评论

邮箱地址不会被公开。 必填项已用*标注