TLS线程局部存储

Windows内核函数的命名

Windows的内核函数在命名上有个很好的特色,就是函数名都按其所在的层次或模块加上了特定的前缀。

主要的前缀有:

Zw:Zw 和同名的 Nt 函数具有相同的功能,中间是从 Zw 到 Nt 函数的简单跳转。本系列内核函数用于文件和注册表方面的操作,比如文件操作、注册表操作、访问进程、事件操作、令牌操作、进程操作和端口操作等。

Ex:管理层,Ex 是 Executive 的开头两个字母。
Ke:核心层,Ke 是 Kernel 的开头两个字母。
Hal:硬件抽象层,Hal 是 Hardware Abstranction Layer 的缩写。
Ob:对象管理,Ob 是 Object 的开头两个字母。
Mm:内存管理,Mm 是 Memory Manager 的缩写。
Ps:进程(线程)管理,Ps 表示Process。
Se:安全管理,Se 是 Security 的开头两个字母。
Io:I/O管理。
Fs:文件系统,Fs 是 File System 的缩写。
Cc:文件缓存管理,Cc 表示 Cache。
Cm:系统配置管理,Cm 是 Configuration Manager 的缩写。
Pp:“即插即用”管理,Pp 表示 PnP。
Rtl:运行时程序库,Rtl 是 Runtime Library 的缩写。本系列内核函数用于运行时库,以Rtl为前缀的函数可以完成多种操作,例如字符串、线程、资源、临界区、安全对象的初始化和使用,内存、进程异常和数据类型的处理,还用于完成定时器、堆、IPv4和IPv6方面的操作,以及压缩和解压缩等。

Ndis:与NDIS网络驱动开发相关的函数。
Wdf:开发WDF驱动相关的函数都是以 Wdf 开头。
KiEtw:本系列内核函数用于系统内核,这些函数只能从内核的内部进行调用,常用的有:KiUserCallbackDispatcher、KiRaiseUserExceptionDispatcher、KiUserApcDispatcher、KiUserExceptionDispatcher等。

Csr系列:此系列函数用于客户机和服务器运行时,如果您想拦截客户机/服务器方面的操作,那么就需要对Csr系列内核函数做进一步的了解。常见的有:CsrClientCallServer、CsrCaptureMessageBuffer、CsrConnectClientToServer和CrsNewThread等。

Ldr系列:本系列内核函数用于加载程序管理器,如果你打算拦截加载程序的话,那么请进一步考察这组以Ldr为前缀的函数,常用的有:LdrInitializeThunk、LdrLockLoaderLock、LdrUnlockLoaderLock、LdrGetDllHandle、LdrGetProcedureAddress等。

Dbg系列:本系列内核函数用于调试管理,如果打算拦截调试操作的话,那么请进一步考察这组以Dbg为前缀的函数,常用的函数包括:、DbgBreakPoint、DbgUserBreakPoint、DbgPrint和DbgUiConnectToDbg等。

Etw系列:本系列内核函数用于追踪窗口事件,如果你打算拦截追踪之类的操作的话,那么请进一步考察这组以Etw为前缀的函数。常用的函数包括:EtwTraceEvent、EtwEnableTrace、EtwGetTraceEnableLevel和EtwGetTraceEnableFlags等。

Pfx系列:本系列内核函数用于ANSI字符串操作,如果你打算拦截ASNI串表方面的操作的话,就需要进一步了解这些函数。常用的包括:PfxInitialize、PfxRemovePrefix、PfxInsertPrefix、PfxFindPrefix等。

不过并非所有函数名都带有这样的前缀,对前缀的使用也并非非常严格。
例如:核心层函数名的前缀本应该是Ke,但实际上有不少核心层函数名的前缀是 Ki,这些函数大都是与中断有关的比较底层的函数。

有时候还在函数名的前缀后面加上小写字母 f,表示这个函数是快速调用函数。
例如:NTKERNELAPI LONG_PTR FASTCALL ObfDereferenceObject(IN PVOID Object);
这里的类型说明 FASTCALL 向编译工具表明这是个快速调用函数,函数名前缀加 f 则使人一看见函数名就知道这个函数是个快速调用函数,一般的函数调用是通过堆栈传递参数的,而快速调用函数则通过 ECX 等寄存器传递参数,因为避免了几个堆栈操作而使效率有所提高。
这里的 FASTCALL 是必须的,否则编译工具不知道应该通过寄存器传递参数,而在函数名中加 f 则只是为了增加程序的可读性。

Cc Cache manager
Cm Configuration manager
Ex Executive support routines
FsRtl File system driver run-time library
Hal Hardware abstraction layer
Io I/O manager
Ke Kernel
Lpc Local Procedure Call
Lsa Local security authentication
Mm Memory manager
Nt Windows 2000 system services (most of which are exported as Win32 functions)
Ob Object manager
Po Power manager
Pp PnP manager
Ps Process support
Rtl Run-time library
Se Security
Wmi Windows Management Instrumentation
Zw Mirror entry point for system services (beginning with Nt) that sets previous access mode to kernel, which eliminates parameter validation, since Nt system services validate parameters only if previous access mode is user

