第6章:线程
# 第6章:线程
线程(Threads)是Windows内核调度以在处理器上执行代码的实体。本章将探讨与线程相关的原生应用程序编程接口(API)。
本章内容包括:
• 创建线程(Creating Threads)
• 线程信息(Thread Information)
• 线程环境块(TEB,Thread Environment Block)
• 异步过程调用(APC,Asynchronous Procedure Calls)
• 线程池(Thread Pools)
• 更多线程应用程序编程接口(More Thread APIs)
# 6.1 创建线程
Windows应用程序编程接口(API)提供了以下用于创建线程的函数:CreateThread、CreateRemoteThread和CreateRemoteThreadEx。原生应用程序编程接口(API)提供了两个函数:RtlCreateUserThread和NtCreateThreadEx。我们先从前者开始介绍(它最终会调用后者)。
# 6.1.1 RtlCreateUserThread
这是两个函数中较简单的一个:
NTSTATUS RtlCreateUserThread(
_In_ HANDLE Process,
_In_opt_ PSECURITY_DESCRIPTOR ThreadSecurityDescriptor,
_In_ BOOLEAN CreateSuspended,
_In_opt_ ULONG ZeroBits,
_In_opt_ SIZE_T MaximumStackSize,
_In_opt_ SIZE_T CommittedStackSize,
_In_ PUSER_THREAD_START_ROUTINE StartAddress,
_In_opt_ PVOID Parameter,
_Out_opt_ PHANDLE Thread,
_Out_opt_ PCLIENT_ID ClientId);
2
3
4
5
6
7
8
9
10
11
该应用程序编程接口(API)与Windows应用程序编程接口(API)的CreateRemoteThread非常相似。各参数说明如下:
• Process——要在其中创建线程的进程句柄(handle)。该句柄必须具有PROCESS_CREATE_THREAD访问掩码(access mask)位。如果该句柄设为NtCurrentProcess(),则线程会在调用者的进程中创建。请注意,此参数不能设为NULL。
• ThreadSecurityDescriptor——可选的安全描述符(security descriptor),用于应用到新创建的线程。
• CreateSuspended——布尔(Boolean)标志,若设为TRUE,则线程创建时处于挂起状态。这种情况下,应在适当的时候调用NtResumeThread以启动线程。
• ZeroBits——通常设为0。这让内核可以自由选择线程堆栈(stack)的分配位置。
• MaximumStackSize和CommittedStack——分别是最大保留堆栈大小和初始提交堆栈大小(以字节为单位),必要时会向上舍入到页边界(page boundary)。如果设为0,则使用可移植可执行文件(PE,Portable Executable)头中的默认值。请注意,Windows应用程序编程接口(API)仅接受初始提交大小或最大保留大小中的一个,而非两者同时接受。
• StartAddress——线程的起始地址,其原型与Windows应用程序编程接口(API)函数要求的基本相同:
typedef NTSTATUS (__stdcall *PUSER_THREAD_START_ROUTINE)(_In_ PVOID ThreadParameter);
• Parameter——传递给线程函数的参数值。
• Thread——可选的句柄指针,调用成功后会存储生成的句柄。应始终获取该句柄,以便(至少)能够关闭它。
• ClientId——可选的CLIENT_ID指针,会返回新线程的唯一标识符(ID)及其所属的进程标识符(ID)。
以下示例在当前进程中创建一个线程:
NTSTATUS __stdcall DoWork(PVOID param)
{
// . . .
}
void SomeFunction()
{
HANDLE hThread;
status = RtlCreateUserThread(NtCurrentProcess(), nullptr ,
FALSE, // 不挂起(not suspended)
0 , 128 << 10 , // 最大大小:128 KB(maximum size: 128 KB)
16 << 10 , // 初始大小:16 KB(initial size: 16 KB)
DoWork, nullptr , // 函数和参数(function and argument)
&hThread, nullptr);
// . . .
NtClose(hThread);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 6.1.2 NtCreateThreadEx
NtCreateThreadEx比RtlCreateUserThread提供了更高的灵活性:
NTSTATUS NtCreateThreadEx(
_Out_ PHANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ HANDLE ProcessHandle,
_In_ PVOID StartRoutine, // PUSER_THREAD_START_ROUTINE
_In_opt_ PVOID Argument,
_In_ ULONG CreateFlags, // THREAD_CREATE_FLAGS_*
_In_ SIZE_T ZeroBits,
_In_ SIZE_T StackSize,
_In_ SIZE_T MaximumStackSize,
_In_opt_ PPS_ATTRIBUTE_LIST AttributeList);
2
3
4
5
6
7
8
9
10
11
12
ThreadHandle——调用成功后返回的线程句柄。DesiredAccess指定返回句柄所需的访问掩码,典型值为THREAD_ALL_ACCESS(Windows应用程序编程接口(API)始终如此)。ObjectAttributes是指向标准OBJECT_ATTRIBUTES的可选指针,通常设为NULL。
ProcessHandle、StartRoutine、Argument和ZeroBits的作用与RtlCreateUserThread中相同。StackSize是初始提交堆栈大小,MaximumStackSize是最大保留堆栈大小。
CreateFlags是一组可选标志,可指定如下:
• THREAD_CREATE_FLAGS_SUSPENDED(1)——若设置,线程创建时处于挂起状态。
• THREAD_CREATE_FLAGS_SKIP_ATTACH(2)——若设置,不会为进程中的动态链接库(DLL)调用DllMain(原因是THREAD_DLL_ATTACH或THREAD_DLL_DETACH)。
• THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER(4)——若设置,调试器(debuggers)不会显示该线程。
• THREAD_CREATE_FLAGS_LOADER_WORKER(0x10)——若线程是加载程序工作线程(loader worker thread),则设置此标志。
• THREAD_CREATE_FLAGS_SKIP_LOADER_INIT(0x20)——若设置,将完全跳过加载程序初始化(loader initialization)。
• THREAD_CREATE_FLAGS_BYPASS_PROCESS_FREEZE(0x40)——若设置,当进程被冻结时(例如通过PsSuspendProcess或通用Windows平台(UWP,Universal Windows Platform)进程被挂起时),线程不会被挂起。
请注意,phnt对上述部分常量的定义有所不同。
最后,AttributeList是应用于所创建线程的可选属性列表。你可以通过查看Windows应用程序编程接口(API)UpdateProcThreadAttribute的文档了解更多可能的属性,尽管原生应用程序编程接口(API)使用不同的方式构建属性列表。
相关结构如下:
typedef struct _PS_ATTRIBUTE
{
ULONG_PTR Attribute;
SIZE_T Size;
union
{
ULONG_PTR Value;
PVOID ValuePtr;
};
PSIZE_T ReturnLength;
} PS_ATTRIBUTE, *PPS_ATTRIBUTE;
typedef struct _PS_ATTRIBUTE_LIST
{
SIZE_T TotalLength;
PS_ATTRIBUTE Attributes[1];
} PS_ATTRIBUTE_LIST, *PPS_ATTRIBUTE_LIST;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
属性列表结构仅能容纳一个属性,但可以根据实际需要的属性数量动态分配,或者通过引入联合体(union)进行静态分配。
以下示例使用组关联(group affinity)属性为线程设置特定的组关联:
GROUP_AFFINITY affinity{}; // 将保留成员(Reserved members)置零(zero out)
affinity.Group = 1; // 组1(group 1)
affinity .Mask = 0; // 组中的所有处理器(all processors in the group)
PS_ATTRIBUTE_LIST attributes;
attributes .TotalLength = sizeof(attributes);
auto& attribute = attributes.Attributes[0];
attribute .Attribute = PS_ATTRIBUTE_GROUP_AFFINITY;
attribute .Size = sizeof(affinity);
attribute .ValuePtr = &affinity;
attribute .ReturnLength = 0;
status = NtCreateThreadEx( . . . , &attributes);
2
3
4
5
6
7
8
9
10
11
12
13
以下示例使用两个属性,展示了静态分配所需结构的一种方法:
union
{
PS_ATTRIBUTE_LIST List;
//
// 确保分配足够大的堆栈(force big enough stack allocation)
//
BYTE Buffer[FIELD_OFFSET(PS_ATTRIBUTE_LIST, Attributes)
+ 2 * sizeof(PS_ATTRIBUTE)];
} attributes;
GROUP_AFFINITY affinity{};
affinity.Group = 0;
affinity.Mask = 0;
{
//
// 第一个属性(first attribute)
//
auto& attribute = attributes.List.Attributes[0];
attribute.Attribute = PS_ATTRIBUTE_GROUP_AFFINITY;
attribute.Size = sizeof(affinity);
attribute.ValuePtr = &affinity;
attribute.ReturnLength = 0;
}
PROCESSOR_NUMBER ideal{};
ideal.Group = 0;
ideal.Number = 5; // 将CPU 5设为理想CPU(set CPU 5 as the ideal CPU)
{
//
// 第二个属性(second attribute)
//
auto& attribute = attributes.List.Attributes[1];
attribute .Attribute = PS_ATTRIBUTE_IDEAL_PROCESSOR;
attribute .Size = sizeof(ideal);
attribute .ValuePtr = &ideal;
attribute .ReturnLength = 0;
}
attributes.List.TotalLength = sizeof(attributes.Buffer);
status = NtCreateThreadEx(..., &attributes.List);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 6.2 线程信息
与进程(以及其他对象类型)类似,有两个主要函数用于获取和设置线程信息:
NTSTATUS NtQueryInformationThread(
_In_ HANDLE ThreadHandle,
_In_ THREADINFOCLASS ThreadInformationClass,
_Out_writes_bytes_ (ThreadInformationLength) PVOID ThreadInformation,
_In_ ULONG ThreadInformationLength,
_Out_opt_ PULONG ReturnLength);
NTSTATUS NtSetInformationThread(
_In_ HANDLE ThreadHandle,
_In_ THREADINFOCLASS ThreadInformationClass,
_In_reads_bytes_(ThreadInformationLength) PVOID ThreadInformation,
_In_ ULONG ThreadInformationLength);
2
3
4
5
6
7
8
9
10
11
12
THREADINFOCLASS是一个枚举,用于指定要设置或查询的信息类型。本节中,我们将介绍其中一些信息类。除非另有说明,用于查询的线程句柄必须具有THREAD_QUERY_LIMITED_INFORMATION或THREAD_QUERY_INFORMATION访问掩码,用于设置的线程句柄必须具有THREAD_SET_LIMITED_INFORMATION或THREAD_SET_INFORMATION访问掩码。
# 6.2.1 ThreadBasicInformation
该值提供线程的一些基本细节:
typedef struct _THREAD_BASIC_INFORMATION
{
NTSTATUS ExitStatus;
PTEB TebBaseAddress;
CLIENT_ID ClientId;
ULONG_PTR AffinityMask;
KPRIORITY Priority;
LONG BasePriority;
} THREAD_BASIC_INFORMATION, *PTHREAD_BASIC_INFORMATION;
2
3
4
5
6
7
8
9
各成员说明如下:
• ExitStatus——线程的退出代码(如果已终止)。否则,返回值为STATUS_PENDING(0x103)。Windows应用程序编程接口(API)将其定义为STILL_ACTIVE。
• TebBaseAddress——线程环境块(TEB,Thread Environment Block)的地址。下一节将详细介绍线程环境块(TEB)。
• ClientId——进程标识符(ID)和线程标识符(ID)。
• AffinityMask——线程可使用的处理器掩码,其中“1”位表示有效的处理器。通常,它设为当前处理器组中所有可用处理器的范围。
• Priority——线程的当前优先级(0到31)。
• BasePriority——线程的基本优先级,由于优先级提升(priority boosting),它可能暂时低于当前优先级(Priority)。
# 6.2.2 ThreadTimes
此信息类返回线程的创建时间、退出时间、内核模式(kernel-mode)执行时间和用户模式(user-mode)执行时间。所使用的结构KERNEL_USER_TIMES与第5章中NtQueryInformationProcess(指定ProcessTimes)所使用的结构相同。Windows应用程序编程接口(API)的GetThreadTimes提供相同的信息。
# 6.2.3 线程CPU优先级(Thread CPU Priorities)
多个信息类与线程CPU优先级相关:
• ThreadPriority(2)——设置当前线程优先级。如果优先级处于实时范围(16-31),则需要SeIncreaseBasePriorityPrivilege权限。
• ThreadBasePriority(3)——设置线程的基本优先级,内核和内核驱动程序可以在此基础上进行优先级提升。客户端/服务器运行时子系统(Csrss.exe,Client/Server Runtime Subsystem)进程和以实时(Realtime)优先级类运行的进程可以不受限制地更改到任何优先级。
• ThreadPriorityBoost(14)——允许设置或查询是否应对线程应用优先级提升(使用ULONG类型,0表示禁用,1表示启用)。默认情况下,优先级提升是启用的。请注意,在实时范围(16-31)内永远不会应用优先级提升。
• ThreadActualBasePriority(25)——允许类似于ThreadBasePriority设置当前线程优先级,但同时会将当前优先级更改为相同的值。该信息类也支持查询操作。
如果启用了优先级提升,以下情况(非详尽列表)会触发优先级提升:
• 线程在事件(event)或信号量(semaphore)上的等待满足后被释放时,内核会给予+1的优先级提升。设备驱动程序在调用KeSetEvent或KeReleaseSemaphore时可以指定自己的提升值。
• 接收消息的图形用户界面(GUI,Graphical User Interface)线程会获得+2的优先级提升。
• 前台进程(其某个窗口处于焦点)中的线程也会获得+2的优先级提升。优先级提升是累积的,因此此类进程中的图形用户界面(GUI)线程可能会获得+4的优先级提升。
• 饥饿线程(处于就绪状态超过4秒)可能会被提升到优先级15,持续一个时间片(quantum)。
除了最后一种提升(运行一个时间片后会降至线程的基本优先级)外,其他提升会在线程每次执行一个时间片后将其优先级降低1,直到优先级达到线程的基本优先级。有关线程优先级和调度的更多信息,请参阅《Windows Internals part 1》一书的第4章。
# 6.2.4 ThreadSystemThreadInformation
此信息类返回SYSTEM_THREAD_INFORMATION结构,该结构我们在第4章中已经介绍过。
# 6.2.5 ThreadNameInformation
此信息类允许设置或获取线程的描述(适用于Windows 10及更高版本)。该名称仅作为描述,没有实际作用,但可用于在调试器中轻松识别感兴趣的线程。Visual Studio的调试器支持设置和查询此值。
预期的结构只是对UNICODE_STRING的封装:
typedef struct _THREAD_NAME_INFORMATION
{
UNICODE_STRING ThreadName;
} THREAD_NAME_INFORMATION, *PTHREAD_NAME_INFORMATION;
2
3
4
此功能也可通过Windows API函数
SetThreadDescription和GetThreadDescription实现。
# 6.2.6 ThreadIoPriority
此信息类用于查询或设置线程的I/O优先级。提供或查询的值来自IO_PRIORITY_HINT枚举:
typedef enum _IO_PRIORITY_HINT
{
IoPriorityVeryLow = 0 , // 磁盘碎片整理(Defragging)、内容索引(content indexing)和其他后台I/O(background I/O)
IoPriorityLow, // 应用程序预取(Prefetching for applications)
IoPriorityNormal, // 正常I/O(默认值,Normal I/O (default))
IoPriorityHigh, // 文件系统用于检查点I/O(Used by filesystems for checkpoint I/O)
IoPriorityCritical, // 内存管理器使用(Used by memory manager)。应用程序不可用(Not available for applications)
MaxIoPriorityTypes
} IO_PRIORITY_HINT;
2
3
4
5
6
7
8
9
对于查询操作,返回的正是上述枚举值。对于设置操作,可以指定一个扩展结构:
typedef struct _IO_PRIORITY_HINT_INFORMATION_EX
{
IO_PRIORITY_HINT PriorityHint;
BOOLEAN BoostOutstanding;
} _IO_PRIORITY_HINT_INFORMATION_EX;
2
3
4
5
如果指定了该结构,BoostOutstanding表示是否应立即提升此线程启动的现有I/O操作(如果新优先级高于当前优先级)。
# 6.2.7 线程挂起计数
该信息类返回线程的挂起计数(suspend count)。值为0表示线程未被挂起,任何正值均表示线程已被挂起。可使用线程挂起函数(NtSuspendThread)和线程恢复函数(NtResumeThread)来挂起/恢复线程。如果通过多次调用线程挂起函数(NtSuspendThread)挂起线程,则必须调用相同次数的线程恢复函数(NtResumeThread)才能恢复该线程。相关API定义如下:
NTSTATUS NtSuspendThread(
_In_ HANDLE ThreadHandle,
_Out_opt_ PULONG PreviousSuspendCount);
NTSTATUS NtResumeThread(
_In_ HANDLE ThreadHandle,
_Out_opt_ PULONG PreviousSuspendCount);
2
3
4
5
6
用于挂起线程的句柄需要具备线程挂起/恢复访问权限(THREAD_SUSPEND_RESUME)。
# 6.3 同步
线程有时需要通过等待各种内核调度程序(“可等待”)对象(kernel dispatcher (“waitable”) objects)来协调工作,这些对象会维持已触发(signaled)或未触发(non-signaled)状态。线程可通过以下基础API等待一个或多个对象:
NTSTATUS NtWaitForSingleObject(
_In_ HANDLE Handle,
_In_ BOOLEAN Alertable,
_In_opt_ PLARGE_INTEGER Timeout);
NTSTATUS NtWaitForMultipleObjects(
_In_ ULONG Count,
_In_reads_(Count) HANDLE Handles[],
_In_ WAIT_TYPE WaitType,
_In_ BOOLEAN Alertable,
_In_opt_ PLARGE_INTEGER Timeout);
2
3
4
5
6
7
8
9
10
Windows API 中对应的(近似)函数是等待单个对象函数(WaitForSingleObject(Ex))和等待多个对象函数(WaitForMultipleObjects(Ex))。
调度程序对象类型示例包括:进程(process)、线程(thread)、互斥体(mutex)、事件(event)、信号量(semaphore)、作业(job)、文件(file)。第7章将详细介绍用于操作各类对象的API。
这两个函数均要求传入超时时间(Timeout)参数,用于指定最长等待时间。指定为NULL表示无限期等待(直到等待条件满足)。负值表示相对时间(以常见的100纳秒为单位),正值表示绝对时间(以1601年1月1日午夜(世界标准时间,UT)为起点,同样以100纳秒为单位)。
传入的句柄指向要等待的对象。线程等待单个对象函数(NtWaitForSingleObject)用于等待单个对象,而线程等待多个对象函数(NtWaitForMultipleObjects)最多可等待64个对象。等待类型(WaitType)指定等待成功的条件:是所有对象都变为已触发状态(WaitAll,等待所有),还是只需任意一个对象变为已触发状态(WaitAny,等待任意一个)。
可警告(Alertable)标志指定等待是否可被警告。如果该标志为TRUE,那么在等待期间若有异步过程调用(APCs)排队到该线程,这些调用会执行,且等待会被满足。有关异步过程调用(APCs)的更多信息,请参见本章后续的“异步过程调用”小节。
单个对象等待和多个对象等待的常见返回值如下:
- 状态超时(STATUS_TIMEOUT):超时时间已到,但对象未变为已触发状态。如果超时时间(Timeout)为NULL(无限期等待),则不会返回此值。
- 状态成功(STATUS_SUCCESS):在超时时间到期前,等待的对象已变为已触发状态。此值适用于单个对象等待和“等待所有”(WaitAll)类型的多个对象等待。
- 状态用户异步过程调用(STATUS_USER_APC):由于异步过程调用(APCs)执行,可警告等待(alertable wait)被满足。
对于单个对象等待,如果等待因互斥体(mutex)被放弃而满足,则可能返回状态放弃(STATUS_ABANDONED)。被放弃的互斥体是指线程在终止前未释放的互斥体。内核会强制释放该互斥体(使其变为已触发状态),但下一个成功获取该互斥体的线程会收到状态放弃(STATUS_ABANDONED),以指示该互斥体的前一个拥有者线程已提前终止。
对于“等待任意一个”(WaitAny)类型的多个对象等待,返回值在状态等待_0(STATUS_WAIT_0,值为0)到状态等待_63(STATUS_WAIT_63,值为63)范围内时,表示变为已触发状态的对象的索引。在极少数情况下,如果变为已触发状态的对象中包含被放弃的互斥体,则返回值在状态放弃(STATUS_ABANDONED,值为0x80)到状态放弃+63(STATUS_ABANDONED + 63)范围内。
有关调度程序对象的更多信息,请参见第7章。
# 6.4 线程环境块(Thread Environment Block,TEB)
每个用户模式线程都有一个用户模式线程环境块(TEB)结构,用于存储该线程的相关重要信息。与进程环境块(PEB)类似,线程环境块(TEB)结构较大,此处不再完整列出。可查看phnt中的ntpebteb.h文件。本节将介绍该结构的部分成员。
在Wow64进程中,一个线程拥有两个线程环境块(TEB):一个64位和一个32位。线程环境块(TEB)有两个变体——32位线程环境块(TEB32)和64位线程环境块(TEB64),两者的区别主要在于指针大小以及部分32位/64位相关的大小差异。
线程环境块(TEB)的第一个成员名为NtTib,类型为线程信息块(TIB)。该结构较小,完整定义如下:
typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD *Next;
PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;
typedef struct _NT_TIB {
struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList;
PVOID StackBase;
PVOID StackLimit;
PVOID SubSystemTib;
union {
PVOID FiberData;
DWORD Version;
};
PVOID ArbitraryUserPointer;
struct _NT_TIB *Self;
} NT_TIB;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
异常列表(ExceptionList)是该线程创建的异常列表(仅在x86架构上实现)。堆栈基址(StackBase)和堆栈限制(StackLimit)是该线程当前使用的堆栈地址范围。子系统线程信息块(SubSystemTib)似乎未被使用。如果线程已转换为纤程(fiber),则纤程数据(FiberData)会存储相关信息。在大多数情况下,版本(Version)成员是“实际使用”的成员,其值似乎为0x1e00(对应OS/2 Windows支持的最新版本)。任意用户指针(ArbitraryUserPointer)看似可用于存储线程特定数据,但实际上开发者不应使用该指针——部分场景已在使用它,线程本地存储(TLS)可用于存储线程特定数据。最后,自身(Self)指针指向线程信息块(NT_TIB)的起始位置,该位置也是线程环境块(TEB)的起始位置。
接下来介绍线程环境块(TEB)的其他成员:
- 环境指针(EnvironmentPointer):似乎始终为NULL。
- 客户端ID(ClientId):客户端ID(CLIENT_ID)结构,存储唯一的进程ID和线程ID。
- 活动远程过程调用句柄(ActiveRpcHandle):似乎未被使用,始终为NULL。
- 进程环境块指针(ProcessEnvironmentBlock):指向进程环境块(PEB)(第5章已讨论)。
- 最后错误值(LastErrorValue):Windows API调用返回的最后一个错误值,通常通过调用获取最后错误函数(GetLastError)读取,可通过Windows API中的设置最后错误函数(SetLastError)设置该值。
- 拥有的临界区计数(CountOfOwnedCriticalSections):通常为0。在Windows的调试版本(Debug build)中,用于跟踪该线程拥有的临界区数量。
- 客户端/服务器运行时子系统客户端线程(CsrClientThread):在除客户端/服务器运行时子系统进程(Csrss.exe)之外的所有进程中均为NULL;在客户端/服务器运行时子系统进程(Csrss.exe)中,可能指向客户端/服务器运行时子系统线程(CSR_THREAD)结构,该结构由客户端/服务器运行时子系统进程(csrss.exe)为已向其注册的线程维护。该结构定义如下:
typedef struct _CSR_THREAD {
LARGE_INTEGER CreateTime;
LIST_ENTRY Link;
LIST_ENTRY HashLinks;
CLIENT_ID ClientId;
struct _CSR_PROCESS *Process;
HANDLE ThreadHandle;
ULONG Flags;
LONG ReferenceCount;
ULONG ImpersonateCount;
} CSR_THREAD;
2
3
4
5
6
7
8
9
10
11
该结构指向以下定义的客户端/服务器运行时子系统进程(CSR_PROCESS),而客户端/服务器运行时子系统进程(CSR_PROCESS)又指向客户端/服务器运行时子系统NT会话(CSR_NT_SESSION):
typedef struct _CSR_PROCESS {
CLIENT_ID ClientId;
LIST_ENTRY ListLink;
LIST_ENTRY ThreadList;
PCSR_NT_SESSION NtSession;
HANDLE ClientPort;
PCH ClientViewBase;
PCH ClientViewBounds;
HANDLE ProcessHandle;
ULONG SequenceNumber;
ULONG Flags;
ULONG DebugFlags;
LONG ReferenceCount;
ULONG ProcessGroupId;
ULONG ProcessGroupSequence;
ULONG LastMessageSequence;
ULONG NumOutstandingMessages;
ULONG ShutdownLevel;
ULONG ShutdownFlags;
LUID Luid;
PVOID ServerDllPerProcessData[ANYSIZE_ARRAY];
} CSR_PROCESS;
typedef struct _CSR_NT_SESSION {
LIST_ENTRY SessionLink;
ULONG SessionId;
LONG ReferenceCount;
STRING RootDirectory;
} CSR_NT_SESSION;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
可将客户端/服务器运行时子系统进程(Csrss.exe)加载到WinDbg中,使用标准的dt命令查看这些数据结构。
回到线程环境块(TEB)的更多字段:
- 当前区域设置(CurrentLocale):该线程使用的当前区域设置。例如,0x409表示“英语-美国(en-US)”,等同于宏
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US)。 - 异常代码(ExceptionCode):存储该线程上发生的最后一个异常代码。
- 最后状态值(LastStatusValue):存储该线程上系统调用返回的最后一个状态值。
- 子进程标记(SubProcessTag):处理服务的线程的该字段非零。服务标记信息由服务控制管理器(Service Control Manager,SCM,运行在
services.exe中)维护。可通过AdvApi32.dll导出的查询标记信息函数(I_QueryTagInformation)获取该标记与服务名称的映射关系。以下是该函数的定义以及相关的结构和枚举:
typedef enum _TAG_INFO_LEVEL {
eTagInfoLevelNameFromTag = 1, // TAG_INFO_NAME_FROM_TAG
eTagInfoLevelNamesReferencingModule, // TAG_INFO_NAMES_REFERENCING_MODULE
eTagInfoLevelNameTagMapping, // TAG_INFO_NAME_TAG_MAPPING
eTagInfoLevelMax
} TAG_INFO_LEVEL;
typedef enum _TAG_TYPE {
eTagTypeService = 1,
eTagTypeMax
} TAG_TYPE;
typedef struct _TAG_INFO_NAME_FROM_TAG_IN_PARAMS {
DWORD dwPid;
DWORD dwTag;
} TAG_INFO_NAME_FROM_TAG_IN_PARAMS, *PTAG_INFO_NAME_FROM_TAG_IN_PARAMS;
typedef struct _TAG_INFO_NAME_FROM_TAG_OUT_PARAMS {
DWORD eTagType;
LPWSTR pszName;
} TAG_INFO_NAME_FROM_TAG_OUT_PARAMS, *PTAG_INFO_NAME_FROM_TAG_OUT_PARAMS;
DWORD I_QueryTagInformation(
_In_opt_ LPCWSTR pszMachineName,
_In_ TAG_INFO_LEVEL eInfoLevel,
_Inout_ PVOID pTagInfo);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
可通过以下代码根据子进程标记(SubProcessTag)获取服务名称:
std::wstring GetServiceNameByTag(DWORD pid, uint32_t tag) {
static auto QueryTagInformation = (PQUERY_TAG_INFORMATION)GetProcAddress(
GetModuleHandle(L"advapi32"), "I_QueryTagInformation");
TAG_INFO_NAME_FROM_TAG info = { 0 };
info.InParams.dwPid = pid;
info.InParams.dwTag = tag;
auto err = QueryTagInformation(nullptr, eTagInfoLevelNameFromTag, &info);
if (err)
return L"";
return info.OutParams.pszName;
}
2
3
4
5
6
7
8
9
10
11
12
读取子进程标记(SubProcessTag)本身可通过获取线程环境块(TEB)地址,然后读取进程内存实现。例如:
uint32_t GetSubProcessTag(HANDLE hThread) {
THREAD_BASIC_INFORMATION tbi;
auto status = NtQueryInformationThread(hThread,
ThreadBasicInformation, &tbi, sizeof(tbi), nullptr);
if (!NT_SUCCESS(status))
return 0;
if (tbi.TebBaseAddress == 0)
return 0;
auto pid = GetProcessIdOfThread(hThread);
auto hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ,
FALSE, pid);
if (!hProcess)
return 0;
uint32_t tag = 0;
BOOL isWow = FALSE;
//
// 假设是64位操作系统
//
IsWow64Process(hProcess, &isWow);
if (isWow) {
auto teb = (TEB32*)tbi.TebBaseAddress;
ReadProcessMemory(process->GetHandle(),
(BYTE*)teb + offsetof(TEB32, SubProcessTag),
&tag, sizeof(ULONG), nullptr);
}
else {
auto teb = (TEB*)tbi.TebBaseAddress;
ReadProcessMemory(process->GetHandle(),
(BYTE*)teb + offsetof(TEB, SubProcessTag),
&tag, sizeof(tag), nullptr);
}
return tag;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
- 线程本地存储槽(TlsSlots):该线程使用的线程本地存储(TLS)数组。Windows API中的线程本地存储设置值函数(TlsSetValue)和线程本地存储获取值函数(TlsGetValue)通过该数组存储/查询线程本地存储(TLS)值。
- OLE保留字段(ReservedForOle):存储指向数据结构的指针,该结构用于管理该线程的组件对象模型(Component Object Model,COM)状态。如果线程未调用组件对象模型初始化函数(CoInitialize(Ex)),则该指针为NULL。该结构类型为OLE线程数据(SOleThreadData):
typedef enum {
OLE_LOCALTID = 0x01, // 线程ID(TID)在当前进程中
OLE_UUIDINITIALIZED = 0x02,
OLE_INTHREADDETACH = 0x04,
OLE_CHANNELTHREADINITIALZED = 0x08,
OLE_WOWTHREAD = 0x10,
OLE_THREADUNINITIALIZING = 0x20, // 线程处于组件对象模型反初始化(CoUninitialize)过程中
OLE_DISABLE_OLE1DDE = 0x40, // 线程无法使用动态数据交换(DDE)
OLE_APARTMENTTHREADED = 0x80, // 单线程单元(STA)线程
OLE_MULTITHREADED = 0x100, // 多线程单元(MTA)线程
OLE_IMPERSONATING = 0x200, // 正在模拟
OLE_DISABLE_EVENTLOGGER = 0x400,
OLE_INNEUTRALAPT = 0x800, // 线程处于中性单元(NTA)
OLE_DISPATCHTHREAD = 0x1000,
OLE_HOSTTHREAD = 0x2000, // 宿主单线程单元(STA)
OLE_ALLOWCOINIT = 0x4000,
OLE_PENDINGUNINIT = 0x8000,
OLE_FIRSTMTAINIT = 0x10000,
OLE_FIRSTNTAINIT = 0x20000,
OLE_APTINITIALIZING = 0x40000,
OLE_UIMSGSINMODALLOOP = 0x80000, // 线程在模态循环中有消息
OLE_MARSHALING_ERROR_OBJECT = 0x100000,
OLE_WINRT_INITIALIZE = 0x200000,
OLE_APPLICATION_STA = 0x400000, // 应用程序单线程单元(ASTA)
OLE_IN_SHUTDOWN_CALLBACKS = 0x800000,
OLE_POINTER_INPUT_BLOCKED = 0x1000000,
OLE_IN_ACTIVATION_FILTER = 0x2000000,
OLE_ASTATOASTAEXEMPT_QUIRK = 0x4000000,
OLE_ASTATOASTAEXEMPT_PROXY = 0x8000000,
OLE_ASTATOASTAEXEMPT_INDOUBT = 0x10000000,
OLE_DETECTED_USER_INITIALIZED = 0x20000000,
OLE_BRIDGE_STA = 0x40000000,
OLE_NAINITIALIZING = 0x80000000,
} OleTlsFlags;
typedef struct tagSOleTlsData {
PVOID pvThreadBase;
CSmAllocator* pSmAllocator;
DWORD dwApartmentID;
OleTlsFlags dwFlags;
LONG TlsMapIndex;
void* ppTlsSlot;
DWORD cComInits;
DWORD cOleInits;
DWORD cCalls;
ServerCall* pServerCall;
ThreadCallObjectCache* pCallObjectCache;
ContextStackNode* pContextStack;
CObjServer* pObjServer;
DWORD dwTIDCaller;
PVOID pCurrentCtxForNefariousReaders;
CObjectContext* pCurrentContext; // 当前上下文
CObjectContext* pEmptyCtx;
ULONGLONG ContextId; // 唯一标识当前上下文
CComApartment* pNativeApt; // 原生单元
IUnknown* pCallContext; // 调用上下文
CCtxCall* pCtxCall;
CPolicySet* pPS;
PVOID pvPendingCallsFront;
PVOID pvPendingCallsBack;
CAptCallCtrl* pCallCtrl;
CSrvCallState* pTopSCS;
HWND hwndSTA; // 单线程单元(STA)窗口
LONG cORPCNestingLevel;
DWORD cDebugData;
UUID LogicalThreadId; // 逻辑线程ID
HANDLE hThread; // 用于取消操作
HANDLE hRevert; // 第一次模拟前的令牌
IUnknown pAsyncRelease;
HWND hwndDdeServer;
HWND hwndDdeClient;
ULONG cServeDdeObjects;
PVOID pSTALSvrsFront;
HWND hwndClip; // 剪贴板窗口
IDataObject* pDataObjClip; // 当前剪贴板数据对象(DataObject)
DWORD dwClipSeqNum;
DWORD fIsClipWrapper;
IUnknown* punkState;
DWORD cCallCancellation;
DWORD cAsyncSends;
CAsyncCall* pAsyncCallList;
CSurrogatedObjectList* pSurrogateList;
PRWLOCKTLSEntry pRWLockTlsEntry;
CallEntryBuffer CallEntry;
InitializeSpyNode* pFirstSpyReg;
InitializeSpyNode* pFirstFreeSpyReg;
CVerifierTlsData* pVerifierData;
DWORD dwMaxSpy;
BYTE cCustomMarshallerRecursion;
PVOID pDragCursors;
IUnknown* punkError;
ULONG cbErrorData;
OutgoingCallData outgoingCallData;
IncomingCallData incomingCallData;
OutgoingActivationData outgoingActivationData;
ULONG cReentrancyFromUserAPC;
ModernSTAWaitContext* pModernSTAWaitContext;
DWORD dwCrossThreadFlags;
DWORD dwNestedRemRelease;
ULONG cIncomingTouchedASTACalls;
PushLogicalThreadId* pTopPushedLogicalThreadId;
ULONG iXslockOwnerTableHint;
OLETLS_PREVENT_RUNDOWN_MITIGATION currentPreventRundownMitigation;
BOOL fOweForcedBulkUpdateForCurrentMitigation;
IUnknown* pClipboardBroker;
DWORD dwActivationType;
ULONG cTouchedAstasInActiveCall;
OXID* pTouchedAstasInActiveCall;
UnmarshalForQueryInterface* pTopmostUnmarshalForQueryInterface;
CoGetStandardMarshalInProgress* pTopmostCoGetStandardMarshalInProgress;
WireContainerThis* requestContainerPassthroughData;
ULONG requestContainerPassthroughDataSize;
BOOL freeRequestContainerPassthroughData;
WireContainerThat* responseContainerPassthroughData;
ULONG responseContainerPassthroughDataSize;
ComTlsFlags comTlsFlags;
ThreadLockOrderVerifier<ComLockOrder> lockOrderVerifier;
} SOleTlsData;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
可将组件对象模型基础库(ComBase.Dll)加载到WinDbg中,使用dt命令查看该结构(及其引用的其他结构)的定义。
# 6.5 异步过程调用(APC,Asynchronous Procedure Call)
异步过程调用(APC)允许将回调函数(callback)附加到某个线程,使得只有该线程能够运行该回调函数。要实际执行回调函数,线程必须处于可警告等待状态(有时称为“警报状态”)。
用于在可警告状态下等待的原生 API(native API)是同步(Synchronization)部分中讨论的 NtWaitForSingleObject 和 NtWaitForMultipleObjects,其中 Alertable 参数需设为 TRUE。此外,NtDelayExecution 提供了一种在休眠期间的可警告等待机制:
NTSTATUS NtDelayExecution(
_In_ BOOLEAN Alertable, // 可警告?
_In_opt_ PLARGE_INTEGER DelayInterval);
2
3
可以通过 NtQueueApcThread 函数将 APC 排入线程队列:
typedef VOID (*PPS_APC_ROUTINE)(
_In_opt_ PVOID ApcArgument1,
_In_opt_ PVOID ApcArgument2,
_In_opt_ PVOID ApcArgument3);
NTSTATUS NtQueueApcThread(
_In_ HANDLE ThreadHandle,
_In_ PPS_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcArgument1,
_In_opt_ PVOID ApcArgument2,
_In_opt_ PVOID ApcArgument3);
2
3
4
5
6
7
8
9
10
11
ThreadHandle 必须具备 THREAD_SET_CONTEXT 访问权限掩码(access mask)才能正常工作。目标线程可以属于与调用方不同的进程,但 32 位进程无法将 APC 排入属于 64 位进程的线程。无论如何,ApcRoutine 必须在目标进程中有效。APC 例程本身接受调用方指定的三个任意参数。
如果目标线程从未进入可警告等待状态,会发生什么?APC 将永远不会执行。在某些情况下,如果向该线程排入更多 APC,排入操作将会失败。这意味着,通常排入方需要以某种方式“知晓”目标线程会时不时进入可警告等待状态,以便 APC 能够执行。
如果线程处于可警告等待状态,但始终没有 APC 到达,会发生什么?如果是有限时等待(non-infinite wait),则超时时间结束后等待会最终终止;如果是无限期等待(infinite wait),线程将永远等待。不过,原生 API 提供了无论是否存在 APC 都能将线程从可警告等待状态中释放的方法:
NTSTATUS NtAlertThread(_In_ HANDLE ThreadHandle);
NTSTATUS NtAlertThreadByThreadId(_In_ HANDLE ThreadId);
NTSTATUS NtAlertResumeThread(
_In_ HANDLE ThreadHandle,
_Out_opt_ PULONG PreviousSuspendCount);
2
3
4
5
6
如果线程处于可警告等待状态,它将被唤醒(alerted)并继续执行。等待函数(或 NtDelayExecution)的返回值将是 STATUS_ALERTED(0x101),而非 STATUS_USER_APC(0xc0)。最后一个函数(NtAlertResumeThread)会在唤醒线程之前先恢复(resume)该线程。如果已知目标线程处于挂起(suspended)状态,这个函数会非常有用。
最后,NtTestAlert 函数用于检测当前线程是否设置了“已警报”(alerted)标志。如果已设置,则返回 STATUS_ALERTED,并执行挂起的 APC:
NTSTATUS NtTestAlert();
请注意,要使
NtAlert*系列 API 正常工作,目标线程必须通过原生 API(NtWait...或NtDelayExecution)处于可警告等待状态,而非通过 Windows API 中的近似等效函数(这也是两者并非完全等效的原因之一)。
# 6.6 线程池
在许多情况下,使用 NtCreateThread 及类似函数显式创建线程是可行的。但在某些场景中,只需执行少量工作,此时创建和管理新线程的开销就显得得不偿失。线程池(thread pool)正是为这种情况设计的。通过线程池,可提交任务请求,由线程池中的线程处理这些请求。这带来了以下优势:
- 无需显式创建和管理线程。
- 执行速度通常更快,因为线程池中的部分线程处于空闲状态,等待工作任务。一旦收到任务,线程会被唤醒、执行任务,之后返回等待状态。这比创建新线程更快。
- 即使线程池负载较高,创建的线程数量也可以得到限制。由于系统中的处理器数量有限,过多的线程无法同时被利用。
- 线程池还支持在计时器到期(timer expiring)或对象受信(objects becoming signaled)时运行代码。这比为此类目的单独创建线程更高效。
与线程池相关的原生 API 函数均以 Tp 开头。文档化的 Windows API 是这些原生 API 的轻量级包装器(thin wrappers)。两者的参数几乎完全相同,不同之处在于:原生 API 返回 NTSTATUS,任何结果都通过间接的第一个参数提供;而 Windows API 直接返回结果(如果没有特殊结果,则返回布尔值)。例如,以下是两个等效的 API:
NTSTATUS TpAllocWork( // 原生 API
_Out_ PTP_WORK *WorkReturn,
_In_ PTP_WORK_CALLBACK Callback,
_Inout_opt_ PVOID Context,
_In_opt_ PTP_CALLBACK_ENVIRON CallbackEnviron);
2
3
4
5
PTP_WORK CreateThreadpoolWork( // Windows API
_In_ PTP_WORK_CALLBACK pfnwk,
_Inout_opt_ PVOID pv,
_In_opt_ PTP_CALLBACK_ENVIRON pcbe);
2
3
4
表 6-1 列出了许多 Windows 线程池 API 及其对应的原生 API。有关这些 API 的详细描述,请参阅 Windows SDK 文档。
可查看 phnt 项目以获取更多 API。
表 6-1:线程池 API
| Windows API | 原生 API |
|---|---|
| TrySubmitThreadPoolCallback | TpSimpleTryPost |
| CreateThreadpoolWork | TpAllocWork |
| CloseThreadpoolWork | TpReleaseWork |
| SubmitThreadpoolWork | TpPostWork |
| CreateThreadpool | TpAllocPool |
| CloseThreadpool | TpReleasePool |
| SetThreadpoolThreadMaximum | TpSetPoolMaxThreads |
| SetThreadpoolThreadMinimum | TpSetPoolMinThreads |
| SetThreadpoolStackInformation | TpSetPoolStackInformation |
| QueryThreadpoolStackInformation | TpQueryPoolStackInformation |
| CreateThreadpoolCleanupGroup | TpAllocCleanupGroup |
| CloseThreadpoolCleanupGroup | TpReleaseCleanupGroup |
| SetThreadpoolCallbackCleanupGroup | TpSetCallbackCleanupGroup |
| CloseThreadpoolCleanupGroupMembers | TpReleaseCleanupGroupMembers |
| CreateThreadpoolTimer | TpAllocTimer |
| SetThreadpoolTimer | TpSetTimer |
| SetThreadpoolTimerEx | TpSetTimerEx |
| IsThreadpoolTimerSet | TpIsTimerSet |
| CreateThreadpoolWait | TpAllocWait |
| CreateThreadpoolIo | TpAllocIoCompletion |
# 6.7 更多线程 API
本节将介绍另外几个与线程相关的原生 API。
线程的上下文(context)以 CPU 寄存器(registers)的形式表示其执行状态。所用的结构名为 CONTEXT,且与平台相关(platform-specific)。该结构在 Microsoft 文档中有详细描述。可以通过以下 API 读取或写入线程上下文:
NTSTATUS NtGetContextThread(
_In_ HANDLE ThreadHandle, // 需要 THREAD_GET_CONTEXT 权限
_Inout_ PCONTEXT ThreadContext);
NTSTATUS NtSetContextThread(
_In_ HANDLE ThreadHandle, // 需要 THREAD_SET_CONTEXT 权限
_In_ PCONTEXT ThreadContext);
2
3
4
5
6
7
为了安全地设置上下文(或以可预测的方式读取上下文),应先通过调用 NtSuspendThread 挂起线程:
NTSTATUS NtSuspendThread(
_In_ HANDLE ThreadHandle,
_Out_opt_ PULONG PreviousSuspendCount);
2
3
每次调用 NtSuspendThread 最终都必须对应调用一次 NtResumeThread:
NTSTATUS NtResumeThread(
_In_ HANDLE ThreadHandle,
_Out_opt_ PULONG PreviousSuspendCount);
2
3
每个线程都维护一个挂起计数(suspend count),上述 API 可选择性地返回该计数。挂起计数的最大值为 127。
有时需要获取线程的句柄(handle),此时 NtOpenThread 可以提供帮助:
NTSTATUS NtOpenThread(
_Out_ PHANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PCLIENT_ID ClientId);
2
3
4
5
它与第 5 章中遇到的 NtOpenProcess 的相似性显而易见。ClientId 应在 UniqueThread 中包含线程 ID,UniqueProcess 设为 0。
可以通过 NtTerminateThread 终止线程(仅在极端情况下使用):
NTSTATUS NtTerminateThread(
_In_opt_ HANDLE ThreadHandle, // 需要 THREAD_TERMINATE 权限
_In_ NTSTATUS ExitStatus);
2
3
以下 API 支持枚举特定进程中的线程:
NTSTATUS NtGetNextThread(
_In_ HANDLE ProcessHandle, // 需要 PROCESS_QUERY_INFORMATION 权限
_In_opt_ HANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ ULONG HandleAttributes,
_In_ ULONG Flags, // 必须为 0
_Out_ PHANDLE NewThreadHandle);
2
3
4
5
6
7
这与我们在第 5 章中遇到的 NtGetNextProcess 类似。枚举时,需将 ThreadHandle 设为 NULL 开始,当返回状态为失败时结束枚举。不要忘记关闭获取到的每个句柄,以防止句柄泄漏(handle leak)。
# 6.8 总结
本章介绍了与线程相关的原生 API。下一章将讨论内核对象(kernel objects)和句柄(handles)。