继续我们的线程相关的数据结构的学习。接下来我们学习 KTHREAD,TEB这两个结构。
1. 相关阅读材料
1. 《加密与解密3》
2. [经典文章翻译]A_Crash_Course_on_the_Depths_of_Win32_Structured_Exception_Handling.pdf
3. 《0 DAY安全: 软件漏洞分析技术》
2. 数据结构分析
二. KTHREAD
KTHREAD(内核层线程对象)。再次重复说明一点:
windows内核中的执行体(ETHREAD, EPROCESS) 负责各种与管理和策略相关的功能,而内核层(或微内核)(KTHREAD, KPROCESS)实现了操作系统的核心机制。
进程和线程在这两层上都有对应的数据结构
之所以多次提到这一点是因为我发现在研究一项技术或者一个机制、数据结构之前,如果脑袋中能建立起对这个事物的功能上的深刻认识,知道这个技术将用在哪里,将实现哪些功能,到具体深入学习的时候就能有很深的体会。所以小瀚这里也建议朋友们也能先深刻理解为什么windows在内核中要有两套"类似"的数据结构来标识进程和线程,以及它们的联系和区别、侧重点都在哪里。
另外,我们知道执行体线程块RTHREAD的第一个成员域就是KTHREAD Tcb,这个Tcb指的就是KPROCESS。这点和KPROCESS是EPROCESS的第一个成员域是一样的
nt!_ETHREAD +0x000 Tcb : _KTHREAD +0x000 Header : _DISPATCHER_HEADER +0x010 MutantListHead : _LIST_ENTRY +0x018 InitialStack : Ptr32 Void +0x01c StackLimit : Ptr32 Void +0x020 Teb : Ptr32 Void +0x024 TlsArray : Ptr32 Void +0x028 KernelStack : Ptr32 Void +0x02c DebugActive : UChar +0x02d State : UChar +0x02e Alerted : [2] UChar +0x030 Iopl : UChar +0x031 NpxState : UChar +0x032 Saturation : Char +0x033 Priority : Char +0x034 ApcState : _KAPC_STATE +0x04c ContextSwitches : Uint4B +0x050 IdleSwapBlock : UChar +0x051 Spare0 : [3] UChar +0x054 WaitStatus : Int4B +0x058 WaitIrql : UChar +0x059 WaitMode : Char +0x05a WaitNext : UChar +0x05b WaitReason : UChar +0x05c WaitBlockList : Ptr32 _KWAIT_BLOCK +0x060 WaitListEntry : _LIST_ENTRY +0x060 SwapListEntry : _SINGLE_LIST_ENTRY +0x068 WaitTime : Uint4B +0x06c BasePriority : Char +0x06d DecrementCount : UChar +0x06e PriorityDecrement : Char +0x06f Quantum : Char +0x070 WaitBlock : [4] _KWAIT_BLOCK +0x0d0 LegoData : Ptr32 Void +0x0d4 KernelApcDisable : Uint4B +0x0d8 UserAffinity : Uint4B +0x0dc SystemAffinityActive : UChar +0x0dd PowerState : UChar +0x0de NpxIrql : UChar +0x0df InitialNode : UChar +0x0e0 ServiceTable : Ptr32 Void +0x0e4 Queue : Ptr32 _KQUEUE +0x0e8 ApcQueueLock : Uint4B +0x0f0 Timer : _KTIMER +0x118 QueueListEntry : _LIST_ENTRY +0x120 SoftAffinity : Uint4B +0x124 Affinity : Uint4B +0x128 Preempted : UChar +0x129 ProcessReadyQueue : UChar +0x12a KernelStackResident : UChar +0x12b NextProcessor : UChar +0x12c CallbackStack : Ptr32 Void +0x130 Win32Thread : Ptr32 Void +0x134 TrapFrame : Ptr32 _KTRAP_FRAME +0x138 ApcStatePointer : [2] Ptr32 _KAPC_STATE +0x140 PreviousMode : Char +0x141 EnableStackSwap : UChar +0x142 LargeStack : UChar +0x143 ResourceIndex : UChar +0x144 KernelTime : Uint4B +0x148 UserTime : Uint4B +0x14c SavedApcState : _KAPC_STATE +0x164 Alertable : UChar +0x165 ApcStateIndex : UChar +0x166 ApcQueueable : UChar +0x167 AutoAlignment : UChar +0x168 StackBase : Ptr32 Void +0x16c SuspendApc : _KAPC +0x19c SuspendSemaphore : _KSEMAPHORE +0x1b0 ThreadListEntry : _LIST_ENTRY +0x1b8 FreezeCount : Char +0x1b9 SuspendCount : Char +0x1ba IdealProcessor : UChar +0x1bb DisableBoost : UChar .....
在WRK的源代码 base\ntos\inc\ke.h 中,我们也可以找到KTHREAD的数据结构定义
typedef struct _KTHREAD { DISPATCHER_HEADER Header; LIST_ENTRY MutantListHead; PVOID InitialStack; PVOID StackLimit; PVOID KernelStack; KSPIN_LOCK ThreadLock; union { KAPC_STATE ApcState; struct { UCHAR ApcStateFill[KAPC_STATE_ACTUAL_LENGTH]; BOOLEAN ApcQueueable; volatile UCHAR NextProcessor; volatile UCHAR DeferredProcessor; UCHAR AdjustReason; SCHAR AdjustIncrement; }; }; KSPIN_LOCK ApcQueueLock; ULONG ContextSwitches;
UCHAR State;
UCHAR NpxState; BOOLEAN Alertable;
UCHAR WaitIrql; BOOLEAN WaitNext; UCHAR WaitReason;
CHAR WaitMode; SCHAR Priority;
UCHAR BasePriority UCHAR EnableStackSwap; volatile UCHAR SwapBusy; BOOLEAN Alerted[MaximumMode]; union { LIST_ENTRY WaitListEntry; SINGLE_LIST_ENTRY SwapListEntry; }; PRKQUEUE Queue;
ULONG WaitTime; PVOID Teb; union { KTIMER Timer; struct { UCHAR TimerFill[KTIMER_ACTUAL_LENGTH]; union { struct { LONG AutoAlignment : 1; LONG DisableBoost : 1; LONG ReservedFlags : 30; }; LONG ThreadFlags; }; }; }; union { KWAIT_BLOCK WaitBlock[THREAD_WAIT_OBJECTS + 1]; struct { UCHAR WaitBlockFill0[KWAIT_BLOCK_OFFSET_TO_BYTE0]; BOOLEAN SystemAffinityActive; }; struct { UCHAR WaitBlockFill1[KWAIT_BLOCK_OFFSET_TO_BYTE1]; CCHAR PreviousMode; }; struct { UCHAR WaitBlockFill2[KWAIT_BLOCK_OFFSET_TO_BYTE2]; UCHAR ResourceIndex; }; struct { UCHAR WaitBlockFill3[KWAIT_BLOCK_OFFSET_TO_BYTE3]; UCHAR LargeStack; }; }; LIST_ENTRY QueueListEntry; PKTRAP_FRAME TrapFrame; PVOID CallbackStack; PVOID ServiceTable; UCHAR ApcStateIndex; UCHAR IdealProcessor; BOOLEAN Preempted; BOOLEAN ProcessReadyQueue; BOOLEAN KernelStackResident; SCHAR BasePriority; SCHAR PriorityDecrement; CHAR Saturation; KAFFINITY UserAffinity; PKPROCESS Process; KAFFINITY Affinity; PKAPC_STATE ApcStatePointer[2]; union { KAPC_STATE SavedApcState; struct { UCHAR SavedApcStateFill[KAPC_STATE_ACTUAL_LENGTH]; CCHAR FreezeCount; CCHAR SuspendCount; UCHAR UserIdealProcessor; UCHAR CalloutActive; }; }; PVOID Win32Thread; PVOID StackBase; union { KAPC SuspendApc; struct { UCHAR SuspendApcFill0[KAPC_OFFSET_TO_SPARE_BYTE0]; SCHAR Quantum; }; struct { UCHAR SuspendApcFill1[KAPC_OFFSET_TO_SPARE_BYTE1]; UCHAR QuantumReset; }; struct { UCHAR SuspendApcFill2[KAPC_OFFSET_TO_SPARE_LONG]; ULONG KernelTime; }; struct { UCHAR SuspendApcFill3[KAPC_OFFSET_TO_SYSTEMARGUMENT1]; PVOID TlsArray; }; struct { UCHAR SuspendApcFill4[KAPC_OFFSET_TO_SYSTEMARGUMENT2]; PVOID BBTData; }; struct { UCHAR SuspendApcFill5[KAPC_ACTUAL_LENGTH]; UCHAR PowerState; ULONG UserTime; }; }; union { KSEMAPHORE SuspendSemaphore; struct { UCHAR SuspendSemaphorefill[KSEMAPHORE_ACTUAL_LENGTH]; ULONG SListFaultCount; }; }; LIST_ENTRY ThreadListEntry; PVOID SListFaultAddress; } KTHREAD, *PKTHREAD, *PRKTHREAD;
接下来我们来一条一条地学习这个数据结构,并尽我的最大的能力给出扩展延伸的知识。
由于在windows中,线程是系统处理器调度的"基本单元",而且线程调度是在内核层完成的,所以,KTHREAD的许多域都跟windows的线程调度机制有关。我们在学习的过程中要时刻集合
windows的线程调度机制来进行理解。
1. DISPATCHER_HEADER Header
Header域是一个DISPATCHER_HEADER结构,说明了内核层的线程对象"也"是一个分发器对象,这里之所以用"也",是因为KPROCESS的第一个成员域同样是一个DISPATCHER_HEADER结构体对象,这说明线程也可以被等待,当被等待的线程结束时,之前等待在该对象上的"等待"被满足,触发信号signal,使阻塞等待解除,这样就达到了同步的效果。
typedef struct _DISPATCHER_HEADER { union { struct { UCHAR Type; union { UCHAR Abandoned; UCHAR Absolute; UCHAR NpxIrql; UCHAR Signalling; }; union { UCHAR Size; UCHAR Hand; }; union { UCHAR Inserted; UCHAR DebugActive; UCHAR DpcActive; }; }; LONG Lock; }; LONG SignalState; LIST_ENTRY WaitListHead; } DISPATCHER_HEADER, *PDISPATCHER_HEADER;
2. LIST_ENTRY MutantListHead
MutantListHead域指向一个链表头,该链表中包含了所有属于"该线程"的"突变体对象(mutan,对应于API中的互斥体mutex对象)"。由于突变体对象是有"所有权"的,一旦被某个线程等到(暂时获得),则其所有权归该线程所有,它也被连接到MutanListHead链表中
3. 内核栈的维护
PVOID InitialStack;
PVOID StackLimit;
PVOID KernelStack;
PVOID StackBase
注意,内核栈是从高地址向低地址方向变化的.
InitialStack: 记录了原始的栈位置(高地址)
StackLimit: 记录了栈的低地址
KernelStack: 记录了真正内核调用栈的开始位置
由于在内核栈的"顶部区域"还记录了浮点处理器保存区和一个异常陷阱帧,所以,KernelStack的位置比InitialStack要低一些(内核栈从高往低发展): KTRAP_FRAME_LENGTH + sizeof(FX_SAVE_AREA)
StackBase: 指向了原始的内核栈高地址
在线程初始化的时候,StackBase和InitialStack是相等的,都指向原始的内核栈高地址。
4. KSPIN_LOCK ThreadLock
ThreadLock域是一个自旋锁,用于保护线程数据成员。
typedef struct { volatile unsigned int lock; } spinlock_t;
5. KAPC_STATE ApcState
ApcState域是一个结构成员,指定了一个线程的APC(Asynchronous Procedure Call)信息,包括APC链表、是否正在处理APC或者是否有内核APC或用户APC正在等待信息。
typedef struct _KAPC_STATE { LIST_ENTRY ApcListHead[2]; PKPROCESS Process; //指向当前线程所属的进程的KPROCESS结构 UCHAR KernelApcInProgress; UCHAR KernelApcPending; UCHAR UserApcPending; } KAPC_STATE, *PKAPC_STATE;
ApcState所在的是一个union,除了ApcState之外,还有其他的结构
union { KAPC_STATE ApcState; struct { UCHAR ApcStateFill[KAPC_STATE_ACTUAL_LENGTH]; BOOLEAN ApcQueueable; volatile UCHAR NextProcessor; volatile UCHAR DeferredProcessor; UCHAR AdjustReason; SCHAR AdjustIncrement; }; };
ApcQueueable: 指示是否可以插入APC
NextProcessor、DeferredProcessor: 关于处理器调度的选择
AdjustReason: 优先级调整原因
AdjustIncrement: 优先级调整的调整量(调整多少,加还是减)
6. KSPIN_LOCK ApcQueueLock
ApcQueueLock域是一个自旋锁,用于保护APC队列的操作
7. ULONG ContextSwitches
ContextSwitches域记录了该线程进行了多少次环境切换
8. UCHAR State
State域反映了该线程当前的状态,关于线程的状态,在 base\ntos\inc\ke.h 中的宏定义 KTHREAD_STATE
typedef enum _KTHREAD_STATE { Initialized, Ready, Running, Standby, Terminated, Waiting, Transition, DeferredReady, GateWait } KTHREAD_STATE;
可以看到,这就是我们在操作系统课上学的线程的状态转义的相关知识,这涉及到线程调度算法。这里扩展一下学习:
每个线程从初始化开始一直到最后终止,其状态既可能随着系统状态而变化,也可能随自身的代码逻辑而发生变化,并非总是一直执行下去直到结束,这是一个动态的过程。
1) 已初始化(Initialized): 说明一个线程对象的内部状态已经初始化,这是线程创建过程中的一个内部状态,此时线程尚未加入到进程的链表中(EPROCESS->ThreadListHead、
KPROCESS->ThreadListHead),也没有启动 2) 就绪(Ready): 代表该线程已经准备就绪,等待被调度执行。当"线程调度器"选择一个线程来执行时,它只考虑处于"就绪"状态的线程。所以,我们要让一个线程执行,就必须将这个线程的
状态设置为Ready,这个线程才会被加入某个处理器的就绪线程链表中(这里之所以说某个是因为线程被哪个处理器执行和这个线程的"亲和性affinity" KTHREAD->affinity
有关) 3) 运行(Running): 线程正在运行。该线程一直占有处理器,直到分到的时限结束,或者被一个更高优先级的线程抢占,或者线程终止,或者主动放弃处理器执行权,或者进入等待状态(正常情况
下,每个线程的运行时间为20ms,这个数值不是绝对的,根据线程优先级,运行情况会上下波动)。 4) 备用(Standby): 处于备用状态的线程已经被选中作为某个处理器上下一个要运行的线程。对于系统中的每个处理器,只能有一个线程可以处于备用状态。然而,一个处于备用状态的线程在
真正被执行以前,有可能被更高优先级的线程抢占这个备用的"位置"(线程的优先级是不不断动态变化的) 5) 已终止(Terminated): 表示线程已经完成任务,正在进行资源回收。KeTerminatedThread()函数用于设置此状态 6) 等待(Waiting): 表示一个线程正在等待某个条件,比如等待一个分发起对象变成有信号状态(signaled)。也可以等待多个对象,当等待的条件满足时,线程或者立即开始运行,或者回到就
绪状态 7) 转移(Transition): 处于转移状态的线程已经准备好运行,但是它的内核栈不再内存中。一旦它的内核被换入内存,则该线程进入就绪状态 8) 延迟的就绪(DeferredReady): 处于延迟的就绪状态的线程也已经准备好可以运行了,但是,与就绪状态不同的是,它尚未确定在哪个处理器上运行。当有机会被调度时,或者直接转入备用
状态,或者转到就绪状态。因此,此状态是为了多处理器而引入的,对于单处理器系统没有意义 9) 门等待(GateWait): 线程正在等待一个门对象。此状态与等待状态类似,只不过它是专门针对门对象而涉及的
回到我们的主线上来,State域反映了该线程当前的状态
9. UCHAR NpxState
NpxState域反映了浮点处理器的状态
10. BOOLEAN Alertable
Alertable域说明了一个线程是否可以被唤醒,当一个线程正在等待时,如果它的Alertable值为TRUE,则它是可以被唤醒的。
Alertable是一个布尔值,TRUE值表示这个线程马上要调用一个内核等待函数,它的用途是,在发出了一个信号(比如释放一个信号量对象)以后,接下来该线程会马上调用等待函数,所以,它不必解除线程调度器锁。
11. IRQL
UCHAR WaitIrql;
BOOLEAN WaitNext;
WaitIrql: 记录了原先的IRQL值
WaitNext: 记录了下一个I/O请求的IRQL的值
12. UCHAR WaitReason
WaitReason域记录了一个线程等待的理由,在 base\ntos\inc\ke.h 中有关于这个值的枚举宏定义
typedef enum _KWAIT_REASON { Executive, FreePage, PageIn, PoolAllocation, DelayExecution, Suspended, UserRequest, WrExecutive, WrFreePage, WrPageIn, WrPoolAllocation, WrDelayExecution, WrSuspended, WrUserRequest, WrEventPair, WrQueue, WrLpcReceive, WrLpcReply, WrVirtualMemory, WrPageOut, WrRendezvous, Spare2, Spare3, Spare4, Spare5, Spare6, WrKernel, WrResource, WrPushLock, WrMutex, WrQuantumEnd, WrDispatchInt, WrPreempted, WrYieldExecution, WrFastMutex, WrGuardedMutex, WrRundown, MaximumWaitReason } KWAIT_REASON;
WaitReason基本上只是记录了等待的理由,而并不参与到线程调度或决策中。
13. CHAR WaitMode
WaitMode域记录了当前线程等待时的处理器模式,即内核模式或用户模式的等待。
NTSTATUS KeWaitForSingleObject( IN PVOID Object, IN KWAIT_REASON WaitReason, IN KPROCESSOR_MODE WaitMode, //指定等待的模式(用户/内核) IN BOOLEAN Alertable, IN PLARGE_INTEGER Timeout OPTIONAL );
14. LONG_PTR WaitStatus
WaitStatus域记录了等待的结果状态
15. PKWAIT_BLOCK WaitBlockList
WaitBlockList成员指向一个以KWAIT_BLOCK为元素的链表,其中的KWAIT_BLOCK对象指明了哪个线程在等待哪个分发器对象。对于一个线程而言:
1) WaitBlockList域以及每个KWAIT_BLOCK对象中的WaitBlockList域构成了一个双链表,指明了该线程正在等待哪些分发器对象。 2) 而对于每个分发器对象而言,它又有另一个KWAIT_BLOCK链表指明了哪些线程正在等待它(二维交叉关系)
typedef struct _KWAIT_BLOCK { LIST_ENTRY WaitListEntry; //该线程等待的分发器对象 PKTHREAD Thread; //哪些线程在等待这个分发器 PVOID Object; PKWAIT_BLOCK NextWaitBlock; WORD WaitKey; UCHAR WaitType; UCHAR SpareByte; } KWAIT_BLOCK, *PKWAIT_BLOCK;
关于线程间调度、同步、分发器的知识请参阅《windows 内核原理与实现》5.4 节
16. PKGATE GateObject
GateObject域记录了正在等待的"门对象(Gate Object,也是一种分发器对象)"。关于门对象和门等待,我们这里拓展一下。
首先要明白,门等待属于线程调度的同步机制的知识范畴,基于线程调度的同步机制有:
1) 线程进入等待: 通过等待"同步对象"或"分发器对象"来进行线程的的调度 2) 事件 3) 突变体 4) 信号量 5) 定时器 6) 推锁 7) 执行体资源
这些都是windows的等待机制以及各种分发器对象。显然,这一套机制足够灵活,可以满足内核和应用程序的同步、异步程序设计的需要。"除此之外",windows还提供了一套更加"轻量"的等待机制: 门等待机制。
我们要明白,门等待机制也是线程同步机制的范畴里的一种而已,只不是是一个更轻量级的机制,我们在学习"门等待"机制的时候要学会把它和其他的线程间同步机制进行类比,加深理解。
门等待是对windows标准等待机制的一个简化,它避免了一个线程从进入等待函数到离开函数过程中的许多步骤。唤醒一个处于"门等待"的线程的方式很简单,只要调用KiDeferredReadyThread()函数将该线程分发到某个处理器上准备执行。
门等待中的分发器对象是KGATE,它只有一个DISPATCHER_HEADER结构成员
typedef struct _KGATE { DISPATCHER_HEADER Header; } KGATE, *PKGATE;
它支持多个线程同时等待一个KGATE对象,每次有信号状态时只唤醒"一个"线程,所以,它不是通知类型的分发器对象,而是同步类型的分发器对象。另外,等待门对象的线程也不会等待多个分发器对象,也没有超时设置,所以,线程进入门等待时的处理也相对简单高效很多。
因此,处于门等待状态的线程一旦它所等待的门对象变成有信号状态,则可以以最快的速度被调度和运行。这也使得门等待比普通的等待要高效一些,在内核中用于一些性能敏感的同步任务。
回到我们的主线上来,GateObject域记录了正在等待的"门对象
17. SCHAR Priority
Priority域包含了该线程的优先级值,这是指它的动态优先级,即在线程执行过程中可能由于某些原因而调整过的优先级。
(请结合学习笔记(2)中的EPROCESS->PriorityClass,以及学习笔记(3)中的KPROCESS->BasePriority 中的关于线程/进程运行中的优先级动态变化的相关知识)
18. UCHAR BasePriority
BasePriority域是线程的静态优先级,其初始值是所属进程的BasePriority值(KPROCESS->BasePriority),以后可以通过KeSetBasePriorityThread()函数重新设定
关于优先级的动态调整,有一点要注意: 1) 0~15是"普通线程"的优先级 2) 16~31是"实时线程"的优先级 3) 无论线程的优先级怎么调整,它们的值不会跨过这个区域,即普通线程的优先级不能超过15,实时线程的优先级不能低于16(高富帅永远是高富帅,屌丝永远是屌丝)
19. UCHAR EnableStackSwap
EnableStackSwap域是一个布尔值,说明本线程的内核栈是否允许被换出到外存的swap内存映射文件中。
20. volatile UCHAR SwapBusy
SwapBusy域也是一个布尔值,指定了本线程当前是否正在进行上下文环境切换(context swap),其用法是,在将执行环境切换到其他线程(线程上下文切换,即线程调度)"以前"设置SwapBusy域为TRUE,切换完成以后再设置回FALSE。
21. BOOLEAN Alerted[MaximumMode]
Alerted域是一个数组,指定了该线程在每一种警告模式下是否可以被唤醒,所谓的警告模式指的也是"内核模式"和"用户模式",所以,这个数组的含义是指该线程分别在内核模式和用户模式下是否可以被唤醒。
(回想之前学习的WaitMode域)
22. 线程处于等待状态
union
{
LIST_ENTRY WaitListEntry;
SINGLE_LIST_ENTRY SwapListEntry;
};
1) WaitListEntry: 它是一个双链表节点,当一个线程正在等待"被执行"时,WaitListEntry作为一个线程节点加入到一个链表中,结合我们之前学习KPROCESS时的KPROCESS->ReadyListHead。
我们知道,在进程被换入内存过程中,就绪状态的线程将被加入到以进程的ReadyListHead域为链表头的双链表中,链表中的节点即为线程的WaitListEntry域。
(请参考学习笔记(3))
2) SwapListEntry: 它是一个单链表节点,它被用于当线程的"内核栈"需要被换入时,插入到以全局变量KiStackInSwapListHead为链表头的单链表中。另外,当一个线程处于
DeferredReady(延迟的就绪)状态时,其SwapListEntry将被插入到某个处理器的DeferredReadyListHead链表中。
23. PRKQUEUE Queue
Queue域是一个队列分发器对象,如果不为NULL,则表示当前进程正在处理此队列对象中的项
24. ULONG WaitTime
WaitTime域记录了一个线程进入等待时刻的时间点(时钟滴答值的低32位),主要用于"平衡集管理器"根据一个线程的等待时间的先后来做一些决策,这里涉及到一些线程调度的知识,我们扩展一下。
平衡集管理器 平衡集管理器(balance set manager)是在系统初始化时创建的。它是一个系统进程,其优先级为16,最低的"实时优先级",因此比普通的非实时线程的优先级高。平衡集管理器的实际用途是
维持系统内存资源的平衡,当多个进程发生资源竞争时,平衡集管理器负责从系统全局来调度"可用的物理内存"以及调整每个进程的"工作集"。 平衡集是指所有具备资源分配资格的进程的集合,当然也包括System进程。当系统内存紧缺时,它一方面把拥有较多内存的进程从它们的工作集中换出一些页面,另一方面把不满足运行条件的
进程排除在平衡集外(即不再给它分配内存)。这些被排除在平衡集以外的进程,只有当其中的线程满足运行条件并且系统有了足够空闲内存时,才会被重新加入到平衡集中。这就是平衡集的
基础概念。
我们现在知道了,"平衡集管理器"在对线程进行调度的时候的确需要使用到"线程已等待的累积时间"这一参数,那它到底是怎么来调度的呢?这里面的调度算法又是怎样的呢?
我们首先看一下如何评价一个调度算法。衡量调度算法的准则包括多个方面
1) 公平性: 调度算法在选择下一个运行的线程时,要考虑到同等地位的线程必须拥有相同的机会获得处理器执行权 2) CPU的有效利用,只要有进程或线程还在等待执行(就绪态),就不能让CPU空闲着 3) 不同类型的操作系统对调度算法会有不同的需求: 实时操作系统对响应时间有"最低要求",有时候在"吞吐量最大化"和"最小响应时间时间"之间可能无法兼顾
(我们要牢记这些基本思想,因为我们会发现,不管操作系统的调度算法怎么变,它的核心思想就是从这3条里衍生而来的)
从大的分类上来说,调度算法可以分为"非抢占式"和抢占式(这只是个大分类,实际的操作系统调度算法中并没有这两种名字的算法)。而是从这两种算法的衍生出来的算法。
1) 先到先服务算法 在"非抢占式算法"中,这一算法比较简单易懂,用FIFO队列来实现排队,此算法简单,易于实现,但是,如果每个线程执行任务单元所需要的时间长短不一的话,则算法的实际效果可能非常
不"公平"(不符合我们的基本原则,所以在windows中并没有采用,或者说没有完全采用这种算法) 2) 时间片轮转调度算法 顾名思义,处理器的时间被分成了最大长度不超过某个值的时间片段(一般是20ms),称为"时间片",然后,用轮转方法分配给每一个线程。当一个线程获得了CPU处理的执行权之后,按照自身的
逻辑执行下去,直到时间片用完,或者自己主动放弃执行权(比如要等待一下信号量)。这个时候,线程把控制权又交还给系统,系统在获得了处理器的控制权之后,用"轮转"的方法找到
下一个"正在等待运行(就绪态)"的线程(CPU总是去调度就绪队列中的线程),让它继续执行。这种策略虽然存在上下文的切换开销,但是却非常有效和灵活,windows中的调度算法虽然不是完全
采用了这种策略,但是也汲取了很多的思想从中 3) 优先级调度算法 我们思考时间片轮转法的一个缺点,即它的轮转时间是同等的,即我们假设所有的线程都同等重要,但实际这是不可能的,所以,优先级调度算法是对时间片轮转调度算法的一种改进(注意,
是改进,不是推翻,在优先级调度算法中也有时间片的思想),每个线程都有一个优先级值,高优先级的线程总是优先被考虑在CPU上执行。操作系统在管理线程时,可以让每个优先级(0~31)
用一个"队列"来存放所有满足执行条件的线程,这样,当一个线程用完了它的时间片或者自动放弃处理器的执行权(等待一个信号量,等待一个锁等)时,线程会交出CPU的控制权,系统在获得
了CPU的控制权后,会选择一个优先级"最高"的线程作为下一个要运行的线程。 每一个线程在优先级队列中的位置是由它的优先级来决定的,同等优先级的线程使用"轮转"或"先到先服务"的策略,这也体现了技术的兼容和进步
那说了这三种,有一个问题了,windows中到底采用了是什么调度策略?前面说的记录当前线程已等待时间的信息用在哪里呢?接下来就引出了最后一个知识点:
windows的调度算法是一个"抢占式"的、支持"多处理器"的"优先级调度算法",可以说是集大成者,几乎综合了前面所说的三种技术的优点。
windows为每个处理器定义了一个链表数组,相同优先级的线程挂在同一个链表中(这就相当于优先级队列),不同优先级的线程分别属于不同的链表。当一个线程满足了执行条件时,它首先被挂到当前处理器的一个待分配的链表(称为延迟的就绪链表),也就是所谓的延迟的就绪(DeferredReady)态,然后调度器会在适当的时候(当它获得了某个CPU控制权时)把"待分配"链表上的这个线程分配到某个处理器(这个线程获得的处理器)对应的优先级的线程链表中。当这个处理器(刚刚被挂载了新线程的处理器)在选择下一个要运行的线程时,会根据"优先级准则"选择此线程(如果没有同等或更高优先级的线程也在等待的话,如果有更高优先级的线程,则可以立刻抢占,体现了抢占式的思想)。
另外,windows中的线程优先级还有一个"静态优先级(KTHREAD->BasePriority)"和"动态优先级(KTHREAD->Priority)"的概念,所谓动态优先级是在静态优先级的"基础上"根据某些特定的条件提升或降低线程的优先级,
windows系统中的真实调度应该是按照动态优先级来进行的,这点一定要注意,
windows中的线程优先级调整也考虑到了很多因素:
1) 前台线程(轻微提升) 2) 等待I/O完成后的线程(轻微提升) 3) 当前线程已等待时间(随等待时间增加而轻微提升优先级) 4) 当前线程已运行时间(随运行时间增加了轻微降低优先级)
回到我们的主线上来,WaitTime域记录了一个线程进入等待时刻的时间点(我们已经知道了它存在的目的)
25. PVOID Teb
Teb域是一个特殊的域,它指向进程地址空间的一个TEB(线程环境块)结构。TEB结构包含了在用户地址空间中需要访问的各种信息,例如与线程相关的GDI信息、系统支持的异常(SEH)、甚至还有WinSock的信息等等,关于TEB的详细学习,我们将放到之后专门专题学习,这里暂且不表
26. 线程的定时器相关
union { KTIMER Timer; struct { UCHAR TimerFill[KTIMER_ACTUAL_LENGTH]; union { struct { LONG AutoAlignment : 1; LONG DisableBoost : 1; LONG ReservedFlags : 30; }; LONG ThreadFlags; }; }; };
1) Timer域是一个附在一个线程上的定时器,当一个线程在执行过程中需要定时器时,比如实现可超时的等待函数(KeWaitForSingleObject、KeWaitForMultipleObjects)
NTSTATUS KeWaitForMultipleObjects(
IN ULONG Count,
IN PVOID Object[],
IN WAIT_TYPE WaitType,
IN KWAIT_REASON WaitReason,
IN KPROCESSOR_MODE WaitMode,
IN BOOLEAN Alertable,
IN PLARGE_INTEGER Timeout OPTIONAL,
IN PKWAIT_BLOCK WaitBlockArray OPTIONAL
);
NTSTATUS KeWaitForSingleObject(
IN PVOID Object,
IN KWAIT_REASON WaitReason,
IN KPROCESSOR_MODE WaitMode,
IN BOOLEAN Alertable,
IN PLARGE_INTEGER Timeout OPTIONAL
);
就会用到此KTHREAD中的这个定时器对象。
2) AutoAlignment 和 DisableBoost 这两个域直接继承自所属进程的同名标记(请参考学习笔记(3)中关于KPROCESS->AutoAlignment, KPROCESS->DisableBoost)
27. 线程的等待定时器对象(分发器对象)
union { KWAIT_BLOCK WaitBlock[THREAD_WAIT_OBJECTS + 1]; struct { UCHAR WaitBlockFill0[KWAIT_BLOCK_OFFSET_TO_BYTE0]; BOOLEAN SystemAffinityActive; }; struct { UCHAR WaitBlockFill1[KWAIT_BLOCK_OFFSET_TO_BYTE1]; CCHAR PreviousMode; }; struct { UCHAR WaitBlockFill2[KWAIT_BLOCK_OFFSET_TO_BYTE2]; UCHAR ResourceIndex; }; struct { UCHAR WaitBlockFill3[KWAIT_BLOCK_OFFSET_TO_BYTE3]; UCHAR LargeStack; }; };
1) WaitBlock域是一个包含4个KWAIT_BLOCK成员的数组,它们表示可等待的定时器对象。前面介绍WaitBlockList域成员的时候说到,KWAIT_BLOCK结构代表了一个线程正在等待一个"分发器对象",或者说是一个分发器对象正在被一个线程等待,它会被同时加入到两个双链表结构中。
WaitBlock域是一个内置的数组,内核在实现等待功能的时候:
1. 如果一个线程所等待的对象数量小于4(即3个分发器对象加上一个定时器对象),则内核无需另外分KWAIT_BLOCK对象内存(即不需要加入到那个双链表中),只需直接使用WaitBlock中的数组
成员即可。 2. 而如果等待的对象数量大于4,则内核必需分配额外的KWAIT_BLOCK对象内存。
由于等待操作在内核中非常频繁,所以,利用静态数组来满足大多数情况下的内存需要,这一优化非常有意义(这也体现了以空间换时间的基本思想)
28. LIST_ENTRY QueueListEntry
QueueListEntry域记录了线程在处理一个队列项时加入到队列对象的线程链表中的节点地址
29. PKTRAP_FRAME TrapFrame
TrapFrame域是线程中很关键的一个成员域,在windows中,线程是系统调度的基础,它代表了一个进程中的一个"控制流"。当一个线程离开运行状态时,其当前的执行状态,比如现在的指令指针(EIP)在哪里,各个寄存器的值(EAX,EBX,..ELAGS)是什么,等等,都必须保存下来,以便下次再"轮"到这个线程运行时(再次获得CPU执行权),可以恢复原来的执行状态。
TrapFrame域记录了"控制流状态"的数据结构,它是一个指向KTRAP_FRAME类型的指针, 在 base\ntos\inc\i386.h 中
typedef struct _KTRAP_FRAME { ULONG DbgEbp; // Copy of User EBP set up so KB will work. ULONG DbgEip; // EIP of caller to system call, again, for KB. ULONG DbgArgMark; // Marker to show no args here. ULONG DbgArgPointer; // Pointer to the actual args ULONG TempSegCs; ULONG TempEsp; ULONG Dr0; ULONG Dr1; ULONG Dr2; ULONG Dr3; ULONG Dr6; ULONG Dr7; ULONG SegGs; ULONG SegEs; ULONG SegDs; ULONG Edx; ULONG Ecx; ULONG Eax; ULONG PreviousPreviousMode; PEXCEPTION_REGISTRATION_RECORD ExceptionList; ULONG SegFs; ULONG Edi; ULONG Esi; ULONG Ebx; ULONG Ebp; ULONG ErrCode; ULONG Eip; ULONG SegCs; ULONG EFlags; ULONG HardwareEsp; // WARNING - segSS:esp are only here for stacks ULONG HardwareSegSs; // that involve a ring transition. ULONG V86Es; // these will be present for all transitions from ULONG V86Ds; // V86 mode ULONG V86Fs; ULONG V86Gs; } KTRAP_FRAME;
它包含了Intel x86的所有常用的寄存器
30. PVOID CallbackStack
CallbackStack域包含了线程的回调(callback)栈地址,此栈在该线程从"内核模式调用"到"用户模式"时使用(即从内核模式返回到用户模式的线程时触发)。
31. PVOID ServiceTable
ServiceTable域指向该线程使用的"系统服务表(全局变量KeServiceDescriptorTable)",如果这是一个图形用户界面(GUI)线程,此域将指向另一个"影子系统服务表(全局变量KeServieceDescriptorTableShadow)"。这里涉及到windows的系统服务相关的知识,我们拓展延伸一下。
windows系统服务 在windows的系统结构中,内核提供的服务都通过ntdll.dll模块被应用程序提供。windows应用程序调用一组系统DLL(kernel32.dll, user.dll等)中的API函数,"间接"地通过ntdll.dll
中的存根函数来调用内核提供的系统服务。例如,NtCreateFile是windows内核中的"创建文件"服务例程,它运行在处理器的内核模式下,而应用程序的代码运行在用户模式下,所以,应用程序
为了调用"创建文件"服务,必须将处理器从用户模式切换到内核模式下。
当然,模式切换工作不需要由应用程序自己来完成,windows提供了一个系统模块ntdll.dll,已经实现了所有系统服务的模式切换工作。为了说明这个概念,我们来看几张图:
使用VC++6的Dependency可以查看kernel32.dll的导出表。可以看到我们常见的win32调用函数。
可以看到,ntdll.dll完成了所谓的"穿越"任务,从用户模式"穿越"到内核模式。Intel x86处理器提供了4种处理器执行模式(硬件),Ring0~Ring3,windows仅使用两种模式: Ring0(内核模式)和Ring3(用户模式)。
另外,Intel x86处理器还提供了"门(gate)"机制允许可执行代码从Ring3进入Ring0模式,以及从Ring0返回到Ring3模式。注意,我们所说的"模式穿越"就是指的这个"门机制",这些概念要重点记忆。
Intel x86支持4种"门描述符":
1) 调用门(call gate) 2) 陷阱门(trap gate) //当系统发生异常时,也会进行模式切换(请参考《深入理解计算机系统(原书第2版)》8.1节 异常的相关知识 ) 3) 中断门(iinterrupt gate) //windows使用中断门来实现"模式切换",其中断号为int 2e(请参考学习笔记(3)关于IDT的知识) 4) 任务门(task gate)
回到我们最开始的问题,我们讲到"系统服务表(全局变量KeServiceDescriptorTable)"的概念,那系统服务表是什么呢?
我们知道在ntdll.dll中的系统服务存根函数指定了一个系统服务号,比如NtCreateFile的系统服务号为27H。在内核模式下,_KiSystemServiceRepeat根据此系统服务号,就知道该调用内核中的哪个系统服务例程(是不是和IDT的思想很想,我们可以类比起来),以及从用户栈拷贝多少数据到内核栈中。这一过程被称为"系统服务分发(System Service Dispatching)"
windows实现系统服务分发的关键是一个称为"SDT(Service Descriptor Table) 服务描述表"的表(windows中的这类表的名词,要注意总结和区分,也有叫SSDT的,都是一个东西)。windows支持多个SDT,ntdll.dll中过的系统存根函数指定的"系统服务号",即eax寄存器的值,包含了表的索引和表内的服务索引。
我们说windows支持多个SDT,是因为内核全局变量 KeServiceDescriptorTable是一个数组(这个全局变量指向这个数组的基地址),数组的每个元素指向一个SDT:
kd> dd KeServiceDescriptorTable 80553fa0 80502b8c 00000000 0000011c 80503000 80553fb0 00000000 00000000 00000000 00000000 80553fc0 00000000 00000000 00000000 00000000 80553fd0 00000000 00000000 00000000 00000000 80553fe0 00002710 bf80c0b6 00000000 00000000 80553ff0 f8172a80 f7d81b60 82814328 806e2f40 80554000 00000000 00000000 46aeb095 000000ec 80554010 11f26a55 01ceefe0 00000000 00000000
这是用winDbg对XP进行dump得到的数据。这个数据要这么理解,因为它是对内存的直接dump,所以我们把要每行看成一个整体,即KeServiceDescriptorTable这个数组的每个成员都是一个结构体
这里面每个地址都对应着一个SDT(KSERVICE_TABLE_DESCRIPTOR_TABLES数据结构)
typedef struct _KSERVICE_TABLE_DESCRIPTOR_TABLES { PULONG_PTR Base; //System Service Dispatch Table(SDT) 这个SDT的基地址。 PULONG Count; // array of usage counters ULONG Limit; // number of table entries PUCHAR Number; // array of byte counts } KSERVICE_TABLE_DESCRIPTOR_TABLES, *PKSERVICE_TABLE_DESCRIPTOR_TABLES;
SDT结构并没有保存"系统服务描述表"的真实内容,知识保存了指针,在第一个成员域中保存了指向一个地址数组,每4个字节为单位,即Base指向的内存空间每4个字节代表一个"服务例程"的入口地址(或者说是函数入口地址)。我们使用winDbg来dump验证一下这个概念:
所以,总结一下:
1. 系统根据系统服务号,首先定位到以KeServiceDescriptorTable为基址的数组中的某个元素: KeServiceDescriptorTable[index] 2. 得到某个SDT(KSERVICE_TABLE_DESCRIPTOR_TABLES)后,然后寻址到这个SDT表的基地址(注意,这是一个表哦,即这个SDT表中还有很多的项) 3. 在SDT表中根据系统服务号进行"系统服务例程"的定位。例如: 服务号为0x27说明NtCreateFile服务在KeServiceDescriptorTable数组第0个表的第39个项
Ps: 关于系统服务的概念,我在学习的时候,发现有很多种名词,从资料上来看,它们都是指同一个东西,我这里写的名词是按照WRK上来的。
1. SDT和SSDT指的是同一个东西,它们都是系统服务描述符表 2. 全局变量KeServiceDescriptorTable的数组成员结构: _SERVICE_DESCRIPTOR_TABLE 和 KSERVICE_TABLE_DESCRIPTOR_TABLES指的都是同一个东西,它们的结构成员都是4个字段,
意义是一样,不知道为啥会出现这种现象
好,回到我们的主线上来,ServiceTable域指向该线程使用的"系统服务表(全局变量KeServiceDescriptorTable)"
32. UCHAR IdealProcessor
IdealProcessor域指明了多处理器的机器上该线程的理想处理器。这个线程的处理器"亲和性"概念
33. BOOLEAN Preempted
Preempted域是一个布尔值,说明这个线程是否被高优先级的线程抢占了,只有当一个线程正在运行或者正在等待运行而被高优先级线程抢占的时候,此值才会被设置成TRUE。在其他情况下,该值总是为FALSE/
34. BOOLEAN ProcessReadyQueue
ProcessReadyQueue域也是一个布尔值,说明一个线程是否在所属的进程KPROCESS对象的ReadyListHead链表中,TRUE表示在此链表中,FALSE表示不在。
(请参考学习笔记(3)中关于ReadyListHead的说明)
35. BOOLEAN KernelStackResident
KernelStackResident域也是一个布尔值,说明该线程的内核栈是否驻留在内存中,当内核栈被换出内存时,该值被置成FALSE,该内核栈被换入内存时,该值被置成TRUE。
36. KAFFINITY Affinity
Affinity域指定了线程的处理器亲和性,此值初始时继承自进程对象的Affinity值。为线程指定的处理器集合必须是其进程的亲和性处理器集合的子集。
在线程执行过程中,其Affinity值可能有两种设置:
1. 一是系统亲和性,当该线程执行系统任务时通过KeSetSystemAffinityThread()函数来设置 2. 二是线程本身的亲和性,称为用户亲和性,通过KeRevertToUserAffinityThread()函数来设置
37. KAFFINITY UserAffinity
UserAffinity域是"线程的用户亲和性",此值初始时也继承子进程对象的Affinity值,以后可通过内核函数KeSetAffinityThread改变。
38. PKPROCESS Process
Process域指向线程的所属的进程对象(KPROCESS),在线程初始化时指定,在此总结一下线程和进程的关系
1) EPROCESS->ThreadListHead: 指向当前进程所属线程的ETHREAD链表 2) KPROCESS->ThreadListHead: 指向当前进程所属线程的KTHREAD链表 3) ETHREAD->ThreadsProcess: 指向当前线程所属的进程的EPROCESS链表 4) KTHREAD->Process: 指向当前线程所属的进程的KPROCESS链表
39.APC相关
PKAPC_STATE ApcStatePointer[2] UCHAR ApcStateIndex
union
{
KAPC_STATE SavedApcState;
struct
{
UCHAR SavedApcStateFill[KAPC_STATE_ACTUAL_LENGTH];
CCHAR FreezeCount;
CCHAR SuspendCount;
UCHAR UserIdealProcessor;
UCHAR CalloutActive;
};
};
union
{
KAPC_STATE ApcState;
struct
{
UCHAR ApcStateFill[KAPC_STATE_ACTUAL_LENGTH];
BOOLEAN ApcQueueable;
volatile UCHAR NextProcessor;
volatile UCHAR DeferredProcessor;
UCHAR AdjustReason;
SCHAR AdjustIncrement;
};
};
1) ApcStateIndex域是一个索引值,它指明了当前APC状态在ApcStatePointer域中的索引。由于ApcStatePointer是一个只有两个元素的数组,所以,ApcStateIndex的值为0或1.
2) ApcStatePointer数组元素的类型是指向KAPC_STATE的指针:
typedef struct _KAPC_STATE { LIST_ENTRY ApcListHead[2]; PKPROCESS Process; UCHAR KernelApcInProgress; UCHAR KernelApcPending; UCHAR UserApcPending; } KAPC_STATE, *PKAPC_STATE;
ApcStatePointer[0]: 指向ApcState
ApcStatePointer[1]: 指向SavedApcState
(关于APC的相关知识请参考学习笔记(4))
40. PVOID Win32Thread
Win32Thread域是一个指针,指向由windows子系统管理的区域。
41. 和APC相关的线程间同步
union { KAPC SuspendApc; struct { UCHAR SuspendApcFill0[KAPC_OFFSET_TO_SPARE_BYTE0]; SCHAR Quantum; }; struct { UCHAR SuspendApcFill1[KAPC_OFFSET_TO_SPARE_BYTE1]; UCHAR QuantumReset; }; struct { UCHAR SuspendApcFill2[KAPC_OFFSET_TO_SPARE_LONG]; ULONG KernelTime; }; struct { UCHAR SuspendApcFill3[KAPC_OFFSET_TO_SYSTEMARGUMENT1]; PVOID TlsArray; }; struct { UCHAR SuspendApcFill4[KAPC_OFFSET_TO_SYSTEMARGUMENT2]; PVOID BBTData; }; struct { UCHAR SuspendApcFill5[KAPC_ACTUAL_LENGTH]; UCHAR PowerState; ULONG UserTime; }; }; union { KSEMAPHORE SuspendSemaphore; struct { UCHAR SuspendSemaphorefill[KSEMAPHORE_ACTUAL_LENGTH]; ULONG SListFaultCount; }; };
SuspendApc这个域被初始化成一个专门的APC。当该APC被插入并交付时,KiSuspendThread函数被执行,其执行结果是在线程的SuspendSemaphore信号量上等待,直到该信号量对象有信号,然后线程被唤醒并继续执行。
线程的挂起(suspend)操作正是通过这一机制来实现的。自然的,线程的恢复(resume)操作则是通过控制SyspendSemaphore信号量的计数来实现的。
(关于线程的挂起和恢复请参阅 base\ntos\ke\thredobj.c 中的KeSuspendThread和KeFreezeAllThreads函数)
42. LIST_ENTRY ThreadListEntry
ThreadListEntry域代表了一个双链表上的节点,当一个线程被创建时,它会被加入到进程对象的ThreadListHead链表中,回想前面学习KPROCESS->ThreadListHead域的相关知识。
43. PVOID SListFaultAddress
SListFaultAddress域与用户模式互锁单链表POP操作(KeUserPopEntrySListFault函数)的错误处理有关,它记录了上一次用户模式的互锁单链表POP操作操作发生页面错误的地址
至此,我们对进程和线程的相关数据结构的学习全部结束,我i也长吁一口气,虽然下面还有一个TEB要继续学习,但这里先对EPROCESS/KPROCESS/ETHREAD/KTHREAD做一个概括性的总结吧:
1. 内核层的进程和线程对象只包含了系统资源管理和多控制流并发执行所涉及的基本信息,而没有包含与应用程序相关联的信息(如进程影像文件和线程的启动函数地址等) 2. 由于windows的线程调度算法比较复杂(在抢占式调度算法的基础上,还支持优先级局部调整等),且需要支持某些硬件结构特性,所以,自然的在KPROCESS和KTHREAD结构,有些成员的引入
直接跟这些特性有关 3. 进程对象提供了线程的基本执行环境,包括进程地址空间和一组进程范围内公用的参数。线程对象提供了为参与线程调度而必须的各种信息及其维护控制流的状态 4. 因此,不同于内核层的进程和线程对象偏重于基本的功能和机制,执行体层的进程和线程对象更侧重于管理和策略
二 TEB
在学习TEB的数据结构之前,我们还是先了解几点
1. 和PEB一样,TEB位于进程地址空间中 2. 寻址到TEB的方式有很多: 2.1 FS:[0] -> TEB 2.2 KTHREAD->Teb 3. KPRCR和TEB指的同一个东西。这是我的理解,因为它们的地址都相同,而且用winDbg进行dump也发现它们是重合的。在WindowsXP中,许多操作系统的系统变量地址值保存在以KPCR开始
的数据结构中
这里提到一个FS寄存器,拓展学习一下。《看雪》上这篇帖子讲的非常好,感谢作者分享: http://bbs.pediy.com/showthread.php?t=159935
kd> dt -r _kpcr 0xFFDFF000 nt!_KPCR +0x000 NtTib : _NT_TIB +0x000 ExceptionList : 0x8054a4b0 _EXCEPTION_REGISTRATION_RECORD +0x000 Next : 0x8054aabc _EXCEPTION_REGISTRATION_RECORD +0x004 Handler : 0x80536e40 _EXCEPTION_DISPOSITION nt!_except_handler3+0 +0x004 StackBase : 0x8054acf0 +0x008 StackLimit : 0x80547f00 +0x00c SubSystemTib : (null) +0x010 FiberData : (null) +0x010 Version : 0 +0x014 ArbitraryUserPointer : (null) +0x018 Self : (null) +0x01c SelfPcr : 0xffdff000 _KPCR +0x000 NtTib : _NT_TIB +0x000 ExceptionList : 0x8054a4b0 _EXCEPTION_REGISTRATION_RECORD +0x004 StackBase : 0x8054acf0 +0x008 StackLimit : 0x80547f00 +0x00c SubSystemTib : (null) +0x010 FiberData : (null) +0x010 Version : 0 +0x014 ArbitraryUserPointer : (null) +0x018 Self : (null) +0x01c SelfPcr : 0xffdff000 _KPCR +0x000 NtTib : _NT_TIB ..+0xb30 PowerState : _PROCESSOR_POWER_STATE +0x024 Irql : 0 '' +0x028 IRR : 0 +0x02c IrrActive : 0 +0x030 IDR : 0xffffffff +0x034 KdVersionBlock : 0x80546ab8 +0x038 IDT : 0x8003f400 _KIDTENTRY +0x000 Offset : 0xf19c +0x002 Selector : 8 +0x004 Access : 0x8e00 +0x006 ExtendedOffset : 0x8053 +0x03c GDT : 0x8003f000 _KGDTENTRY +0x000 LimitLow : 0 +0x002 BaseLow : 0 +0x004 HighWord : __unnamed +0x040 TSS : 0x80042000 _KTSS +0x000 Backlink : 0x8b24 ..+0x208c IntDirectionMap : [32] "???" +0x044 MajorVersion : 1 +0x046 MinorVersion : 1 +0x048 SetMember : 1 +0x04c StallScaleFactor : 0x8f7 +0x050 DebugActive : 0 '' +0x051 Number : 0 '' +0x052 Spare0 : 0 '' +0x053 SecondLevelCacheAssociativity : 0 '' +0x054 VdmAlert : 0 +0x058 KernelReserved : [14] 0 +0x090 SecondLevelCacheSize : 0 +0x094 HalReserved : [16] 0 +0x0d4 InterruptMode : 0 +0x0d8 Spare1 : 0 '' +0x0dc KernelReserved2 : [17] 0 +0x120 PrcbData : _KPRCB +0x000 MinorVersion : 1 ..+0x020 Prcb : 0xffdff120 _KPRCB +0x000 MinorVersion : 1 +0x002 MajorVersion : 1 +0x004 CurrentThread : 0x80553740 _KTHREAD +0x000 Header : _DISPATCHER_HEADER .. +0x1bb DisableBoost : 0 '' +0x008 NextThread : (null) +0x00c IdleThread : 0x80553740 _KTHREAD +0x000 Header : _DISPATCHER_HEADER .. +0x1bb DisableBoost : 0 '' +0x010 Number : 0 '' +0x011 Reserved : 0 '' +0x012 BuildType : 2 +0x014 SetMember : 1 +0x018 CpuType : 6 '' +0x019 CpuID : 1 '' +0x01a CpuStep : 0x2a07 +0x01c ProcessorState : _KPROCESSOR_STATE +0x000 ContextFrame : _CONTEXT +0x2cc SpecialRegisters : _KSPECIAL_REGISTERS +0x33c KernelReserved : [16] 0 +0x37c HalReserved : [16] 0 +0x3bc PrcbPad0 : [92] "" +0x418 LockQueue : [16] _KSPIN_LOCK_QUEUE +0x000 Next : (null) +0x004 Lock : 0x8054bd54 -> 0 +0x498 PrcbPad1 : [8] "" +0x4a0 NpxThread : 0x8256e440 _KTHREAD +0x000 Header : _DISPATCHER_HEADER .. +0x1bb DisableBoost : 0 '' +0x4a4 InterruptCount : 0x61f0d6 +0x4a8 KernelTime : 0x55a5fa +0x4ac UserTime : 0x6228 +0x4b0 DpcTime : 0x10f +0x4b4 DebugDpcTime : 0 +0x4b8 InterruptTime : 0x4aa +0x4bc AdjustDpcThreshold : 0x14 +0x4c0 PageColor : 0 +0x4c4 SkipTick : 0 +0x4c8 MultiThreadSetBusy : 0 '' +0x4c9 Spare2 : [3] "" +0x4cc ParentNode : 0x80553e00 _KNODE +0x000 ProcessorMask : 1 +0x004 Color : 0 +0x008 MmShiftedColor : 0 +0x00c FreeCount : [2] 0 +0x018 DeadStackList : _SLIST_HEADER +0x020 PfnDereferenceSListHead : _SLIST_HEADER +0x028 PfnDeferredList : (null) +0x02c Seed : 0 '' +0x02d Flags : _flags +0x4d0 MultiThreadProcessorSet : 1 +0x4d4 MultiThreadSetMaster : (null) +0x4d8 ThreadStartCount : [2] 0 +0x4e0 CcFastReadNoWait : 0 +0x4e4 CcFastReadWait : 0 +0x4e8 CcFastReadNotPossible : 0 +0x4ec CcCopyReadNoWait : 0 +0x4f0 CcCopyReadWait : 0 +0x4f4 CcCopyReadNoWaitMiss : 0 +0x4f8 KeAlignmentFixupCount : 0 +0x4fc KeContextSwitches : 0x654342d +0x500 KeDcacheFlushCount : 0 +0x504 KeExceptionDispatchCount : 0xf11e +0x508 KeFirstLevelTbFills : 0 +0x50c KeFloatingEmulationCount : 0 +0x510 KeIcacheFlushCount : 0 +0x514 KeSecondLevelTbFills : 0 +0x518 KeSystemCalls : 0x1652d035 +0x51c SpareCounter0 : [1] 0 +0x520 PPLookasideList : [16] _PP_LOOKASIDE_LIST +0x000 P : 0x82da5d30 _GENERAL_LOOKASIDE +0x004 L : 0x8054d000 _GENERAL_LOOKASIDE +0x5a0 PPNPagedLookasideList : [32] _PP_LOOKASIDE_LIST +0x000 P : 0x82db3000 _GENERAL_LOOKASIDE +0x004 L : 0x8054fb00 _GENERAL_LOOKASIDE +0x6a0 PPPagedLookasideList : [32] _PP_LOOKASIDE_LIST +0x000 P : 0x82db3080 _GENERAL_LOOKASIDE +0x004 L : 0x8054eb00 _GENERAL_LOOKASIDE +0x7a0 PacketBarrier : 0 +0x7a4 ReverseStall : 0 +0x7a8 IpiFrame : (null) +0x7ac PrcbPad2 : [52] "" +0x7e0 CurrentPacket : [3] (null) +0x7ec TargetSet : 0 +0x7f0 WorkerRoutine : (null) +0x7f4 IpiFrozen : 0 +0x7f8 PrcbPad3 : [40] "" +0x820 RequestSummary : 0 +0x824 SignalDone : (null) +0x828 PrcbPad4 : [56] "" +0x860 DpcListHead : _LIST_ENTRY [ 0x80553da4 - 0x80553da4 ] +0x000 Flink : 0x80553da4 _LIST_ENTRY [ 0xffdff980 - 0xffdff980 ] +0x004 Blink : 0x80553da4 _LIST_ENTRY [ 0xffdff980 - 0xffdff980 ] +0x868 DpcStack : 0xf8a46000 +0x86c DpcCount : 0x86cf8 +0x870 DpcQueueDepth : 1 +0x874 DpcRoutineActive : 0 +0x878 DpcInterruptRequested : 0 +0x87c DpcLastCount : 0x86cf8 +0x880 DpcRequestRate : 0 +0x884 MaximumDpcQueueDepth : 1 +0x888 MinimumDpcRate : 3 +0x88c QuantumEnd : 0 +0x890 PrcbPad5 : [16] "" +0x8a0 DpcLock : 0 +0x8a4 PrcbPad6 : [28] "" +0x8c0 CallDpc : _KDPC +0x000 Type : 19 +0x002 Number : 0x20 ' ' +0x003 Importance : 0x2 '' +0x004 DpcListEntry : _LIST_ENTRY [ 0x0 - 0x0 ] +0x00c DeferredRoutine : (null) +0x010 DeferredContext : (null) +0x014 SystemArgument1 : (null) +0x018 SystemArgument2 : (null) +0x01c Lock : (null) +0x8e0 ChainedInterruptList : (null) +0x8e4 LookasideIrpFloat : 1240 +0x8e8 SpareFields0 : [6] 0 +0x900 VendorString : [13] "GenuineIntel" +0x90d InitialApicId : 0 '' +0x90e LogicalProcessorsPerPhysicalProcessor : 0x1 '' +0x910 MHz : 0x8f6 +0x914 FeatureBits : 0x20033fff +0x918 UpdateSignature : _LARGE_INTEGER 0x17`00000000 +0x000 LowPart : 0 +0x004 HighPart : 23 +0x000 u : __unnamed +0x000 QuadPart : 98784247808 +0x920 NpxSaveArea : _FX_SAVE_AREA +0x000 U : __unnamed +0x208 NpxSavedCpu : 0 +0x20c Cr0NpxState : 0 +0xb30 PowerState : _PROCESSOR_POWER_STATE +0x000 IdleFunction : 0x80525bf2 void nt!PopProcessorIdle+0 ..+0x114 PerfSetThrottle : 0xf87fa53c long *** ERROR: Module load completed but symbols could not be loaded for intelppm.sys intelppm+53c +0x118 LastC3KernelUserTime : 0 +0x11c LastPackageIdleTime : 0 +0x024 Irql : 0 '' +0x028 IRR : 0 +0x02c IrrActive : 0 +0x030 IDR : 0xffffffff +0x034 KdVersionBlock : 0x80546ab8 +0x038 IDT : 0x8003f400 _KIDTENTRY +0x000 Offset : 0xf19c +0x002 Selector : 8 +0x004 Access : 0x8e00 +0x006 ExtendedOffset : 0x8053 +0x03c GDT : 0x8003f000 _KGDTENTRY +0x000 LimitLow : 0 +0x002 BaseLow : 0 +0x004 HighWord : __unnamed +0x000 Bytes : __unnamed +0x000 Bits : __unnamed +0x040 TSS : 0x80042000 _KTSS +0x000 Backlink : 0x8b24 .. .. +0x11c LastPackageIdleTime : 0
线程运行在RING0(系统地址空间)和RING3(用户地址空间)时,FS段寄存器分别指向不同内存段的。
1. 线程运行在RING0下,FS段值是0x3B(WindowsXP下值,在Windows2000下值为0x38)(在GDT中的编号); 2. 运行在RING3下时,FS段寄存器值是0x30(在GDT中的编号)。
FS寄存器值的改变是在程序从Ring3进入Ring0后和从Ring0退回到Ring3前完成的,也就是说:都是在Ring0下给FS赋不同值的。
当线程运行在Ring3下时,FS指向的段是GDT中的0x30段。该段的长度为4K,基地址为当前线程的线程环境块(TEB),所以该段也被称为"TEB段"。
因为Windows中线程是不停切换的,所以该段的基地址值将随线程切换而改变的。
Windows2000中进程环境块(PEB)的地址为0X7FFDF000,该进程的
第一个线程的TEB地址为0X7FFDE000,
第二个TEB的地址为0X7FFDD000…..
但是在WindowsXP SP2 下这些结构的地址都是随机映射的。所以总的来说,进程的PEB,线程的TEB的地址只能通过FS的方式来动态获取了。
接下来来看看TEB的数据结构的定义,我结合我能找到的资料来作说明,还有一样的情况,在google上并没有找到对TEB的全部数据结构字段的详细分析,对SEH,PEB的这一类的文章倒是不少,希望知道的朋友能分享一些好的资料,不胜感激。
typedef struct _TEB { NT_TIB Tib; PVOID EnvironmentPointer; CLIENT_ID Cid; PVOID ActiveRpcInfo; PVOID ThreadLocalStoragePointer; PPEB Peb; ULONG LastErrorValue; ULONG CountOfOwnedCriticalSections; PVOID CsrClientThread; PVOID Win32ThreadInfo; ULONG Win32ClientInfo[0x1F]; PVOID WOW32Reserved; ULONG CurrentLocale; ULONG FpSoftwareStatusRegister; PVOID SystemReserved1[0x36]; PVOID Spare1; ULONG ExceptionCode; ULONG SpareBytes1[0x28]; PVOID SystemReserved2[0xA]; ULONG GdiRgn; ULONG GdiPen; ULONG GdiBrush; CLIENT_ID RealClientId; PVOID GdiCachedProcessHandle; ULONG GdiClientPID; ULONG GdiClientTID; PVOID GdiThreadLocaleInfo; PVOID UserReserved[5]; PVOID GlDispatchTable[0x118]; ULONG GlReserved1[0x1A]; PVOID GlReserved2; PVOID GlSectionInfo; PVOID GlSection; PVOID GlTable; PVOID GlCurrentRC; PVOID GlContext; NTSTATUS LastStatusValue; UNICODE_STRING StaticUnicodeString; WCHAR StaticUnicodeBuffer[0x105]; PVOID DeallocationStack; PVOID TlsSlots[0x40]; LIST_ENTRY TlsLinks; PVOID Vdm; PVOID ReservedForNtRpc; PVOID DbgSsReserved[0x2]; ULONG HardErrorDisabled; PVOID Instrumentation[0x10]; PVOID WinSockData; ULONG GdiBatchCount; ULONG Spare2; ULONG Spare3; ULONG Spare4; PVOID ReservedForOle; ULONG WaitingOnLoaderLock; PVOID StackCommit; PVOID StackCommitMax; PVOID StackReserved; } TEB, *PTEB;
1. NT_TIB Tib
第一个成员域Tib很重要,它是一个指向NT_TIB结构体的指针,它和SEH的寻址有很大关系。(题外话,结构体的第一个成员域和结构体本身的地址是重合的,一定要注意)
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; typedef NT_TIB *PNT_TIB;
1) NT_TIB结构体的第一个成员ExceptionList指向一个_EXCEPTION_REGISTRATION_RECORD结构体
typedef struct _EXCEPTION_REGISTRATION_RECORD { struct _EXCEPTION_REGISTRATION_RECORD *Next; PEXCEPTION_ROUTINE Handler; } EXCEPTION_REGISTRATION_RECORD;
Next: 指向一个和自身相同的结构(SEH链)
Handler: 指向SEH链上当前这个SEH的处理函数的入口地址
nt!_KPCR +0x000 NtTib : _NT_TIB +0x000 ExceptionList : 0x8054a4b0 _EXCEPTION_REGISTRATION_RECORD +0x000 Next : 0x8054aabc _EXCEPTION_REGISTRATION_RECORD +0x004 Handler : 0x80536e40 _EXCEPTION_ROUTINE ...
所以,我们可以使用: FS[0x04] 来获取当前SEH链的最顶部的那个处理函数的入口地址。我们在逆向或者shellcode的编写中会经常看到以FS:[XX]这种格式的代码,这个时候一定要从数据结构出发,去了解代码到底是在寻址什么。
更多关于SEH的相关知识请参阅:
1. 《加密与解密3》 11.2 SEH 2. [经典文章翻译]A_Crash_Course_on_the_Depths_of_Win32_Structured_Exception_Handling.pdf 3. 《0 DAY安全: 软件漏洞分析技术》 6.1 windows异常处理机制
2) NT_TIB结构体的最后一个成员Self也很重要,它也可以被用来获取DLL模块的基址,在学习笔记(3)中我们已经谈过了"通过PEB(FS:[30])获取KERNEL32.DLL基地址"
这里再次介绍一种获取KERNEL32.DLL的方法,即利用这个Self成员域。
首先明白,Self位于TEB的0x18偏移位置,所以是FS:[0x18]
本地线程的栈里偏移18H的指针指向kernel32.dll内部,而FS:[ 0x18 ] 指向当前线程而且往里四个字节指向线程栈,结合栈顶指针进行对齐遍历,找到PE文件头(DLL的文件格式)
的"MZ"MSDOS标志,就拿到了kernel32.dll基址。
下面给出代码:
xor esi, esi mov esi, FS:[ esi + 0x18 ] // TEB mov eax, [ esi + 4 ] // 这个是需要的栈顶 mov eax, [ eax - 0x1c ] // 指向Kernel32.dll内部 find_kernel32_base: dec eax // 开始地毯式搜索Kernel32空间 xor ax, ax cmp word ptr [ eax ], 0x5a4d // "MZ" jne find_kernel32_base // 循环遍历 ,找到则返回 eax
在编写shellcode的时候会经常用到这种技术
3) NT_TIB结构体的第一个成员ExceptionList除了指向一个_EXCEPTION_REGISTRATION_RECORD结构体,它还可以用来枚举DLL模块的基址。
注意:FS:[ 0 ] 指向的是SHE,它指向kernel32.dll内部链,这样就可以顺藤摸瓜了。 FS:[ 0 ] 指向的是SHE的内层链,为了找到顶层异常处理,我们向外遍历找到prev成员等于 0xffffffff 的EXCEPTION_REGISTER结构,该结构的handler值就是系统 默认的处理例程。 这里有个细节,DLL的装载是64K边界对齐的,所以需要利用遍历到的指向最后的异常处理的指针进行页查找,再结合PE文件MSDOS标志部分,只要在每个64K边界查找"MZ"字符就能
找到kernel32.dll基址。
汇编代码如下:
xor ecx, ecx mov esi, FS : [ ecx ] find_seh: mov eax, [ esi ] mov esi, eax cmp [ eax ], ecx jns find_seh // 0xffffffff mov eax, [ eax + 0x04 ] // handler find_kernel32_base: dec eax xor ax, ax cmp word ptr [ eax ], 0x5a4d jne find_kernel32_base
2. 其他
PVOID EnvironmentPointer;
CLIENT_ID Cid;
PVOID ActiveRpcInfo;
PVOID ThreadLocalStoragePointer;
不详..
3. PPEB Peb
Peb域指向当前线程所属的进程的PEB结构体的基址。也就是说,我们要获取进程的PEB,如果在用户模式下(即ring3应用程序)就必须先走TEB这条道路
4. 其他
剩下的字段全部都不详,我既没有找到相关的资料也没有找到利用这些其他的字段来达到一些"特殊"目的的用法,也许是还没开发出来吧,不过更有可能是因为我水平不够,对这些不了解
下面是网上找到的一张图:
后记:
至此,我的《寒江独钓》学习笔记全部结束,我们完成了对进程和线程的数据结构的学习,在文章中,我尽我最大的能力为大家扩展了知识点并在自己不太清楚的地方给出了相关书籍的参考章节,windows系统和内核确实是博大精深,我不敢100%甚至不敢80%保证我写的都是对的,一定会有理解错误的地方,但我愿意和大家一起共同学习,共同讨论,我相信这也会是一个良好的开端,因为,我发现学习windows的数据结构能够很好的"拓展"到windows的方方面面,不仅能让我们积累大量的基础知识,还能让我们对windows有一个整体的概念。
接下来准备系统的学习潘老师的《windows 内核原理与实现》,对windows的线程和进程创建做源码级的研究,也希望有共同爱好的朋友和我共同讨论,共同学习