Windows体系结构

前置知识

陷阱(一种内部中断)
计算机有两种运行模式:用户态、内核态。 其中操作系统运行在内核态,在内核态中,操作系统具有对所有硬件的完全访问权限,可以使机器运行任何指令;相反,用户程序运行在用户态,在用户态下,软件只能使用少数指令,它们并不具备直接访问硬件的权限。这就出现了问题,假如软件需要访问硬件或者需要调用内核中的函数该怎么办呢,这就是陷阱的作用了。陷阱指令可以使执行流程从用户态陷入内核(这也就是为什么叫做陷阱,而不是捕猎的陷阱)并把控制权转移给操作系统,使得用户程序可以调用内核函数和使用硬件从而获得操作系统所提供的服务,比如用视频软件放一个电影,视频软件就发出陷阱使用显示器和声卡从而访问硬件。

操作系统有很多系统调用接口供用程序调用。陷阱的发生时间是固定的,比如第一次用视频软件时,在加载视频时软件会向操作系统发送陷阱指令,第二次播放时,软件仍然会在同样的时刻发送陷阱指令。这一点是和中断的明显的差别之一。下面介绍中断。

中断
中断是由外部事件导致并且它发生的时间是不可预测的,这一点和陷阱不同。**外部事件主要是指时钟中断,硬件中断等。**由于CPU一次只能运行一条指令,所以在一个时刻只能有一个程序运行,但我们感觉在我们的计算机中明明可以同时运行很多程序啊,这是由于CPU在多个进程之间快速切换所导致的伪并行。如果某一个程序运行了足够长用完了分配给它的时间片,CPU决定切换到另一个进程运行,就会产生一个时钟中断,切换到下一个进程运行。

硬件中断顾名思义就是由硬件引起的中断,比如一个程序需要用户输入一个数据,但用户一直没有输入,操作系统决定是一直等待用户输入还是转而运行别的进程,一般情况是转而运行别的进程,如果用户的输入到来了,那么键盘驱动器会产生一个中断通知操作系统,操作系统保存正在运行的程序的状态,从而切换到原来的进程处理到来的数据。

所以中断发生是随机的且主要作用是完成进程间切换,从而支持CPU和设备之间的并行。
中断和异常的另一个重要差别是,CPU处理中断的过程中会屏蔽中断,不接受新的中断直到此次中断处理结束。而陷阱的发生并不屏蔽中断,可以接受新的中断。

优先级越高CPU就优先处理,优先级越低,CPU就最后处理。

异常
异常就是程序执行过程中的异常行为。比如除零异常,缓冲区溢出异常等。不同的操作系统定义了不同种类和数量的异常并且每个异常都有一个唯一的异常号,异常会扰乱程序的正常执行流程,所以异常是在CPU执行指令时本身出现的问题,比如除数为零而出现的除零异常。异常的产生表示程序设计不合理,所以在编程的时候要尽量避免异常的产生。

体系结构

总体上讲,Windows体系结构包含用户模式部分和内核模式部分。

请添加图片描述

内核模式负责向用户模式提供可供操作的接口,这种模式下,各模块独立操作,通过良好的消息沟通机制进行彼此间的通信。
应用程序奉时运行在用户模式下,当需要用到系统内核所提供的服务时,操作系统通过特殊的指令将指令地址指针eip转移到内核模式下执行,完成后再将控制权交还给用户模式下的代码。有效地保护Windows操作系统的核心代码,使其不受应用程序的错误所影响,具有良好的稳定性和可扩展性。

请添加图片描述

人们把Windows安全层次设置为Ring (环状)结构,最外层环被命名为Ring3,表示用户模式;最里层环被命名为Ring0,表示内核模式。

通常情况下,用户在应用程序中会使用Win32API调用相关函数,对大部分函数的调用最终都转到ntdll.dll中。ntdll.dll是连接用户模式代码和内核模式系统服务的桥梁,对于内核模式中提供的毎一个服务,在ntdll.dll中都有一个相应的存根函数。该存根没有代码的具体实现,只提供参数传递和跳转功能,大部分函数的运行都转到了内核模式的ntoskml.exe中进行

内核模式下进程与线程的创建过程

在内核中,一个进程的创建是从函数NtCreateProcess开始的。该函数位于文件ntoskml.exe 中,该文件位于%windir%\system32。它对用户传进的部分参数进行简单处理,然后交给函数 NtCreateProcessEx。

NTSTATUS NtCreateProcessEx(
  OUT	PHANDLE           	ProcessHandle,	                         //返回进裎句柄
  IN	ACCESS_MASK      	DesiredAccess,	                        //新进枝访问权限
  IN	POBJECT_ATTRIBUTES  ObjectAttributes OPTIONAL,	             //进程对象属性
  IN	HANDLE	            ParentProcess,	                         //父进裎句柄
  IN	ULANG	            Flags,	                               //标志集合
  IN	HANDLE	            SectionHandle OPTIONAL,	               //该进锃映像文件句柄
  IN	HANDLE	            DebugPort OPTIONAL,	                   //调试端口对象指针
  IN	HANDLE	            ExceptionPort OPTIONAL,           	 //异常端口对象指针
  IN	ULONG	J           obMemberLevel	                     //要创建的进植在一个Job集中的级别
)

该函数检查句柄ProcessHandle是否可写,然后将创建工作交给函数,PspCreateProcess (psp指程序段的前缀)系统中所有进程的创建工作均是由该函数负责完成的,其创建进程的步骤大致如下:
步骤1:调用函数ObCreateObject 创建Windows执行体内核对象EPROCESS,该对象为新进程的进程对象:该对象归系统所有,并由系统统一管理。
步骤2:调用函数ObReferenceObjectByHandle 获取内存区对象的指针SectionHandle (节点句柄)
步骤3:根据传入的端口参数内容初始化新进程对象的相应字段。
步骤4:如果父进程不为空,则创建一个全新的地址空间。
步骤5:调用函数KelnitializcProcess 初始化新进程对象的基本优先级、Affinity(CPU亲和性或CPU绑定)、进程页表目录和超空间的页帧号。
步骤6:调用函数PspInitializeProcessSecurity 从父进程复制一个令牌初始化新进程的安全属性,并设置进程优先级。
步骤7:调用函数MmlnitializeProcessAddressSpace 初始化新进程的地址空间,并将映像映射到地址空间;同时加载Ntdll.dll和系统范围的国家语言支持(NLS)表。
步骤8:调用函数ExCreateHandle 在CID句柄表中创建一个进程ID项。CID是客户身份号(Client ID),一般由进程号和线程号两部分组成,用于识别某个进程。
步骤9:审计本次进程创建行为,并构造PEB(进程环境信息块),将新进程加入全局进程链表PsActive ProcessHead 中
步骤10:调用函数OblnsertObject 将新进程对象插入当前进程的句柄表中。
步骤11:设置进程基本优先级、访问权限、创建时间。
通过以上步骤,进程对象创建完成。但进程是有“惰性”的,离开了线程,进程的空间只是一个固定在内存中的死的空间而已:直到它的第一个线程被创建和初始化之后,进程中的代码才能被真正地运作起来。

内核中线程的创建是从函数NtCreateThread开始的。该函数对用户 传递来的参数进行简单检査,并复制InitialTeb(初始线程环境块)到局部变量CaptureInitialTeb(捕获初始TEB)中;处理完参数 后,交给函数PspCreateThreado该函数是真正创建线程的函数

其原型如下:

