第八讲,TLS表(线程局部存储)
一丶复习线程相关知识 首先讲解TLS的时候,需要复习线程相关知识,? (thread local storage?) 1.了解经典同步问题 首先我们先写一段C++代码,开辟两个线程去跑,看看会不会出现同步问题. 看结果得知,结果并不是正确的,造成同步的问题的原因是两个线程都对同一个变量进行访问. 解决问题: 1.使用同步对象.? (自旋锁自加锁互斥体事件? 信号灯? 临界区.....等等都可以.) 这里使用自加锁解决(当然可以用别的) InterlockedIncrement? API? ?原型: LONG InterlockedIncrement( ? LPLONG volatile lpAddend?? // variable to increment ); 使用之后结果是正确的 ? 二丶何为TLS? (Thread??local storage) 所谓TLS,意思就是指,每个线程都有自己的空间,局部存储,什么意思? 比如上方我们对一个g_dwNumber进行操作,那么我们就要使用同步对象,我们不妨这样去想,每个线程,开辟一个空间 当对A线程进行操作的时候,操作的是A线程的g_dwNumber,当对B线程进行操作的时候,是对B线程的g_dwNumber进行操作. 其实很简单,介绍一下TLS的API 总共4个 分别是: TlsAlloc? 分配线程局部存储空间 TlsFree? 释放线程局部存储空间 TlsGetValue 获得线程局部存储空间里面的值 TlsSetValue 设置线程局部存储空间的值 ? 三丶TLSAPI的使用 1.首先是TlsAlloc的使用 DWORD TlsAlloc(VOID);? 函数原型 首先,我们为每个线程开辟了4个字节的空间 BThread (当前索引为1) AThread[1] = 0; DWORD g_dwNumber = AThread[1]; printf(g_dwNumber); AThread[1] = g_dwNumber++; 替换成API则是 很简单 1.我们定义一个p指针,指向了一块new的内存 2.初始化的时候,设置数组索引的当前索引的值为p的指针 3.从索引中获得p指针 4.修改p指向的m_dwCount的值 注意,这里因为p是一个指针,我们修改的只是它空间成员变量的值,所以不用重新再设置回去了. 到了现在感觉TLS是不是有点难用了.其实使用TLS 比使用任何同步对象都快,就相当于没同步的时候的速度. 但是TLS的真正的语法不是这样用的.(上面是动态使用不会生成TLS表) ? 3.Tls的静态使用(真正用法) 其实TLS真正的用法是静态使用,操作系统已经帮你集成了语法了 看下用法,以及语法; 语法: __declspec(thread) 类型? 变量名 然后tls就会自动生成表了,操作系统帮你升成上面动态使用的代码.(所以为啥要理解动态使用) 用的时候还是正常使用. 我们的代码都不用变的. 但其实汇编代码还是会编译为上面的动态使用. 如果变为结构体,那么是一样的,只需要把类型变成结构体的类型即可. ? 四丶PE中TLS表的设计 了解了上方的原理了,那么如果让你设计表格你要怎么设计? 1.我们全局变量初始化为0了,那么我们肯定有地方存储了这个全局变量的数据,所以我会设计一段分为存储这个值. 2.我们常用的nindex索引,那么我觉着也要存储一下 看下结构体 typedef struct _IMAGE_TLS_DIRECTORY32 { DWORD StartAddressOfRawData; TLS初始化数据的起始地址 DWORD EndAddressOfRawData; TLS初始化数据的结束地址 两个正好定位一个范围,范围放初始化的值 DWORD AddressOfIndex; TLS 索引的位置 DWORD AddressOfCallBacks; Tls回调函数的数组指针 DWORD SizeOfZeroFill; 填充0的个数 union { DWORD Characteristics; 保留 struct { DWORD Reserved0 : 20; DWORD Alignment : 4; DWORD Reserved1 : 8; } DUMMYSTRUCTNAME; } DUMMYUNIONNAME; } 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")其中关于.CRTXLB")其中关于.CRTXLB 为什么是这个节,我发下连接看雪论坛的,自己看下吧,很简单了.https://bbs.pediy.com/thread-108015.htm /*中间写代码,定义函数回调数组*/ PIMAGE_TLS_CALLBACK ary[] = {MyTlsCallBack,0}; //0结尾,那么操作系统就会在文件中找到这个位置,调用一下这个回调.如果多个,里面可以写多个,0结尾即可. #pragma data_seg(); ? 发现1已经成功弹出来了,那么现在结构体的第四个成员,就是指向这个数组首地址的.PE加载的时候,会默认调用,然后依次执行一遍.. 请注意,只会在文件中存储,如果你跑到内存中查看,这个地址是没有的. (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |