继续学习windows 中和线程有关系的数据结构: ETHREAD、KTHREAD、TEB
1. 相关阅读材料
《windows 内核原理与实现》 --- 潘爱民
2. 数据结构分析
我们知道,windows内核中的执行体层负责各种与管理和策略相关的功能,而内核层(微内核)实现了操作系统的核心机制。进程和线程在这两层上都有对应的数据结构。
我们先从执行体层的ETHREAD开始。
一. ETHREAD
ETHREAD(执行体线程块)是执行体层上的线程对象的数据结构。在windows内核中,每个进程的每一个线程都对应着一个ETHREAD数据结构。接下来,我们以windows XP下的notepad.exe为实验材料进行学习,winDbg的双机调试和winDbg的命令使用请参阅(学习笔记(2))。
kd> dt _ethread 80553740
ntdll!_ETHREAD
+0x000 Tcb : _KTHREAD
+0x1c0 CreateTime : _LARGE_INTEGER 0x0
+0x1c0 NestedFaultCount : 0y00
+0x1c0 ApcNeeded : 0y0
+0x1c8 ExitTime : _LARGE_INTEGER 0x0
+0x1c8 LpcReplyChain : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x1c8 KeyedWaitChain : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x1d0 ExitStatus : 0
+0x1d0 OfsChain : (null)
+0x1d4 PostBlockList : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x1dc TerminationPort : (null)
+0x1dc ReaperLink : (null)
+0x1dc KeyedWaitValue : (null)
+0x1e0 ActiveTimerListLock : 0
+0x1e4 ActiveTimerListHead : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x1ec Cid : _CLIENT_ID
+0x1f4 LpcReplySemaphore : _KSEMAPHORE
+0x1f4 KeyedWaitSemaphore : _KSEMAPHORE
+0x208 LpcReplyMessage : (null)
+0x208 LpcWaitingOnPort : (null)
+0x20c ImpersonationInfo : (null)
+0x210 IrpList : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x218 TopLevelIrp : 0
+0x21c DeviceToVerify : (null)
+0x220 ThreadsProcess : (null)
+0x224 StartAddress : (null)
+0x228 Win32StartAddress : (null)
+0x228 LpcReceivedMessageId : 0
+0x22c ThreadListEntry : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x234 RundownProtect : _EX_RUNDOWN_REF
+0x238 ThreadLock : _EX_PUSH_LOCK
+0x23c LpcReplyMessageId : 0
+0x240 ReadClusterSize : 0
+0x244 GrantedAccess : 0x1f03ff
+0x248 CrossThreadFlags : 0
+0x248 Terminated : 0y0
+0x248 DeadThread : 0y0
+0x248 HideFromDebugger : 0y0
+0x248 ActiveImpersonationInfo : 0y0
+0x248 SystemThread : 0y0
+0x248 HardErrorsAreDisabled : 0y0
+0x248 BreakOnTermination : 0y0
+0x248 SkipCreationMsg : 0y0
+0x248 SkipTerminationMsg : 0y0
+0x24c SameThreadPassiveFlags : 0
+0x24c ActiveExWorker : 0y0
+0x24c ExWorkerCanWaitUser : 0y0
+0x24c MemoryMaker : 0y0
+0x250 SameThreadApcFlags : 0
+0x250 LpcReceivedMsgIdValid : 0y0
+0x250 LpcExitThreadCalled : 0y0
+0x250 AddressSpaceOwner : 0y0
+0x254 ForwardClusterOnly : 0 ''
+0x255 DisablePageFaultClustering : 0 ''
在windows的"开源"项目WRK中,我们也可以找到ETHREAD的结构体定义代码:
typedef struct _ETHREAD
{
KTHREAD Tcb;
LARGE_INTEGER CreateTime;
union
{
LARGE_INTEGER ExitTime;
LIST_ENTRY LpcReplyChain;
LIST_ENTRY KeyedWaitChain;
};
union
{
NTSTATUS ExitStatus;
PVOID OfsChain;
};
LIST_ENTRY PostBlockList;
union
{
PTERMINATION_PORT TerminationPort;
struct _ETHREAD *ReaperLink;
PVOID KeyedWaitValue;
};
KSPIN_LOCK ActiveTimerListLock;
LIST_ENTRY ActiveTimerListHead;
CLIENT_ID Cid;
union
{
KSEMAPHORE LpcReplySemaphore;
KSEMAPHORE KeyedWaitSemaphore;
};
union
{
PVOID LpcReplyMessage; // -> Message that contains the reply
PVOID LpcWaitingOnPort;
};
PPS_IMPERSONATION_INFORMATION ImpersonationInfo;
LIST_ENTRY IrpList;
ULONG_PTR TopLevelIrp; // either NULL, an Irp or a flag defined in FsRtl.h
struct _DEVICE_OBJECT *DeviceToVerify;
PEPROCESS ThreadsProcess;
PVOID StartAddress;
union {
PVOID Win32StartAddress;
ULONG LpcReceivedMessageId;
};
LIST_ENTRY ThreadListEntry;
EX_RUNDOWN_REF RundownProtect;
EX_PUSH_LOCK ThreadLock;
ULONG LpcReplyMessageId; // MessageId this thread is waiting for reply to
ULONG ReadClusterSize;
ACCESS_MASK GrantedAccess;
union
{
ULONG CrossThreadFlags;
struct
{
ULONG Terminated : 1;
ULONG DeadThread : 1;
ULONG HideFromDebugger : 1;
ULONG ActiveImpersonationInfo : 1;
ULONG SystemThread : 1;
ULONG HardErrorsAreDisabled : 1;
ULONG BreakOnTermination : 1;
ULONG SkipCreationMsg : 1;
ULONG SkipTerminationMsg : 1;
};
};
union
{
ULONG SameThreadPassiveFlags;
struct
{
ULONG ActiveExWorker : 1;
ULONG ExWorkerCanWaitUser : 1;
ULONG MemoryMaker : 1;
ULONG KeyedEventInUse : 1;
};
};
union
{
ULONG SameThreadApcFlags;
struct
{
BOOLEAN LpcReceivedMsgIdValid : 1;
BOOLEAN LpcExitThreadCalled : 1;
BOOLEAN AddressSpaceOwner : 1;
BOOLEAN OwnsProcessWorkingSetExclusive : 1;
BOOLEAN OwnsProcessWorkingSetShared : 1;
BOOLEAN OwnsSystemWorkingSetExclusive : 1;
BOOLEAN OwnsSystemWorkingSetShared : 1;
BOOLEAN OwnsSessionWorkingSetExclusive : 1;
BOOLEAN OwnsSessionWorkingSetShared : 1;
BOOLEAN ApcNeeded : 1;
};
};
BOOLEAN ForwardClusterOnly;
BOOLEAN DisablePageFaultClustering;
UCHAR ActiveFaultCount;
} ETHREAD, *PETHREAD;
我们现在来一条一条地学习这个数据结构的域成员.
1. KTHREAD Tcb
如同EPROCESS结构包含了内核层的KPROCESS对象一样,ETHREAD结构也"内嵌(注意是内嵌,不是指向)"了KTHREAD对象作为第一个数据成员,所以,一个指向ETHREAD对象的指针同时也是一个指向KTHREAD对象的指针。关于内核层的KTHREAD结构我们放到后面介绍,我们集中精力学习ETHREAD的结构。
+0x000
2. LARGE_INTEGER CreateTime
CreateTime域包含饿了线程的创建时间,它是在线程创建时被赋值的。我们要明白的是,我们在任务管理器中之所以能看那么多的性能参数,"计划任务"的运行,包括线程的"饥饿算法"的调度,很大程度上是因为在线程和进程的数据结构中保存了大量一些基础参数。
3. LARGE_INTEGER ExitTime
ExitTime域包含了线程的退出时间。它是在线程退出函数中被赋值的:
VOID ExitThread(DWORD dwExitCode);
我们回想在KPROCESS中的KernelTime和UserTime域成员。我们现在知道了,只有在线程退出时,在赋值ExitTime的同时还会给当前线程所属的进程的KernelTime和UserTime域进行"更新"。
4. LPC(跨进程通信)
LIST_ENTRY LpcReplyChain;
LIST_ENTRY KeyedWaitChain;
LpcReplyChain域用于跨进程通信(LPC),KeyedWaitChain域用于带键事件的等待链表,这里对LPC的相关知识做一下拓展。
4.1) 本地过程调用(LPC Local Procedure Call),有时也称为Lightweight Procdure Call。
(回想还有多少种其他的进程间通信机制)
1) 互斥体(mutex)
2) 信号量(samaphore)
3) 锁(lock)
4) 临界区(critical section)
5) 自旋锁(spinlock)和忙等待
6) 消息
//关于进程/线程间的"通信"机制的详细内容请参考《windows 内核原理与实现》5.1 章
4.2) 那LPC有什么特点呢?
LPC是一种直接由内核支持的进程间通信机制,它非常高效,主要用于操作系统各个"组件"之间进行通信,或者用户模式程序与系统组件之间通信。
LPC涉及两方面通信,其基本工作方式是消息传递。一个进程创建一个"LPC端口对象(注意这些名词,第一步一定是先创建一个端口对象,之后会解释它们的关系)",然后等待其他进程连接过来。当其他进程成功地连接到一个LPC端口之后,两者便可以开始通信。LPC允许两个进程进行双向通信。
它在windows中的主要应用场景如下:
1) windows应用程序与系统进程,包括windows环境子系统之间的通信。这通常发生在一些windows API函数的内部
2) 用户模式程序与内核模式组件之间的通信。比如lsass(Local Security Authority SubSystem)进程与安全引用监视器(SRM Security Reference Monitor)之间的通信就是通过LPC
来完成的
3) 当RPC(远过程调用 Remote Procedure Call)的"两端(LPC是一个C/S模型)"在同一个系统中时,RPC通信会自动转化为LPC
回想我们之前学习EPROCESS数据结构的时候曾经说过有两个成员域:
PVOID DebugPort;
PVOID ExceptionPort;
分别为调试端口和异常端口,它们有机会接收和处理该进程中的异常,包括调试事件。这里的异常端口正是LPC端口,windows子系统通过此端口可以获取进程中发生的异常。
4.3) LPC结构模型(通信建立过程)
通过LPC进行通信的两个进程本质上是客户-服务器模型,其工作方式与基于连接的socket编程模型相仿(只不过socket是通过发送TCP/UDP数据包的方式来进行通信,而LPC是内核中建立端口进行通信)。
1. 服务器进程创建一个LPC"连接端口"对象,然后在该"连接端口"上监听连接请求。
2. 客户进程根据已知的端口名称,请求连接到此端口对象上
3. 当服务器收到连接请求时,它创建一个"通信端口"对象,用以代表与"该客户进程(一个LPC的服务端可以同时和多个客户端交互)"之间的LPC连接
4. 而客户进程也会创建一个LPC"通信端口"对象,代表它与服务器进程之间的LPC连接
5. 连接端口对象有名称,通信端口对象没有名称,它们是私有对象(这也很好理解,通信端口是临时的,而连接端口必须常在),所以,除了服务器进程和客户进程通过"通信端口句柄"来访问它们
以外,其他进程是无法访问的(思考原因: 因为句柄是进程私有的,同一个句柄值在不同进程空间是不同的,所以只能本进程来使用这个通信端口句柄)
6. LPC连接端口对象属于服务器进程,它们有公开的名称,通常被加入到系统的"对象管理器目录"中,所以,其他进程可以通过"名称"访问它们,从而建立起单独的LPC连接。
LPC服务器进程可以同时连接多个客户,每个客户通过名称连接到服务器的连接端口对象,而服务器为每个接受的客户进程创建一个通信端口对象。所以说,LPC是一对多的通信模型。LPC的连接端口对象是一个中心的连接点,它只接受连接请求,不接受数据请求(思考FTP的21号端口是不是同样的思想),而通信端口可以进行任意的数据请求和服务(思考FTP的20号数据端口是不是同样的思想)。这是连接端口和通信端口职责的区别。
(更多内容请参阅《windows 内核原理与实现》 8.2章)
5. NTSTATUS ExitStatus
ExitStatus是线程的退出状态。当线程主动退出或被动退出时这个域会由框架代码填充,回想EPROCESS的LastThreadExitStatus域,进程中的每个线程退出时,除了给自己的ETHREAD的ExitStatus赋值以外,还会给当前线程所属的进程的EPROCESS的LastThreadExitStatus进行赋值。
6. LIST_ENTRY PostBlockList
PostBlockList域是一个双链表头节点,该链表中的各个节点类型为PCM_POST_BLOCK,它被用于一个线程向"配置管理器"登记注册表键的变化通知。
typedef struct _CM_POST_BLOCK
{
#if DBG
BOOLEAN TraceIntoDebugger;
#endif
LIST_ENTRY NotifyList;
LIST_ENTRY ThreadList;
LIST_ENTRY CancelPostList; // slave notifications that are attached to this notification
struct _CM_POST_KEY_BODY *PostKeyBody;
ULONG NotifyType;
PCM_POST_BLOCK_UNION u;
} CM_POST_BLOCK, *PCM_POST_BLOCK;
7. PTERMINATION_PORT TerminationPort
TerminationPort域是一个链表头,当一个线程退出时,系统会通知所有已经登记过要接收其终止事件的那些"端口"
8. struct _ETHREAD *ReaperLink
ReaperLink域是一个单链表节点,它仅在线程退出时使用。当线程被终止时,该节点将被挂到PsReaperListHead链表上(用以告知内核当前线程将要退出了,请收到相关的线程资源),所以,在线程回收器(reaper)的工作项目(WorkItem)中该线程的内核栈得以收回。
9. 线程的定时器
KSPIN_LOCK ActiveTimerListLock;
LIST_ENTRY ActiveTimerListHead;
ActiveTimerListHead域是一个双链表的头,链表中包含了当前线程的所有定时器。
ActiveTimerListHead域操作这个链表(包含当前线程的所有定时器的双链表)的自旋锁。使用自旋锁可以把原本可能发生的并行事件导致的问题通过强制串行化得到解决。比如对线程中的定时器这个互斥
资源就典型的需要串行化,否则将导致定时器的错乱等很多问题
10. CLIENT_ID Cid
Cid域是一个常用的域,其中包含了线程的"唯一标识符",其类型为CLIENT_ID。
typedef struct _CLIENT_ID
{
PVOID UniqueProcess;
PVOID UniqueThread;
} CLIENT_ID, *PCLIENT_ID;
CLIENT_ID包括两部分:
UniqueProcess: 等于所属进程的UniqueProcessId
UniqueThread: 等于此线程对象在进程句柄表中的句柄
(每个内核对象都是以句柄的形式在其他内核对象的句柄表中存在的,关于内核对象及其句柄的定义请参阅《windows 核心编程》 3章 内核对象。 关于句柄表的相关知识请参阅学习笔记(2))
11. LPC通信相关
union
{
KSEMAPHORE LpcReplySemaphore;
KSEMAPHORE KeyedWaitSemaphore;
};
union
{
LPC_MESSAGE// -> Message that contains the reply
PVOID LpcWaitingOnPort;
};
这两个union域域LPC有关。我们合并学习
1) LpcReplySemaphore域用于LPC应答通知
2) KeyedWaitSemaphore域用于处理带键的事件
typedef struct _KSEMAPHORE
{
DISPATCHER_HEADER Header;
LONG Limit;
} KSEMAPHORE, *PKSEMAPHORE;
3) LpcReplyMessage域是一个指向LPCP_MESSAGE的指针,其中包含了LPC应答的消息
//Structure LPC_MESSAGE it's a header for all LPC messages. Any LPC message must contains at least 0x18 bytes length for LPC_MESSAGE header.
typedef struct _LPC_MESSAGE
{
USHORT DataLength;
USHORT Length;
USHORT MessageType;
USHORT DataInfoOffset;
CLIENT_ID ClientId;
ULONG MessageId;
ULONG CallbackId;
} LPC_MESSAGE, *PLPC_MESSAGE;
4) LpcWaitingOnPort
LpcWaitingOnPort说明了当前线程在哪个"端口对象"上等待消息
LpcReplyMessage和LpcWaitingOnPort两个域虽然公用同一个union空间,但是其最低位可用来区分应该用哪个,在WRK的 \base\ntos\lpc\lpcp.h中有两个宏专门用来进行此判断
#define LpcpGetThreadMessage(T) \
( \
(((ULONG_PTR)(T)->LpcReplyMessage) & LPCP_PORT_BIT) ? NULL : \
(PLPCP_MESSAGE)((ULONG_PTR)(T)->LpcReplyMessage & ~LPCP_THREAD_ATTRIBUTES) \
)
#define LpcpGetThreadPort(T) \
( \
(((ULONG_PTR)(T)->LpcReplyMessage) & LPCP_PORT_BIT) ? \
(PLPCP_PORT_OBJECT)(((ULONG_PTR)(T)->LpcWaitingOnPort) & ~LPCP_THREAD_ATTRIBUTES): \
NULL \
)
关于LPC的,在《windows 内核原理与实现》 8.2节 有更详细的介绍
12. PPS_IMPERSONATION_INFORMATION ImpersonationInfo
ImpersonationInfo域指向线程的模仿信息,windows允许一个线程在执行过程中模仿其他的用户来执行一段功能,这样可以实现更为灵活的访问控制安全特性。关于这个字段,我没有找到很多相关的资料。
http://blogs.ejb.cc/archives/6955/windows-internal-quest-thread
文章中提到:
ImpersonationInfo 字段: 保存了身份模仿信息的指针(访问令牌和模仿级别,如果该线程正在模仿一个客户的话)
我自己理解的是是不是有点类似在javascript编程中很常见的那种情况,模拟用户点击,移动等事件的那种思路。具体的我也不是太清除,希望知道的朋友能给出链接,共同学习
13. LIST_ENTRY IrpList
IrpList域是一个双链表头,其中包含了当前线程所有正在处理但尚未完成的I/O请求(Irp对象)。关于IRP的相关知识请参阅学习笔记(1)
14. ULONG_PTR TopLevelIrp
TopLevelIrp域指向线程的顶级IRP,它有三种可能的取值:
1) 指向NULL
2) 指向一个IRP
3) 包含了fsrtl.h中定义的标记FSRTL_FAST_IO_TOP_LEVEL_IRP或FSRTL_FSP_TOP_LEVEL_IRP
仅当一个线程的I/O调用层次中最顶层的组件是文件系统时(内核中设备栈的概念),TopLevelIrp域才指向当前线程发送的IRP
15. struct _DEVICE_OBJECT *DeviceToVerify
DeviceToVerify域指向的是一个"待检验"的设备,当磁盘或CD-ROM设备的驱动程序"发现"自从上一次该线程访问该设备以来,该设备有了"变化",就会设置线程的DeviceToVerify域,从而使最高层的驱动程序(比如文件系统),可以检测到设备变化
DeviceToVerify指向的是一个设备对象:
typedef struct _DEVICE_OBJECT
{
SHORT Type;
WORD Size;
LONG ReferenceCount;
PDRIVER_OBJECT DriverObject;
PDEVICE_OBJECT NextDevice;
PDEVICE_OBJECT AttachedDevice;
PIRP CurrentIrp;
PIO_TIMER Timer;
ULONG Flags;
ULONG Characteristics;
PVPB Vpb;
PVOID DeviceExtension;
ULONG DeviceType;
CHAR StackSize;
BYTE Queue[40];
ULONG AlignmentRequirement;
KDEVICE_QUEUE DeviceQueue;
KDPC Dpc;
ULONG ActiveThreadCount;
PVOID SecurityDescriptor;
KEVENT DeviceLock;
WORD SectorSize;
WORD Spare1;
PDEVOBJ_EXTENSION DeviceObjectExtension;
PVOID Reserved;
} DEVICE_OBJECT, *PDEVICE_OBJECT;
16. PEPROCESS ThreadsProcess
ThreadsProcess域指向当前线程所属的进程的EPROCESS结构,这是在线程初始创建时赋值的,因为每个线程都是从一个进程中创建来的,利用这个字段可以很方便的从一个线程寻址到它所属的进程。
base\ntos\inc\ps.h 中的宏 THREAD_TO_PROCESS 就是专门用来实现此功能的。
#define
回想一下,在KPROCESS和EPROCESS中也都有有一个成员域: ThreadListHead用来指向进程对应的线程的。我们可以发现,windows中的进程和线程结构是互相绑定的。要互相寻址非常方便。
1) EPROCESS->ThreadListHead: 指向当前进程所属线程的ETHREAD链表
2) KPROCESS->ThreadListHead: 指向当前进程所属线程的KTHREAD链表
3) ETHREAD->ThreadsProcess: 指向当前线程所属的进程的EPROCESS链表
4) KTHREAD->Process: 指向当前线程所属的进程的KPROCESS链表
17. PVOID StartAddress
StartAddress域包含了线程的启动地址,这是真正的线程启动地址,即入口地址。也就是我们在创建线程的之后指定的入口函数的地址。
19. PVOID Win32StartAddress
Win32StartAddress域包含的是windows子系统接收到的线程启动地址,即CreateThread API函数接收到的线程启动地址。那这就有一个问题了,这个Win32StartAddress和我们之前学的StartAddress有什么区别呢?
StartAddress域包含的通常是系统DLL中的线程启动地址,因而往往是相同的(例如kernel32.dll中的BaseProcessStart或BaseThreadStart函数)。而Win32StartAddress域中包含的才真正是windows子系统接收到的线程启动地址,即CreateThread中指定的那个函数入口地址。
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, //线程安全属性
DWORD dwStackSize, // 堆栈大小
LPTHREAD_START_ROUTINE lpStartAddress, // 线程函数,即入口地址
LPVOID lpParameter, //线程参数
DWORD dwCreationFlags, // 线程创建属性
LPDWORD lpThreadId // 线程ID
所以,Win32StartAddress不仅对于windows子系统有意义,对于诊断和分析不同线程的行为更有重要的意义
20. ULONG LpcReceivedMessageId
LpcReceivedMessageId域包含了接收到的LPC消息的ID,此域仅当SameThreadPassiveFlags(后面会介绍)域中的LpcReceivedMesIdValid位被置上时候才有效。
//要注意的是:
由于LpcReceivedMessageId和Win32StartAddress域组成了一个union:
union
{
PVOID Win32StartAddress;
ULONG LpcReceivedMessageId;
};
所以,当一个windows子系统线程在接收到LPC消息时,它的Win32StartAddress域也会被修改
21. LIST_ENTRY ThreadListEntry
ThreadListEntry是一个双链表节点,每个线程都会加入到它所属的EPROCESS结构的ThreadListHead双链表中(仔细读这句话,线程和进程的从属关系一定要搞清楚)。我们可以看到,windows通过数据结构很好的维护了进程和线程之间的从属关系
(可以想到可以利用这个域成员来进行进程中的线程枚举,我们会在之后的进程/线程枚举学习笔记中看到)
22. EX_RUNDOWN_REF RundownProtect
RundownProtect域是线程的停止保护锁,对于跨线程引用TEB结构或者挂起线程的执行等操作,需要获得此锁才能运行,以避免在操作过程中线程被销毁。
typedef struct _EX_RUNDOWN_REF
{
union
{
ULONG Count;
PVOID Ptr;
};
} EX_RUNDOWN_REF, *PEX_RUNDOWN_REF;
23. EX_PUSH_LOCK ThreadLock
ThreadLock域是一把推锁,用户保护线程的数据属性,例如PspLockThreadSecurityExclusive和PspLockThreadSecurityShared利用该域来保护线程的安全属性
typedef struct _EX_PUSH_LOCK
{
union
{
ULONG Locked: 1;
ULONG Waiting: 1;
ULONG Waking: 1;
ULONG MultipleShared: 1;
ULONG Shared: 28;
ULONG Value;
PVOID Ptr;
};
} EX_PUSH_LOCK, *PEX_PUSH_LOCK;
24. ULONG LpcReplyMessageId
LpcReplyMessageId域指明了当前线程正在等待对一个LPC消息的应答。
25. ULONG ReadClusterSize
ReadClusterSize域指明了在一次I/O操作中读取多少个页面,用于页面交换文件和内存映射文件的读操作。
关于这个页面交换文件或者内存映射文件我的理解都是使用了"内存映射技术",就是把磁盘上的文件直接映射到虚拟内存中,这样CPU就可以直接对内存数据进行读写,"内存映射技术"可以大大提高效率,并且在windows中应该算很常见的了。
我在学习《寒江独钓》8章 -- 文件系统透明加密中,就会涉及到这个notepad.exe的内存映射以及缓存的知识,希望看这篇文章的朋友也能动手自己去编码实验一下,对内存映射文件一定会有
一个更好的体会。
26. ACCESS_MASK GrantedAccess
GrantedAccess域包含了线程的访问权限,这里的访问权限是一个"位组合",各种权限定义的宏以 bit 位的形式 OR到一起. public\sdk\inc\ntpsapi.h 中的宏 THREAD_XXX定义了线程的权限
#define THREAD_TERMINATE (0x0001) // winnt
// end_ntddk end_wdm end_ntifs
#define THREAD_SUSPEND_RESUME (0x0002) // winnt
#define THREAD_ALERT (0x0004)
#define THREAD_GET_CONTEXT (0x0008) // winnt
#define THREAD_SET_CONTEXT (0x0010) // winnt
// begin_ntddk begin_wdm begin_ntifs
#define THREAD_SET_INFORMATION (0x0020) // winnt
// end_ntddk end_wdm end_ntifs
#define THREAD_QUERY_INFORMATION (0x0040) // winnt
// begin_winnt
#define THREAD_SET_THREAD_TOKEN (0x0080)
#define THREAD_IMPERSONATE (0x0100)
#define THREAD_DIRECT_IMPERSONATION (0x0200)
// begin_ntddk begin_wdm begin_ntifs
#define THREAD_ALL_ACCESS (STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | \
0x3FF)
这里我们回想一下在EPROCESS中也有一个GrantedAccess域表名了进程的权限:
#define PROCESS_TERMINATE (0x0001) // winnt
#define PROCESS_CREATE_THREAD (0x0002) // winnt
#define PROCESS_SET_SESSIONID (0x0004) // winnt
#define PROCESS_VM_OPERATION (0x0008) // winnt
#define PROCESS_VM_READ (0x0010) // winnt
#define PROCESS_VM_WRITE (0x0020) // winnt
// begin_ntddk begin_wdm begin_ntifs
#define PROCESS_DUP_HANDLE (0x0040) // winnt
// end_ntddk end_wdm end_ntifs
#define PROCESS_CREATE_PROCESS (0x0080) // winnt
#define PROCESS_SET_QUOTA (0x0100) // winnt
#define PROCESS_SET_INFORMATION (0x0200) // winnt
#define PROCESS_QUERY_INFORMATION (0x0400) // winnt
#define PROCESS_SET_PORT (0x0800)
#define PROCESS_SUSPEND_RESUME (0x0800) // winnt
// begin_winnt begin_ntddk begin_wdm begin_ntifs
#define PROCESS_ALL_ACCESS (STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | \
0xFFF)
我们可以发现进程和线程在某种程度上来说具有一定的"对称性"
27. 线程的位标识
接下来合并学习三组unino:
union
{
ULONG CrossThreadFlags;
struct
{
ULONG Terminated : 1;
ULONG DeadThread : 1;
ULONG HideFromDebugger : 1;
ULONG ActiveImpersonationInfo : 1;
ULONG SystemThread : 1;
ULONG HardErrorsAreDisabled : 1;
ULONG BreakOnTermination : 1;
ULONG SkipCreationMsg : 1;
ULONG SkipTerminationMsg : 1;
};
};
union
{
ULONG SameThreadPassiveFlags;
struct
{
ULONG ActiveExWorker : 1;
ULONG ExWorkerCanWaitUser : 1;
ULONG MemoryMaker : 1;
ULONG KeyedEventInUse : 1;
};
};
union
{
ULONG SameThreadApcFlags;
struct
{
BOOLEAN LpcReceivedMsgIdValid : 1;
BOOLEAN LpcExitThreadCalled : 1;
BOOLEAN AddressSpaceOwner : 1;
BOOLEAN OwnsProcessWorkingSetExclusive : 1;
BOOLEAN OwnsProcessWorkingSetShared : 1;
BOOLEAN OwnsSystemWorkingSetExclusive : 1;
BOOLEAN OwnsSystemWorkingSetShared : 1;
BOOLEAN OwnsSessionWorkingSetExclusive : 1;
BOOLEAN OwnsSessionWorkingSetShared : 1;
BOOLEAN ApcNeeded : 1;
};
};
1) ULONG CrossThreadFlags
CrossThreadFlags域是一些针对跨线程访问的标志位,可以看到结构体中的 ":1"这种结构,这是C中的特有的写法,代表这个字段只占 1bit位。
所以,CrossThreadFlags标志位包含:
Terminated: 线程已终止操作
DeadThread: 线程创建失败
HideFromDebugger: 该线程对于调试器不可见
ActiveImpersonationInfo: 线程正在模仿
SystemThread: 是一个系统线程
HardErrorsAreDisabled: 对于该线程,硬件错误无效
BreakOnTermination: 调试器在线程终止时停下该线程
SkipCreationMsg: 不向调试器发送创建消息
SkipTerminationMsg: 不向调试器发送终止消息
2) ULONG SameThreadPassiveFlags
SameThreadPassiveFlags域是一些只有在最低中断级别(被动级别)上才可以访问的标志,并且只能被该线程自身访问,所以对这些标志位的访问不需要互锁操作
3) ULONG SameThreadApcFlags
SameThreadApcFlags域是一些在APC中断级别(也是很低的级别)上被该线程自身访问的标志位,同样地,对这些标志位的访问也不需要互锁操作
关于APC的知识属于windows中断与异常相关方面的知识,这里做一些拓展,更多详细内容请参阅《windows 内核原理与实现》 5.2节
APC:
要说APC,首先要介绍一些windows中的中断的知识。
1) CPU硬件中断的优先级顺序
在Intel x86体系结构中,外部硬件中断是通过处理器上的"中断管脚"或者一个称为"本地APIC(local APIC)"的内置模块来发生的。本地APIC可以接收的中断源包括:
1) 处理器管脚(LINT0和LINT1)
2) 本地APIC定时器(timer)
3) 性能监视计数器中断
4) 热传感器中断
5) APIC内部错误中断
//这些中断源都称为本地中断源,另外还有通过I/O APIC转送过来的中断源: 外部连接的I/O设备的中断消息和IPI消息
Ps: "中断"属于"异常"范畴中的一种,我们在学习的时候要注意归类这些概念,对我们整体把握windows的机制有很大好处,建议参考《深入理解计算机系统(原书第2版)》 8.2章异常的相关知识
我们这里介绍就只是中断,即"异常"大范畴的一个子类,我通过查阅资料发现严格意义的"中断"就是指的硬件中断,只要是计算机中的的硬件产生的请求都叫"中断",我自己是这样总结的,
如有不对,欢迎指正,不吝感激。
而APIC这个硬件中断设备是有优先级的,它使用了一个可编程阵列硬件来实现,并且在系统初始化的时候就完成了,Intel x86定义了256个中断向量号(Interrupt Vector Number),也称为中断向量,从0~255。这里我们可以把知识联立起来了,IDT是个中断描述符表,IDT的表项数目就是256,也就是说,APIC定义的是IDT中的中断例程的优先级
2) 对于一个处理器,它一旦被中断(可能来自内部,可能来自外部,可能是硬中断,可能是软中断),则某个预设的"中断服务例程"便被执行。而系统软件(操作系统)要做的事情就是,提供这些例程,并将它们设定到处理器的硬件中断向量表(即IDT)中。
结合我们之前学习的IDT方面的知识,我们可以这样理解:
1. IDT本身可以放在线性地址空间的任何地方,但基地址应该8字节对齐(在学习笔记(3)中对IDT有详细介绍)。
2. 我们是可以把IDT放在任意地方,但是要告知系统它存在在哪里,所以这个IDT的基址保存在IDTR中
3. 可以通过LIDT和SIDT指令分别用于加载和存储IDTR寄存器(LIDT只有在特权级0下才可以使用)
3) 中断请求级别(IRQL)
尽管APIC中断控制器已经提供了中断优先级支持,不过,windows还是自己定义了一套优先级方案,称为"中断请求级别(IRPL Interrupt Request Level)"。在Intel x86系统中,windows使用了0~31来表示优先级,数值越大,优先级越高(联想进程/线程的优先级是不是也是这样?)。
软件中断后非中断代码的IRQL是在内核中管理的,而硬件中断则在HAL中被映射到对应的IRQL。IRQL的宏定义如下
#define PASSIVE_LEVEL 0 //Passive release level(被动级别)
#define LOW_LEVEL 0 //Lowest interrupt level
#define APC_LEVEL 1 //APC interrupt level
#define DISPATCH_LEVEL 2 //Dispatcher level
#define PROFILE_LEVEL 27 //Timer used for profiling
#define CLOCK1_LEVEL 28 //Interval clock 1 level
#define CLOCK2_LEVEL 28 //Interval clock 2 level
#define IPI_LEVEL 29 //Interprocessor interrupt level
#define POWER_LEVEL 30 //Power failure level
#define HIGH_LEVEL 31 //Highest interrupt level
对于这个IRQL,我们要这么理解,它就相当于一种"重要程度"。PASSIVE_LEVEL(被动级别)代表了最低的IRQL,那既然你最低,运行在PASSIVE_LEVEL的线程可以被任何更高的IRQL(从LOW_LEVEL开始都可以)的事情打断,所有的的用户模式代码都运行在PASSIVE_LEVEL(被动模式)上。
Ps: 这里插个题外话,我们使用的APC进程注入的原理也在于此,APC_LEVEL(APC级别)比PASSIVE_LEVEL高,这也正是在一个线程中插入一个APC可以打断该线程(如果被插入的这个线程正在
PASSIVE_LEVEL上运行)的原因
4) APC(异步过程调用)
我们对IRQL中的APC_LEVEL进行一下重点学习
它位于PASSIVE_LEVEL和DISPATCH_LEVEL之间,这是"专门"为另一种称为APC(异步过程调用 Asynchronous Procedure Call)的"软件中断"而保留的IRQL。每个APC都是在特定的线程环境中执行的,从而也一定在特定的进程环境中执行。APC是针对线程的,每个线程都有自己特有的APC链表。同一个线程的APC也是被排队执行的(思考线程的IRP对清队列,我们的IO请求要被排队执行,APC作为IRP请求的一种也不例外)。
由于APC的IRQL高于PASSIVE_LEVEL,所以,它优先于普通的"线程代码"。当一个线程获得控制时,它的APC进程会立刻被执行(即执行APC队列中的IRP请求)。这一特性使得APC非常适合于实现各种异步通知事件。例如,I/O完成通知可以用APC来实现。
(同样,我们可以在进程的创建完成IO回调中插入APC事件,达到APC进程注入的目的)
//详细细节参考sudami的经典文章《N种内核注入DLL的思路及实现》
http://www.pediy.com/kssd/pediy10/75887.html
一下是APC的内核对象(称为APC对象)的数据结构定义:
typedef struct _KAPC
{
UCHAR Type;
UCHAR SpareByte0;
UCHAR Size;
UCHAR SpareByte1;
ULONG SpareLong0;
PKTHREAD Thread;
LIST_ENTRY ApcListEntry;
PVOID KernelRoutine;
PVOID RundownRoutine;
PVOID NormalRoutine;
PVOID NormalContext;
PVOID SystemArgument1;
PVOID SystemArgument2;
CHAR ApcStateIndex;
CHAR ApcMode;
UCHAR Inserted;
} KAPC, *PKAPC;
更多细节请参阅《windows 内核原理与实现》 5.2.6 APC(异步过程调用)
28. 线程的错误处理相关
BOOLEAN ForwardClusterOnly;
BOOLEAN DisablePageFaultClustering;
UCHAR ActiveFaultCount;
最后三个域成员合并学习,它们与线程中发生的页面错误处理有关。
ForwardClusterOnly: 指示是否仅仅前向聚集
DisablePageFaultClustering: 用于控制页面交换的聚集与否
ActiveFaultCount: 包含了正在进行之中的页面错误数量
至此,ETHREAD的数据结构就学习完成了。限于篇幅的原因,我们把KPROCESS和TEB放到下一篇中继续学习