NTSTATUS
PspCreateThread( 
OUT	PHANDLE ThreadHandle,                                   //返回线程句柄
IN	ACCESS_MASK DesiredAccess,                              //新线裎的访问权限
IN	POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,           //新线程对象属性
IN	HANDLE ProcessHandle,                                   //线程所属进植句柄
IN	PEPROCESS ProcessPointer,                               //指向所属进程的EPROCESS对象
OUT	PCLIENT_ID Clientld OPTIONAL,                           //返回新线程的CLIENT_ID结构
IN	PCONTEXT Threadcontext OPTIONAL,                        //新线程的执行坏境
IN	PINITIAL_TEB InitialTeb OPTIONAL,                       //新线程TEB
IN	PKSTART_ROUTINE StartRoutine OPTIONAL,                  //系统线权启动函致地址
IN	PVOID StartContext                                      //系统线程启动凼数的执行环境

内核中创建线程的步骤大致如下:
步骤1:根据进程句柄获得进程对象并将其放到局部变量中。
步骤2:调用函数ObCreateObject创建线程对象ETHREAD,并初始化。
步骤3:获取进程的RundownProtect锁,以免线程创建过程中该进程被down掉。(RundownProtect锁:停用保护机制。RundownProtect域是进程的停止保护锁,当一个进程到最后被销毁时,它要等到所有其他进程和线程已经释放了此锁,才可以继续进行,否则就会产生孤儿线程。加锁机制也是windows中进程间或线程间同步的一个很经典的机制,进程只要设置阻塞等待在一个锁对象上,等待拥有那个锁的其他进程或线程释放锁对象,操作系统会发出signal信号(软中断信号),重新激活之前阻塞等待在那个锁上的进程,这样就完成了进程间,线程间的同步。)
步骤4:创建线程环境块(TEB),并用InitTeb函数进行初始化;然后,利用ThreadContext 中的程序指针eip设置线程的启动地址StartAddress字段,并且将ThreadContext的eax寄存器设置到线程的Win32StartAddress字段。完成这些操作后,调用KelnitThread函数初始化新线程的一些属性。
步骤5:锁住进程,并将进程的活动线程数量加1,然后调用函数OblnsertObject把新线程加入到进程的线程链表中。
步骤6:调用函数KeStartThread,新线程即可运行。

用户模式下进程与线程的创建过程

在用户层,创建一个进程通常使用kemel32.dll中的函数CreateProcess来完成。该函数一 旦返回成功,新的进程和进程中的第一个线程就建立起来了。

从用户层的角度看进程创建的大致步骤如下:
步骤1:调用函数CreateProcessW打开指定的可执行映像文件
步骤2:调用ntdll.dll中的存根函数NtCreateProcessEx,该函数利用处理器的陷阱机制切换到内核模式下
步骤3:PEB创建完成后,意味着进程的创建暂时告一段落。这时候还必须为进程创建第一个线程,通过调用函数NtCreateThread构造一个可供第一个线程运行的环境,如堆的大小、初始线程栈的大小等。这些值的默认初始值来自于PE文件头部相应的字段,ntdll.dll中的该函数依旧将任务交由执行层的NtCreateThread来完成。执行体层的线程对象ETHREAD 建立以后,线程的ID、TEB等也就完成了初始化
步骤4:进程的第一个线程的启动函数是kemel32.dll中的BaseProcessStart函数(其他线程则是调用BaseThreadStart函数)。此时的线程是被挂起的,要等到进程完全初始化完成后才真正开始(至此,用户模式下的进程创建实际上才刚刚开始,因为最终的进程要交给Windows子系统来运行,并由子系统维护进程的状态、进程的消息等信息。)
步骤5:kemel32.dll向Windows子系统发送一个消息,该消息包含了刚建立的进程和线程的相关信息:Windows子系统csrss.exe接收到此消息后,开始在子系统内部建立进程环境;最后,以显示应用程序启动光标作为进程环境创建结束。
步骤6:当Windows子系统已经知道并登记了新进程和线程后,先前挂起的初始线程被允许恢复执行。(在内核中,新线程的启动例程是KiThreadStartup。函数KiThreadStartup函数首先将中断请求级别(Interrupt ReQuest Level, IRQL)降低到 APC_LEVEL,然后进入内核模式调用系统初始的线程函数PspUserThreadStartup。函数 PspUserThreadStartup通知缓存管理器预取可执行映像文件的页面,即该进程上一次启动的前10s内引用到的页面。读入后,将一个用户模式APC插入线程的用户APC队列中,此APC 例程指向 ntdll.dll 的 LdrInitializeThunk函数,然后函数 PspUserThreadStartup返回。)
步骤7:当函数KiThreadStartup返回到用户模式时,由PspUserThreadStartup插入的 APC也巳被交付,于是函数Ntdll.LdrinitializeThunk被调用,该函数是PE映像加载器的初始化函数。
完成初始化工作以后,加载PE映像中任何必要的DLL,并调用这些DLL的入口函数。 最后,当LdrInitializeThunk返回到用户模式APC分发器时,该线程开始在用户模式下执行;然 后,它调用用户栈中的线程启动函数。
至此,进程与线程全部建立完成,开始执行用户空间中的代码。

进程环境块PEB

操作系统会为每个进程设置一个数据结构,用来记录进程的相关信息。在NT中,该结构可以从进程空间的FS:[0x30]处找到。PEB描述的信息主要包括:进程状态、进程堆、PE映像信息等,其中有一个很重要的字段Ldr,该字段指向的结构中记录了进程加载进内存的所有模块的基地址,通过Ldr指向的三个链表就可以找到kemel32.dll的基地址。

以下是进程环境块的完整定义:

typedef struct _PEB
{
    UCHAR InheritedAddressSpace;                        // 00h
    UCHAR ReadImageFileExecOptions;                     // 01h
    UCHAR BeingDebugged;                                // 02h 进程是否正在被调试
    UCHAR Spare;                                        // 03h
    PVOID Mutant;                                       // 04h
    PVOID ImageBaseAddress;                             // 08h 进程映像基地址
    PPEB_LDR_DATA Ldr;                                  // 0Ch 加载的其他模块信息
    PRTL_USER_PROCESS_PARAMETERS ProcessParameters;     // 10h
    PVOID SubSystemData;                                // 14h
    PVOID ProcessHeap;                                  // 18h
    PVOID FastPebLock;                                  // 1Ch
    PPEBLOCKROUTINE FastPebLockRoutine;                 // 20h
    PPEBLOCKROUTINE FastPebUnlockRoutine;               // 24h
    ULONG EnvironmentUpdateCount;                       // 28h
    PVOID* KernelCallbackTable;                         // 2Ch
    PVOID EventLogSection;                              // 30h
    PVOID EventLog;                                     // 34h
    PPEB_FREE_BLOCK FreeList;                           // 38h
    ULONG TlsExpansionCounter;                          // 3Ch TLS索引计数
    PVOID TlsBitmap;                                    // 40h TLS位图指针
    ULONG TlsBitmapBits[0x2];                           // 44h TLS进程标志位
    PVOID ReadOnlySharedMemoryBase;                     // 4Ch
    PVOID ReadOnlySharedMemoryHeap;                     // 50h
    PVOID* ReadOnlyStaticServerData;                    // 54h
    PVOID AnsiCodePageData;                             // 58h
    PVOID OemCodePageData;                              // 5Ch
    PVOID UnicodeCaseTableData;                         // 60h
    ULONG NumberOfProcessors;                           // 64h
    ULONG NtGlobalFlag;                                 // 68h 全局标志位
    UCHAR Spare2[0x4];                                  // 6Ch
    LARGE_INTEGER CriticalSectionTimeout;               // 70h
    ULONG HeapSegmentReserve;                           // 78h
    ULONG HeapSegmentCommit;                            // 7Ch
    ULONG HeapDeCommitTotalFreeThreshold;               // 80h
    ULONG HeapDeCommitFreeBlockThreshold;               // 84h
    ULONG NumberOfHeaps;                                // 88h
    ULONG MaximumNumberOfHeaps;                         // 8Ch
    PVOID** ProcessHeaps;                               // 90h
    PVOID GdiSharedHandleTable;                         // 94h
    PVOID ProcessStarterHelper;                         // 98h
    PVOID GdiDCAttributeList;                           // 9Ch
    PVOID LoaderLock;                                   // A0h
    ULONG OSMajorVersion;                               // A4h
    ULONG OSMinorVersion;                               // A8h
    ULONG OSBuildNumber;                                // ACh
    ULONG OSPlatformId;                                 // B0h
    ULONG ImageSubSystem;                               // B4h
    ULONG ImageSubSystemMajorVersion;                   // B8h
    ULONG ImageSubSystemMinorVersion;                   // C0h
    ULONG GdiHandleBuffer[0x22];                        // C4h
    ULONG PostProcessInitRoutine;                      // 14Ch
    ULONG TlsExpansionBitmap;                           // 150h
    BYTE  TlsExpansionBitmapBits[0x80];                 // 154h                 
    ULONG?SessionId;                                    // 1D4h
}PEB, *PPEB;

其中,偏移+0040h处为指向一个RTL_BITMAP数据结构的指针,该数据结构定义如下:

typedef struct _RTL_BITMAP{
   ULONG SizeOfBitMap; // TLS 进程标志位长度 
   PULONG Buffer;	   // TLS进程标志所在缓冲
} RTL_BITMAP, *PRTL_BITMAP;

显然,其目的是要提供一个缓冲区,而真正的缓冲区在Buffer所指的地方。通常情况下, 该值为PEB中的TlsBitmapBits[2],但是有需要时也不排斥采用别的缓冲区,两个32位长的字只能提供64个标志位。

线程环境块TEB

定义:

typedef struct _NT_TEB
{
    NT_TIB Tib;                         // 00h
    PVOID EnvironmentPointer;           // 1Ch
    CLIENT_ID Cid;                      // 20h
    PVOID ActiveRpcInfo;                // 28h
    PVOID ThreadLocalStoragePointer;    // 2Ch        合并后的TLS副本指针 无类型指针数组,40H字节 PVIOD占4个字节
    PPEB Peb;                           // 30h        指向所属进程的PEB
    ULONG LastErrorValue;               // 34h
    ULONG CountOfOwnedCriticalSections; // 38h
    PVOID CsrClientThread;              // 3Ch
    PVOID Win32ThreadInfo;              // 40h
    ULONG Win32ClientInfo[0x1F];        // 44h
    PVOID WOW32Reserved;                // C0h
    ULONG CurrentLocale;                // C4h
    ULONG FpSoftwareStatusRegister;     // C8h
    PVOID SystemReserved1[0x36];        // CCh
    PVOID Spare1;                       // 1A4h
    LONG ExceptionCode;                 // 1A8h
    ULONG SpareBytes1[0x28];            // 1ACh
    PVOID SystemReserved2[0xA];         // 1D4h
    GDI_TEB_BATCH GdiTebBatch;          // 1FCh
    ULONG gdiRgn;                       // 6DCh
    ULONG gdiPen;                       // 6E0h
    ULONG gdiBrush;                     // 6E4h
    CLIENT_ID RealClientId;             // 6E8h
    PVOID GdiCachedProcessHandle;       // 6F0h
    ULONG GdiClientPID;                 // 6F4h
    ULONG GdiClientTID;                 // 6F8h
    PVOID GdiThreadLocaleInfo;          // 6FCh
    PVOID UserReserved[5];              // 700h
    PVOID glDispatchTable[0x118];       // 714h
    ULONG glReserved1[0x1A];            // B74h
    PVOID glReserved2;                  // BDCh
    PVOID glSectionInfo;                // BE0h
    PVOID glSection;                    // BE4h
    PVOID glTable;                      // BE8h
    PVOID glCurrentRC;                  // BECh
    PVOID glContext;                    // BF0h
    NTSTATUS LastStatusValue;           // BF4h
    UNICODE_STRING StaticUnicodeString; // BF8h
    WCHAR StaticUnicodeBuffer[0x105];   // C00h
    PVOID DeallocationStack;            // E0Ch
    PVOID TlsSlots[0x40];               // E10h   线程的TLS存储槽
    LIST_ENTRY TlsLinks;                // F10h
    PVOID Vdm;                          // F18h
    PVOID ReservedForNtRpc;             // F1Ch
    PVOID DbgSsReserved[0x2];           // F20h
    ULONG HardErrorDisabled;            // F28h
    PVOID Instrumentation[0x10];        // F2Ch
    PVOID WinSockData;                  // F6Ch
    ULONG GdiBatchCount;                // F70h
    ULONG Spare2;                       // F74h
    ULONG Spare3;                       // F78h
    ULONG Spare4;                       // F7Ch
    PVOID ReservedForOle;               // F80h
    ULONG WaitingOnLoaderLock;          // F84h
    PVOID StackCommit;                  // F88h
    PVOID StackCommitMax;               // F8Ch
    PVOID StackReserve;                 // F90h
    PVOID MessageQueue;                 // ???
}

字段ThreadLocalStoragePointer用于静态TLS,操作系统会根据静态TLS目录把所有.tls 段的原始副本收集汇总,并复制到所分配的缓冲区中。这样,就为一个线程构建了一个合并后的静态TLS副本,字段ThreadLocalStoragePointer即指向该副本的起始位置。

线程局部储存

线程局部存储很好地解决了多线程程序设计中变量的同步问题。
线程局部存储(Thread Local Storage,TLS)用来将数据与一个正在执行的指定线程关联起来。

TLS技术分为两种:
1.动态线程局部存储技术
2.静态线程局部存储技术

TLS技术的分类主要依据是:线程局部存储的数据所用空间在程序运行期,操作系统完成的是动态申请还是静态分配。动态线程局部存储通过四个Win32 API函数实现对线程局部数据的存储:而静态线程局部存储则通过预先在PE文件中声明数据存储空间,由PE加载器加载该PE进入内存后为其预留存储空间实现。

动态线程局部存储技术

动态TLS存在以下四个API函数。显式TLS变量的申请、取值、赋值和释放
1.TlsAlloc
2.TlsGetValue
3.TlsSetValuc
4.TlsFree
应用程序或DLL在合适的时候会调用这四个函数,通过索引对迸程中每个线程的存储区 进行统一操作。它们位于动态链接库文件kemel32.dll中。

动态TLS

(1)每个进程都有一组正在使用标志,共TLS_MINIMUM_AVAILABLE个。每个标志可以被设为FREE或INUSE,表示该TLS元素是否正在使用。(注意这组标志属进程所有)
(2)当系统创建一个线程的时候,会为该线程分配与线程关联的、属于线程自己的PVOID型数组(共有TLS_MINIMUM_AVAILBALE个元素),数组中的每个PVOID可以保存任意值。

使用动态TLS

(1)调用TlsAlloc函数

​ ①该函数会检索系统进程中的位标志并找到一个FREE标志,然后将该标志从FREE改为INUSE,并返回该标志在位数组中的索引,通常将该索引保存在一个全局变量中,因为这个值会在整个进程范围内(而不是线程范围内)使用。
​ ②如果TlsAlloc无法在列表中找到一个FREE标志,会返回TLS_OUT_OF_INDEXES。
​ ③以上就是TlsAlloc99%的工作,剩1%的工作就是在函数返回之前,会遍历进程中的每个线程,并根据新分配的索引,在每个线程的Tls数组中把对应的素素设为0(具体原因请看后面的分析)。

(2)调用TlsSetValue(dwTlsIndex,pvTlsValue)将一个值放到线程的数组中
①该函数把pvTlsValue所标志的一个PVOID值放到线程的数组中,dwTlsIndex指定了在数组中的具体位置(由TlsAlloc得到)
②当一个线程调用TlsSetValue的时候,会修改自己的数组。但它无法修改另一个线程的TLS数组中的值。

(3)从线程的数组中取回一个值:PVOID TlsGetValue(dwTlsIndex)
①与TlsSetValue相似,TlsGetValue中会查看属于调用线程的数组

(4)释放己经预订的TLS元素:TlsFree(dwTlsIndex)
①该函数会将进程内的位标志数组对应的INUSE标志重设回FREE
②同时该函数还会将所有线程中该元素的内容设为0。
③试图释放一个尚未分配的TLS元素将导致错误

无论我们在多线程设计中将线程设计得多复杂,调用了多少个函数,只要用到每个线程的私有变量,就可以到TLS存储槽中存取。

请添加图片描述

获取索引 TIsAlloc

函数功能:分配一个线程局部存储(TLS)索引。该进程的任何线程都可以使用该索引来 存储和检取线程中的值。
函数原型:

DWORD TIsAlloc(
void
)

参数:无。
返回值:若函数成功,则返回值为一个TLS索引。失败则返回TLS_OUT_OF_INDEX, 其十六进制值为OFFFFFFFFh。

微软保证每个进程最少有64个(常量符号为TLS_MINIMUM_AVAILABLE)索引可供使用;如果程序需要,系统还会为线程提供更多的索引。一旦索引获取成功,该索引对应的进程标志位即被设置为1,表示已被使用。同时,每个线程的该索引对应的双字值也被声明为占用。

如果索引越过进程边界,该索引即视为无效。一个DLL不能假定在一个进程中分配的索引在另一个进程中依然有效。当一个DLL被附加到一个宿主进程时,它使用TIsAlloc分配一个TLS索引。然后,DLL会分配一些动态存储单元,并调用函数TlsSetValue向该索引对应的双字数组相应位置写入分配的动态存储单元地址。TLS索引存储在DLL的全局或静态变量中。

有了这个索引,我们就可以通过它来取得、设置数据。注意,这些数据只对当前线程可见。当函数得到一个可用的索引值后,还会遍历进程中的每个线程,并将对应的TLS存储槽全部清0。所以,函数返回以后,所有的操作就基本完成了。

按索引取值 TIsGetValue

函数功能:检取调用线程的TLS存储槽的值。对于每个TLS索引,进程中的每个线程都有它自己的槽。
函数原型:

LPVOID TIsGetValue( 
DWORD dwTlsIndex //TLS 索引
) 

参数:dwTlsIndex,由TlsAlloc分配的索引。
返回值:若函数成功,则返回调用线程的TLS存储槽中的值;失败则返回0。

注意:由于存放在TLS存储槽中值可以为0,在这种情况下,需要通过函数GetLastError的 返回值来判断。如果为NO_ERROR,则表示取到了值,且值是0;否则表示函数调用失败。

按索引存储 TlsSetValue

函数功能:存储指定值倒调用线程的指定索引处。
函数原型:

BOOL TlsSetValue(
DWORD dwTlsIndex //TLS 索引 
LPVOID lpTlsValue //要设置的值
)

参数:dwTlsIndex,由TlsAlloc分配的索引。
LpTlsValue,存储指定值到线程的TLS存储槽。
返回值:若函数成功,则返回值不为0;失败则返回0。
为了达到提高运行速度的目的,TlsSetValue和TIsGetValue两个函数执行最小的参数验证和错误检查。所以,如果你没有通过函数TlsAlloc得到一个合法的索引值,自行指定的索引在以上两个函数中同样可用。

释放索引TIsFree

函数功能:释放调用线程局部存储(TLS)索引。
函数原型:

BOOL TIsFree( 
DWORD dwTlsIndex //TLS 索引
)

参数:dwTlsIndex,由TlsAlloc分配的索引。
返回值:若函数成功,返回值不为0,失败则返回0。
当数据不再有用时,最好将索引释放。
特别提醒函数TlsFree并不释放任何线程中与TLS相关联的动态存储单元。因为这些存储空间是由线程自己申请的,需要线程自行维护这些存储空间。
TlsFree函数会先检验你交给它的索引值是否的确被配置过。如果是,它将进程标志位对应的索引位设置为0,即声明未使用。然后,TlsFree遍历进程中的毎一个线程,把0放到刚刚被释放的索引所对应的TLS存储槽上。
TLS存储槽实际对应于线程环境块中的TlsSlots字段。

静态线程局部储存

静态线程局部存储预先将变量定义在PE文件内部,一般使用“.tls”节存储,对相关API 的调用由操作系统来完成。这种实现方式使得TLS数据的定义与初始化就像程序中使用普通的静态变量那样。

对静态TLS变量的定义不需要像动态线程局部存储一样,调用相关的 Windows API函数,只需要做如下声明即可:

_declspec (thread) int tlsFlag = 1;

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

为了支持这种编程模式,PE的“.tls”节会包含以下信息:
1.初始化数据
2.用于毎个线程初始化和终止的回调函数
3.TLS索引

可执行代码访问静态TLS数据一般需要经过以下几个步骤:
步骤1:在链接的时候,链接器设置TLS目录中的AddressOflndex字段。这个字段指向一个位置,在这个位置保存程序用到的TLS索引。
微软运行时库为了处理方便定义了一个TLS目录的内存映像,并命名为“_ds_used”(该 名称适应于Intel x86平台)。微软链接器査找这个内存映像,并直接使用其中的数据来创建 PE中的TLS目录。
步骤2:当创建线程时,加载器通过将线程环境块(TEB)的地址放入FS寄存器来传递线程的TLS数组地址。距TEB幵头0x2C的位置处的字段ThreadLocalStoragePointer指向TLS数组。这是特定于Intel x86平台的。
步骤3:加载器将TLS索引值保存到AddressOflndex字段指向的位置处。
步骤4:可执行代码获取TLS索引以及TLS数组的位置。
步骤5:可执行代码将索引乘以4,并将该值作为这个数组内的偏移地址来使用。通过以上方法获取给定程序和模块的TLS数据区的地址。每个线程拥有它自己的TLS数据区,但这对于程序是透明的,它并不需要知道是怎样为箪个线程分配数据的。
步骤6:单个的TLS数据对象都位于TLS数据区的某个固定偏移处,因此可以用这种方 式访问。
说明:TLS数组即系统为每个线程维护的一个地址数组。这个数组中的每个地址即为前面提到的TLS存储槽。它指出了程序中给定模块的TLS数据区的位置:TLS索引指出了是这个数组的哪个元素。

TLS 目录结构IMAGE_TLS_DIRECTORY32

线程局部存储数据以数据结构IMAGE_TLS_DIRECTORY32开始。定义如下:

IMAGE_TLS_DIRECTORY32 STRUCT 
  StartAddressOfRawData  dd   0000h - TLS模板的起始地址
  EndAddressOfRawData    dd   0004h - TLS模板的结束地址
  AddressOfIndex         dd   0008h - TLS索引的位置
  AddressOfCallBacks     dd   000ch - TLS回调函敎数组指针
  SizeOfZeroFill         dd   0010h -填充0的个数
  Characteristics        dd  0014h  -保留 
IMAGE_TLS_DIRECTORY32 ENDS
  1. IMAGE_TLS_DIRECTORY32. StartAddressOfRawData

+0000h,双字。表示TLS模板的起始地址。这个模板是一块数据,用于对TLS数据进行初始化。每当创建线程时,系统都要复制所有这些数据,因此这些数据一定不能出错。

注意:这个地址并不是一个RVA,而是一个VA。所以,在 .reloc 节中应当有一个相应的基址重定位信息是用来说明这个地址。

  1. IMAGE_TLS_DIRECTORY32. EndAddressOfRawData

+0004h,双字。表示TLS模板的结束地址。TLS的最后一个字节的地址,不包括用于填充的0。

  1. IMAGE_TLS_DIRECTORY32. AddressOflndex

+0008h,双字。用于保存TLS索引的位置,索引的具体值由加载器确定。这个位置在普通的数据节中,因此,可以给它取一个容易理解的符号名,便于在程序中访问。

  1. IMAGE_TLS_DIRECTORY32. AddressOfCallBacks

+000Ch,双字。这是一个指针,它指向由TLS回调函数组成的数组。这个数组是以NULL结尾的,因此,如果没有回调函数的话,这个字段指向的位置处应该是4个字节的0。

  1. IMAGE_TLS_DIRECTORY32. SizeOfZeroFill

+0010h,双字。TLS 模板中除了由 StartAddressOfRawData 和 EndAddressOfRawData字段组成的已初始化数据界限之外的大小(以字节计)。TLS模板的大小应该与映像文件中TLS 数据的大小一致。用0填充的数据就是已初始化的非零数据后面的那些数据。

  1. IMAGE_TLS_D I RECTO RY32. Characteristics

+0014h,双字。保留未用。

TLS回调函数

程序可以通过PE文件的方式提供一个或者多个TLS回调函数,用以支持对TLS数据进行附加的初始化和终止操作,该操作类似于面向对象程序设计中的构造函数和析构函数。

TLS回调函数是指,每当创建/终止进程的线程时会自动调用执行的函数(前后共调用两次)。创建进程的主线程时也会自动调用回调函数,且其调用执行先于EP代码。

TLS回调函数的声明:

void NTAPI TLS_CALLBACK(PVOID DllHandle, DWORD Reason, PVOID Reserved)

DllMain的声明:

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)

可以看到两者声明非常相似,第一个参数为模块句柄,即加载地址,第二个参数为调用原因

调用原因有四种

#define DLL_PROCESS_ATTACH 1  启动了一个新进程,包括第一个进程 
#define DLL_THREAD_ATTACH  2  创建了一个新进程,创建所有进程时都会发送这个通知,除第一个进程外
#define DLL_THREAD_DETACH  3  线程将要被终止,终止所有线程时都会发送这个通知,除第一个线程外
#define DLL_PROCESS_ATTACH 0  进程将要被终止,包括第一个线程

TLS机制方便了多线程程序的设计

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值