二、线程调度与切换
众所周知:Windows系统是一个分时抢占式系统,分时指每个线程分配时间片,抢占指时间片到期前,中途可以被其他更高优先级的线程强制抢占。
背景知识:每个cpu都有一个TSS,叫‘任务状态段’。这个TSS内部中的一些字段记录着该cpu上当前正在运行的那个线程的一些信息(如ESP0记录着该线程的内核栈位置,IO权限位图记录着当前线程的IO空间权限)
IO空间有64KB,IO权限位图中的每一位记录着对应IO地址的IN、OUT许可权限,所以IO权限位图本身有8KB大小,TSS中就就记录着当前线程IO权限位图的偏移位置。
每当切换线程时:自然要跟着修改TSS中的ESP0和IO权限位图。TSS0中为什么要保存当前线程的内核栈位置?原因是:每当一个线程内部,从用户模式进入内核模式时,需要将cpu中的esp换成该线程的内核栈(各线程的内核栈是不同的)每当进入内核模式时,cpu就自动从TSS中找到ESP0,然后MOV ESP, TSS.ESP0,换成内核栈后,cpu然后在内核栈中压入浮点寄存器和标准的5个寄存器:原cs、原eip、原ss、原esp、原eflags。这就是为什么需要在TSS中记录当前线程的内核栈地址。(注意ESP0并不是栈底地址,而是要压入保存寄存器处的存放地址)
与线程切换相关的数据结构定义:
Struct KPCR //处理器控制块(内核中的fs寄存器总是指向这个结构体的基址)
{
KPCR_TIB Tib;
KPCR* self;//方便寻址
KPRCB* Prcb;
KIRQL irql;//物理上表示cpu的当前中断级,逻辑上理解为当前线程的中断级更好
USHORT* IDT;//本cpu的中断描述符表的地址
USHORT* GDT;//本cpu的全局描述符表的地址
KTSS* TSS;//本cpu上当前线程的信息(ESP0)
…
}
Struct KPCR_TIB
{
Void* ExceptionList;//当前线程的内核seh链表头结点地址
Void* StackBase;//内核栈底地址
Void* StackLimit;//栈的提交边界
…
KPCR_TIB* self;//方便寻址
}
Struct KPRCB
{
…
KTHREAD* CurrentThread;//本cpu上当前正在运行的线程
KTHREAD* NextThread;//将剥夺(即抢占)当前线程的下一个线程
KTHREAD* IdleThread;//空转线程
BOOL QuantumEnd;//重要字段。指当前线程的时间片是否已经用完。
LIST_ENTRY WaitListHead;//本cpu的等待线程队列
ULONG ReadSummary;//各就绪队列中是否为空的标志
ULONG SelectNextLast;
LIST_ENTRY DispatcherReadyListHead[32];//对应32个优先级的32个就绪线程队列
FX_SAVE_AREA NpxSaveArea;
…
}
typedef struct _KSWITCHFRAME //切换帧(用来保存切换线程)
{
PVOID ExceptionList;//保存线程切换时的内核she链表(不是用户空间中的seh)
Union
{
BOOLEAN ApcBypassDisable;//用于首次调度
UCHAR WaitIrql;//用于保存切换时的WaitIrql
};
//实际上首次时为KiThreadStartup,以后都固定为call KiSwapContextInternal后面的那条指令
PVOID RetAddr;//保存发生切换时的断点地址(以后切换回来时从这儿继续执行)
} KSWITCHFRAME, *PKSWITCHFRAME;
typedef struct _KTRAP_FRAME //Trap现场帧
{
------------------这些是KiSystemService保存的---------------------------
ULONG DbgEbp;
ULONG DbgEip;
ULONG DbgArgMark;
ULONG DbgArgPointer;
ULONG TempSegCs;
ULONG TempEsp;
ULONG Dr0;
ULONG Dr1;
ULONG Dr2;
ULONG Dr3;
ULONG Dr6;
ULONG Dr7;
ULONG SegGs;
ULONG SegEs;
ULONG SegDs;
ULONG Edx;//xy 这个位置不是用来保存edx的,而是用来保存上个Trap帧,因为Trap帧是可以嵌套的
ULONG Ecx; //中断和异常引起的自陷要保存eax,系统调用则不需保存ecx
ULONG Eax;//中断和异常引起的自陷要保存eax,系统调用则不需保存eax
ULONG PreviousPreviousMode;
struct _EXCEPTION_REGISTRATION_RECORD FAR *ExceptionList;//上次seh链表的开头地址
ULONG SegFs;
ULONG Edi;
ULONG Esi;
ULONG Ebx;
ULONG Ebp;
----------------------------------------------------------------------------------------
ULONG ErrCode;//发生的不是中断,而是异常时,cpu还会自动在栈中压入对应的具体异常码在这儿
-----------下面5个寄存器是由int 2e内部本身保存的或KiFastCallEntry模拟保存的现场---------
ULONG Eip;
ULONG SegCs;
ULONG EFlags;
ULONG HardwareEsp;
ULONG HardwareSegSs;
---------------以下用于用于保存V86模式的4个寄存器也是cpu自动压入的-------------------
ULONG V86Es;
ULONG V86Ds;
ULONG V86Fs;
ULONG V86Gs;
} KTRAP_FRAME, *PKTRAP_FRAME;
下面这个核心函数用来切换线程(从当前线程切换到新线程去)。这个函数的原型是:
BOOL FASTCALL KiSwapContex(KTHREAD* Currentthread*, KTHREAD* NewThread);
返回值表示下次切换回来时是否需要手动扫描执行内核APC。这个函数的汇编代码为:
@KiSwapContext@8: //开头的@表示fastcall调用约定
{
sub esp, 4 * 4 //腾出局部变量空间
//保存这4个寄存器,因为KiSwapContextInternal函数内部要使用这几个寄存器
mov [esp+12], ebx
mov [esp+8], esi
mov [esp+4], edi
mov [esp+0], ebp
mov ebx, fs:[KPCR_SELF] //ebx=当前cpu的KPCR*
mov edi, ecx //edi= KiSwapContext的第一个参数,即CurrentThread
mov esi, edx //edi= KiSwapContext的第而个参数,即NewThread
movzx ecx, byte ptr [edi+KTHREAD_WAIT_IRQL] //ecx=当前线程的WaitIrql
call @KiSwapContextInternal@0 //调用真正的切换工作函数
这中间已经被切换到新线程去了,当前线程已经让出cpu,挂入了就绪队列。需要等到下次重新被调度运行时,才又从这儿的断点处继续向下执行下去
mov ebp, [esp+0] //这条指令就是断点处,以后切换回来时就从这个断点处继续执行
mov edi, [esp+4]
mov esi, [esp+8]
mov ebx, [esp+12
add esp, 4 * 4
ret
}
下面的函数完成真正的切换工作(返回值表示切换回来后是否需要手动扫描执行内核apc)
@KiSwapContextInternal@0: //edi指向当前线程,esi指向要切换到的新线程,ebx指向当前KPCR*
{
inc dword ptr es:[ebx+KPCR_CONTEXT_SWITCHES] //递增当前cpu上发生的历史线程切换计数
push ecx //保存本线程切换时的WaitIrql
push [ebx+KPCR_EXCEPTION_LIST] //保存本线程切换时的内核seh链表
-------------------------至此,上面的两条push连同本函数的返回地址(即断点地址),就构成了一个切换帧。当前线程切换时的内核栈顶位置就在此处-----------------------------
AfterTrace:
mov ebp, cr0
mov edx, ebp //将cr0寄存器保存在edx中(cr0的Bit3位“TaskSwitched”标志位,与浮点运算相关)
SetStack:
mov [edi+KTHREAD_KERNEL_STACK], esp //保存本线程切换时的内核栈顶位置
mov eax, [esi+KTHREAD_INITIAL_STACK] //eax=新线程的内核栈底地址
--------------------------------------------------------------------------------
cli //下面检查Npx浮点寄存器,要关中断
movzx ecx, byte ptr [esi+KTHREAD_NPX_STATE] //ecx=新线程的Npx状态
and edx, ~(CR0_MP + CR0_EM + CR0_TS)
or ecx, edx
or ecx, [eax - (NPX_FRAME_LENGTH - FN_CR0_NPX_STATE)] //获得新线程需要的cr0
cmp ebp, ecx
jnz NewCr0 //如果新线程需要的cr0不同于当前的cr0,则修改当前cr0为新线程的cr0
StackOk:
Sti
--------------------------------------------------------------------------------
mov esp, [esi+KTHREAD_KERNEL_STACK] //关键。恢复成新线程当初被切换时的内核栈顶
mov ebp, [esi+KTHREAD_APCSTATE_PROCESS] //ebp=目标进程
mov eax, [edi+KTHREAD_APCSTATE_PROCESS] //eax=当前进程
cmp ebp, eax //检查是否是切换到同一个进程中的其他线程(若是。就不用切换LDT和cr3)
jz SameProcess
//若切换到其他进程中的线程,则要同时修改LDT和CR3
mov ecx, [ebp+KPROCESS_LDT_DESCRIPTOR0]
or ecx, [eax+KPROCESS_LDT_DESCRIPTOR0]
jnz LdtReload //如果两个进程的LDT不同,就要换用不同的LDT
UpdateCr3:
mov eax, [ebp+KPROCESS_DIRECTORY_TABLE_BASE]
mov cr3, eax //关键。将cr3换成目标进程的页目录
SameProcess:
xor eax, eax
mov gs, ax
mov eax, [esi+KTHREAD_TEB] //新线程的TEB地址
mov [ebx+KPCR_TEB], eax //当前KPCR中的TEB指向新线程的TEB
mov ecx, [ebx+KPCR_GDT]
//修改GDT中的TEB描述符,指向新线程的TEB
mov [ecx+0x3A], ax
shr eax, 16
mov [ecx+0x3C], al
mov [ecx+0x3F], ah
mov eax, [esi+KTHREAD_INITIAL_STACK] //eax=新线程的内核栈底位置
sub eax, NPX_FRAME_LENGTH //跳过浮点保存区空间
test dword ptr [eax - KTRAP_FRAME_SIZE + KTRAP_FRAME_EFLAGS], EFLAGS_V86_MASK
jnz NoAdjust //检查新线程是否运行在V86模式
sub eax, KTRAP_FRAME_V86_GS - KTRAP_FRAME_SS //跳过V86保存区
NoAdjust:
mov ecx, [ebx+KPCR_TSS]
mov [ecx+KTSS_ESP0], eax //关键,修改TSS中的ESP0,指向新线程的内核栈底
mov ax, [ebp+KPROCESS_IOPM_OFFSET]
mov [ecx+KTSS_IOMAPBASE], ax //修改TSS中的IO权限位图偏移指向新进程中的IO权限位图
inc dword ptr [esi+KTHREAD_CONTEXT_SWITCHES] //递增线程的切换次数(也即历史调度次数)
pop [ebx+KPCR_EXCEPTION_LIST] //将当前KPCR中记录的seh链表恢复成新线程的seh链表
pop ecx //ecx=新线程原来切换前的WaitIrql
cmp byte ptr [ebx+KPCR_PRCB_DPC_ROUTINE_ACTIVE], 0 //检查当前是否有DPC函数处于活动状态
jnz BugCheckDpc //蓝屏
//至此,cpu中的寄存器内容全部换成了新线程的那些寄存器,从这个意思上说,此时就已完成了全部切换工作,下面的代码都是在新线程的环境中运行了。
--------------------------------新线程环境---------------------------------------
cmp byte ptr [esi+KTHREAD_PENDING_KERNEL_APC], 0
jnz CheckApc //看到没,每次线程得到重新调度运行前,都会扫描执行内核apc队列中的函数
xor eax, eax
ret //此处返回值表示没有内核apc
CheckApc:
cmp word ptr [esi+KTHREAD_SPECIAL_APC_DISABLE], 0 //检查是否禁用了APC
jnz ApcReturn
test cl, cl //检查WaitIrql,如果是APC级,就在本函数内部返回前,发出apc中断
jz ApcReturn
//if(SPECIAL APC 没禁用 && WaitIrql!=PASSIVE_LEVEL),切换回来时就先执行内核APC
mov cl, APC_LEVEL
call @HalRequestSoftwareInterrupt@4 //发出一个apc中断
or eax, esp //既然发出apc中断了,那么就return FALSE表示无需手动扫描执行apc
ApcReturn:
setz al
ret //此处返回值表示切回来后是否需要手动扫描执行apc
//当这个函数返回时,之前已经换成新线程的内核栈了。当函数返回后,将回到KiSwapContext中,当KiSwapContext返回到调用方时,那个调用方就是新线程当初调用的KiSwapContext的函数,这样,就沿着新线程的内核栈,逐级向上回溯到新线程中了。因此,可以说,切换内核栈,即是切换线程。
LdtReload:
mov eax, [ebp+KPROCESS_LDT_DESCRIPTOR0]
test eax, eax //检测目标进程有没有LDT
jz LoadLdt
mov ecx, [ebx+KPCR_GDT]
mov [ecx+KGDT_LDT], eax //改指目标进程的LDT
mov eax, [ebp+KPROCESS_LDT_DESCRIPTOR1]
mov [ecx+KGDT_LDT+4], eax//改指目标进程的LDT
/* Write the INT21 handler */
mov ecx, [ebx+KPCR_IDT]
mov eax, [ebp+KPROCESS_INT21_DESCRIPTOR0]
mov [ecx+0x108], eax
mov eax, [ebp+KPROCESS_INT21_DESCRIPTOR1]
mov [ecx+0x10C], eax
mov eax, KGDT_LDT
LoadLdt:
lldt ax
jmp UpdateCr3
NewCr0:
mov cr0, ecx
jmp StackOk
BugCheckDpc:
mov eax, [edi+KTHREAD_INITIAL_STACK]
push 0
push eax
push esi
push edi
push ATTEMPTED_SWITCH_FROM_DPC
call _KeBugCheckEx@20 //蓝屏提示:“尝试从活动DPC例程中切换线程”
}
如上:线程从KiSwapContextInternal这个函数内部切换出去,某一时刻又切换回这个函数内。
或者也可以理解为:线程从KiSwapContext这个函数切换出去,某一时刻又切换回这个函数内。
(注:可以hook这两个函数,来达到检测隐藏进程的目的)
明白了线程切换的过程,所做的工作后,接下来看:线程的切换时机(也即一个线程什么时候会调用
KiSwapContext这个函数把自己切换出去),相信这是大伙最感兴趣的问题。
三、线程的调度策略与切换时机
调度策略:Windows严格按优先级调度线程。
优先级分成32个,每个cpu对应有32个就绪线程队列。每当要发生线程切换时,就根据调度策略从32条就绪队列中,按优先级从高到低的顺序扫描(同一个就绪队列中,由于优先级相同,则按FIFO顺序扫描),这样,从32条就绪队列中,找到优先级最高的那个候选就绪线程,给予调度执行。
当一个线程得到调度执行时,如果一直没有任何其他就绪线程的优先级高于本线程,本线程就可以畅通无阻地一直执行下去,直到本次的时间片用完。但是如果本次执行的过程中,如果有个就绪线程的优先级突然高于了本线程,那么本线程将被抢占,cpu将转去执行那个线程。但是,这种抢占可能不是立即性的,只有在当前线程的irql在DISPATCH_LEVEL以下(不包括),才会被立即抢占,否则,推迟抢占(即把那个高优先级的就绪线程暂时记录到当前cpu的KPCR结构中的NextThread字段中,标记要将抢占)。
切换时机:一句话【时片、抢占、等、主动】
1、 时间片耗尽
2、 被抢占
3、 因等待事件、资源、信号时主动放弃cpu(如调用WaitForSingleObject)
4、 主动切换(如主动调用SwitchToThread这个Win32 API)
但是:即使到了切换时机了,也只有当线程的irql在DISPATCH_LEVEL以下(不包括)时,才可以被切换出去,否则,线程将继续占有cpu,一直等到irql降到DISPATCH_LEVEL以下。
线程的状态(不含挂起态,其实挂起态本质上也是一种等待态)
1、Ready就绪态(挂入相应的就绪队列)
2、某一时刻得到调度变成Running运行态
3、因等待某一事件、信号、资源等变成Waiting等待状态
4、Standby状态。指处于抢占者状态(NextThread就是自己)
5、DeferredReady状态。指‘将’进入就绪态。
先看一下主动放弃cpu,切换线程的函数
NTSTATUS NtYieldExecution()
{
NTSTATUS Status = STATUS_NO_YIELD_PERFORMED;
KIRQL OldIrql;
PKPRCB Prcb = KeGetCurrentPrcb();//当前cpu的控制块
PKTHREAD Thread = KeGetCurrentThread(), NextThread;
if (Prcb->ReadySummary==0)
return Status;//如果没有其他线程处于就绪态,就不用切换了
//重要。线程的调度过程与切换过程,本身就运行在SynchLevel,目的是防止在执行调度、切换工作的过程中又被切换了出去。因此,可以说,调度、切换这个过程是原子的。
OldIrql = KeRaiseIrqlToSynchLevel();//先提到SynchLevel,再做调度、切换工作
if (Prcb->ReadySummary!=0)//如果当前cpu上有就绪线程
{
KiAcquireThreadLock(Thread);
KiAcquirePrcbLock(Prcb);
if (Prcb->NextThread != NULL)
NextThread = Prcb->NextThread;//优先选择那个等待抢占的线程
Else //如果当前没有候选抢占线程,就从就绪队列调度出一个线程
NextThread = KiSelectReadyThread(1, Prcb);
if (NextThread)
{
Thread->Quantum = Thread->QuantumReset;//设置下次调度运行的时间片
Thread->Priority = KiComputeNewPriority(Thread, 1);//略微降低一个优先级
KiReleaseThreadLock(Thread);
KiSetThreadSwapBusy(Thread);//标记本线程正在被切换
Prcb->CurrentThread = NextThread;//标记已切换到下一个线程
Prcb->NextThread = NULL;//初始运行时尚未有任何抢占者线程
NextThread->State = Running;//标记线程状态正在运行
Thread->WaitReason = WrYieldExecution;//标记本线程上次被切换的原因是主动放弃
KxQueueReadyThread(Thread, Prcb);//将本线程转入就绪队列
Thread->WaitIrql = APC_LEVEL;//这将导致下次切换回来时会自动发出apc中断
MiSyncForContextSwitch(NextThread);
KiSwapContext(Thread, NextThread);//真正切换到目标线程
---------------------------华丽的分割线---------------------------------------
Status = STATUS_SUCCESS;//本线程下次切回来时继续从这里执行下去
}
else
{
KiReleasePrcbLock(Prcb);
KiReleaseThreadLock(Thread);
}
}
KeLowerIrql(OldIrql);//完成调度、切换过程后,降低到原irql(这个过程可能会执行apc)
return Status;
}
//下面就是调度策略:按优先级从高到低的顺序扫描32条就绪队列,取下最高优先级的线程
PKTHREAD
KiSelectReadyThread(IN KPRIORITY Priority,//指调度出的线程必须>=这个优先级
IN PKPRCB Prcb)//指定cpu
{
ULONG PrioritySet;
LONG HighPriority;//含有就绪线程的最高优先级队列
PLIST_ENTRY ListEntry;
PKTHREAD Thread = NULL;//调度出来的线程
PrioritySet = Prcb->ReadySummary >> Priority;
if (!PrioritySet) goto Quickie;
BitScanReverse((PULONG)&HighPriority, PrioritySet);//从高位到地位扫描那个标志位图
HighPriority += Priority;
ASSERT(IsListEmpty(&Prcb->DispatcherReadyListHead[HighPriority]) == FALSE);
ListEntry = Prcb->DispatcherReadyListHead[HighPriority].Flink;//队列中的第一个线程
Thread = CONTAINING_RECORD(ListEntry, KTHREAD, WaitListEntry);
ASSERT(HighPriority == Thread->Priority);//确保优先级符合
ASSERT(Thread->Affinity & AFFINITY_MASK(Prcb->Number));//确保cpu亲缘性
ASSERT(Thread->NextProcessor == Prcb->Number);//确保是在那个cpu中等待调度
if (RemoveEntryList(&Thread->WaitListEntry))//取下来
Prcb->ReadySummary ^= PRIORITY_MASK(HighPriority);//如果队列变空了,修改对应的标志位
Quickie:
return Thread;
}
每当一个非实时线程被切换出去,放弃cpu后,系统都会略微降低该线程的优先级,以免该线程总是占住cpu不放。下面的函数就是做这个目的。
SCHAR KiComputeNewPriority(IN PKTHREAD Thread,//非实时线程
IN SCHAR Adjustment)//‘调减量’
{
SCHAR Priority;
Priority = Thread->Priority;//原优先级
if (Priority < LOW_REALTIME_PRIORITY)//只对非实时性线程做调整
{
//先减去‘恢减量’(对应于唤醒线程时系统临时提高的优先级量,现在要把它恢复回去)
Priority -= Thread->PriorityDecrement;
//再减去‘调减量’,这才是真正的调整,上面只是恢复优先级
Priority -= Adjustment;
if (Priority < Thread->BasePriority)
Priority = Thread->BasePriority;//优先级不管怎么调,不能低于基本优先级
Thread->PriorityDecrement = 0;
}
return Priority;
}
下面的函数用来将现场加入指定cpu的相应优先级的就绪队列
VOID KxQueueReadyThread(IN PKTHREAD Thread,IN PKPRCB Prcb)
{
BOOLEAN Preempted;
KPRIORITY Priority;
ASSERT(Prcb == KeGetCurrentPrcb());
ASSERT(Thread->State == Running);
ASSERT(Thread->NextProcessor == Prcb->Number);
{
Thread->State = Ready;//有运行态改为就绪态
Priority = Thread->Priority;
Preempted = Thread->Preempted;//表示是否是因为被抢占原因而让出的cpu
Thread->Preempted = FALSE;
Thread->WaitTime = KeTickCount.LowPart;//记录上次被切换的时间
//若是被抢占原因让出的cpu,就把那个线程加入队列的开头,以平衡它的怒气,否则加入尾部
Preempted ? InsertHeadList(&Prcb->DispatcherReadyListHead[Priority],
&Thread->WaitListEntry) :
InsertTailList(&Prcb->DispatcherReadyListHead[Priority],
&Thread->WaitListEntry);
Prcb->ReadySummary |= PRIORITY_MASK(Priority);//标志相应的就绪队列不空
KiReleasePrcbLock(Prcb);
}
}
前面说的主动切换。但主动切换是非常少见的,一般都是不情愿的,被动切换。典型的被动切换情形是:
每触发一次时钟中断(通常每10毫秒触发一次),就会在时钟中断的isr中递减当前线程KTHREAD结构中的Quantum字段(表示剩余时间片),当减到0时(也即时间片耗尽时),会将KPCRB结构中的QuantumEnd字段标记为TRUE。同时,当cpu在每次扫描执行完DPC队列中的函数后,irql将降到DISPATCH_LEVEL以下,这时系统会检查QuantumEnd字段,若发现时间片已经用完(可能已经用完很久了),就会调用下面的函数切换线程,这时切换线程的一种典型时机。
VOID KiQuantumEnd() //每次时间片自然到期后执行这个函数
{
PKPRCB Prcb = KeGetCurrentPrcb();
PKTHREAD NextThread, Thread = Prcb->CurrentThread;//当前线程
if (InterlockedExchange(&Prcb->DpcSetEventRequest, 0))//检查是否有‘触发DPC事件’的请求
KeSetEvent(&Prcb->DpcEvent, 0, 0);
KeRaiseIrqlToSynchLevel();//提升到SynchLevel,准备调度、切换
KiAcquireThreadLock(Thread);
KiAcquirePrcbLock(Prcb);
if (Thread->Quantum <= 0)//确认该线程的时间片已到期
{
if ((Thread->Priority >= LOW_REALTIME_PRIORITY) &&
(Thread->ApcState.Process->DisableQuantum))
{
Thread->Quantum = MAX_QUANTUM;//实时线程可以禁用时间片机制
}
else
{
Thread->Quantum = Thread->QuantumReset;//设置下次调度时的时间片
Thread->Priority = KiComputeNewPriority(Thread,1);//降低一个优先级(以免占住cpu)
if (Prcb->NextThread != NULL)
{
NextThread = Prcb->NextThread//直接使用这个候选的线程
Thread->Preempted = FALSE;//因为是时间片到期发生的切换,所以不是被抢占
}
else
{
NextThread = KiSelectReadyThread(Thread->Priority, Prcb);//调度出一个线程
//表示这个线程已被选中处于候选抢占状态,将立马上架投入运行
NextThread->State = Standby;
}
}
}
KiReleaseThreadLock(Thread);
KiSetThreadSwapBusy(Thread);//标记当前线程正在被切换
Prcb->CurrentThread = NextThread;//标记为切换到下一个线程了
Prcb->NextThread = NULL;//初始运行时没有抢占者线程
NextThread->State = Running;//已在运行了
Thread->WaitReason = WrQuantumEnd;//标记上次被切换的原因是时间片到期
KxQueueReadyThread(Thread, Prcb);//当前线程转入就绪队列
Thread->WaitIrql = APC_LEVEL;// 这将导致下次切换回来时会自动发出apc中断
KiSwapContext(Thread, NextThread);//正式切换到新线程
---------------------------华丽的分割线---------------------------------------
KeLowerIrql(DISPATCH_LEVEL);
}
除了时间片自然到期,线程被切换外,线程还可以在运行的过程中被其他高优先级线程,强制抢占而切换。
如一个线程调用ResumeThread将别的线程恢复调度时,自己会检查那个刚被恢复成就绪态的线程是否因优先级高于自己而要抢占本线程,如果是,就会切换到那个线程。因此这个api内部有切换线程的可能
ULONG KeResumeThread(IN PKTHREAD Thread) //恢复指定目标线程
{
KLOCK_QUEUE_HANDLE ApcLock;
ULONG PreviousCount;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);//当前irql一定<=DISPATCH_LEVEL
KiAcquireApcLock(Thread, &ApcLock);//锁定apc队列,同时提升irql到DISPATCH_LEVEL
PreviousCount = Thread->SuspendCount;
if (PreviousCount)
{
Thread->SuspendCount--;//递减挂起计数
//若挂起计数减到0,唤醒目标线程,进入就绪队列或者变成抢占者线程
if ((!Thread->SuspendCount) && (!Thread->FreezeCount))
{
KiAcquireDispatcherLockAtDpcLevel();
Thread->SuspendSemaphore.Header.SignalState++;
KiWaitTest(&Thread->SuspendSemaphore.Header, IO_NO_INCREMENT);//尝试唤醒它
KiReleaseDispatcherLockFromDpcLevel();
}
}
KiReleaseApcLockFromDpcLevel(&ApcLock);//注意这个函数只释放apc队列锁,不降低irql
//关键函数。降低当前线程的irql,同时先检查是否有抢占者线程,若有,先执行抢占切换。
KiExitDispatcher(ApcLock.OldIrql);
return PreviousCount;//返回之前的挂起计数
}
//下面这个函数的主功能是降回当前线程的irql到指定OldIrql。不过在正式的降低前,会先检查是否发生了抢占,若有,就先执行线程切换,等下次切换回来后再降低当前线程的irql。
//这个函数经常在系统中的其它线程的运行状态一改变后,就主动调用。其目的是检测是否为此而发生了可能的抢占现象,若已发生,就立即进行抢占式切换。比如,改变了某其它线程的优先级,唤醒了某其他的线程,挂起恢复了某其他线程,给某线程挂入了一个APC等等操作后,都会调用,以尝试立即切换。
VOID FASTCALL //注意,这个函数只能在DISPATCH_LEVEL及其以上irql级别调用
KiExitDispatcher(IN KIRQL OldIrql) //降低irql,检测是否有抢占
{
PKPRCB Prcb = KeGetCurrentPrcb();
PKTHREAD Thread, NextThread;
BOOLEAN PendingApc;
ASSERT(KeGetCurrentIrql() >= DISPATCH_LEVEL); //确保
KiCheckDeferredReadyList(Prcb);
if (OldIrql >= DISPATCH_LEVEL)//如果要降回的irql不在DISPATCH_LEVEL以下,那就不能切换
{
if ((Prcb->NextThread) && !(Prcb->DpcRoutineActive))
HalRequestSoftwareInterrupt(DISPATCH_LEVEL);
goto Quickie;
}
if (!Prcb->NextThread)//如果没有抢占者线程,那很好,直接降低irql就是
goto Quickie;
//若发现有抢占发生,下面将执行抢占切换
KiAcquirePrcbLock(Prcb);
NextThread = Prcb->NextThread;
Thread = Prcb->CurrentThread;
KiSetThreadSwapBusy(Thread);
Prcb->CurrentThread = NextThread;
Prcb->NextThread = NULL;
NextThread->State = Running;
KxQueueReadyThread(Thread, Prcb);
Thread->WaitIrql = OldIrql;//可以肯定:OldIrql=APC_LEVEL或PASSIVE_LEVEL,并且:如果原irql是在AP_LEVEL的话,KiSwapContext内部会在返回前发出apc中断
PendingApc = KiSwapContext(Thread, NextThread);
-------------------------------------华丽的分割线---------------------------------------
//如果切回来后发现阻塞有内核apc,需要手动扫描执行apc(可以肯定原irql不是APC_LEVEL)
if (PendingApc)
{
ASSERT(OldIrql == PASSIVE_LEVEL);//可以肯定原来是PASSIVE_LEVEL级
KeLowerIrql(APC_LEVEL);//当然要先降到APC级别去
KiDeliverApc(KernelMode, NULL, NULL);//切换回来后,自己手动扫描执行内核apc
}
Quickie:
KeLowerIrql(OldIrql);//本函数真正的工作:降低到指定irql
}
//如上,上面的函数在降低irql前,先尝试检测是否发生了抢占式切换。若有,立即切换。
否则,降低irql。注意降低irql到DISPATCH_LEVEL下以后,也可能会因为之前时间片早已到期,但是在DISPATCH_LEVEL以上迟迟没有得到切换,现在降到下面了就会引发线程切换(迟来的切换!)
当一个线程被唤醒时(如isr中将某线程唤醒),往往会提高其优先级,导致发生抢占。一旦发现某个线程的优先级高于当前线程的优先级(并且也高于上一个候选的抢占者线程的优先级),系统就会把这个线程作为新的候选抢占者线程记录到KPCRB结构的NextThread字段中。这样,只要时机一成熟吗,就会发生抢占式切换。
下面的函数用来唤醒一个线程
VOID FASTCALL
KiUnwaitThread(IN PKTHREAD Thread,
IN LONG_PTR WaitStatus,
IN KPRIORITY Increment)//略微提高的优先级量(以便目标线程尽快得到调度)
{
KiUnlinkThread(Thread, WaitStatus);//从所有等待对象的线程链表中脱链
Thread->AdjustIncrement = (SCHAR)Increment;//要调整的优先级量
Thread->AdjustReason = AdjustUnwait;//跳转原因为唤醒
KiReadyThread(Thread);//关键函数。将线程转为就绪态
}
下面的函数用来将一个线程转为就绪态
VOID KiReadyThread(IN PKTHREAD Thread)
{
IN PKPROCESS Process = Thread->ApcState.Process;
if (Process->State != ProcessInMemory)
ASSERT(FALSE);//蓝屏
else if (!Thread->KernelStackResident)//如果该线程的内核栈被置换到外存了
{
ASSERT(Process->StackCount != MAXULONG_PTR);
Process->StackCount++;
ASSERT(Thread->State != Transition);
Thread->State = Transition;
ASSERT(FALSE);//蓝屏
}
else
KiInsertDeferredReadyList(Thread);//实质函数
}
VOID KiInsertDeferredReadyList(IN PKTHREAD Thread)
{
Thread->State = DeferredReady;//将进入就绪态
Thread->DeferredProcessor = 0;//0号cpu
KiDeferredReadyThread(Thread);//实质函数,就绪化指定线程
}
//下面的函数将指定线程转换为‘就绪态’或者‘抢占态’
//也可理解为‘就绪化’某个线程,但特殊处理抢占情形(抢占态是一种特殊的就绪态)
VOID FASTCALL KiDeferredReadyThread(IN PKTHREAD Thread)
{
PKPRCB Prcb;
BOOLEAN Preempted;
ULONG Processor = 0;//一律挂入0号cpu的就绪队列
KPRIORITY OldPriority;//目标线程的当前优先级
PKTHREAD NextThread;
if (Thread->AdjustReason == AdjustBoost) //if是线程首次启动时的调整优先级 。。。
else if (Thread->AdjustReason == AdjustUnwait) //if是唤醒时调整的优先级 。。。
Preempted = Thread->Preempted;
OldPriority = Thread->Priority;
Thread->Preempted = FALSE;
Thread->NextProcessor = 0;
Prcb = KiProcessorBlock[0];
KiAcquirePrcbLock(Prcb);
if (KiIdleSummary)//如果0号cpu运行着空转线程,目标线程的优先级肯定高于那个空转线程
{
KiIdleSummary = 0;
Thread->State = Standby;//将目标程序改为‘抢占态’
Prcb->NextThread = Thread;//指向自己
KiReleasePrcbLock(Prcb);
return;
}
Thread->NextProcessor = (UCHAR)Processor;//0
NextThread = Prcb->NextThread;//获得0号cpu上的原抢占者线程
if (NextThread)//如果原来已有一个抢占者线程
{
ASSERT(NextThread->State == Standby);//可以确定那个线程处于抢占态
if (OldPriority > NextThread->Priority)//若高于原‘抢占者线程’的优先级
{
NextThread->Preempted = TRUE;//标志那个抢占者线程又被目标线程抢占了
Prcb->NextThread = Thread;//更改新的抢占者线程,时机一成熟就抢占
Thread->State = Standby;//更为抢占态
NextThread->State = DeferredReady;//原抢占者线程进入将就绪态
NextThread->DeferredProcessor = Prcb->Number;//0
KiReleasePrcbLock(Prcb);
KiDeferredReadyThread(NextThread);//原抢占者线程转入0号cpu就绪队列
return;
}
}
else//如果原来没有抢占者线程(最典型的情况)
{
NextThread = Prcb->CurrentThread;
if (OldPriority > NextThread->Priority)//如果优先级高于当前运行的那个线程
{
if (NextThread->State == Running)
NextThread->Preempted = TRUE;//标记已被抢占
Prcb->NextThread = Thread; //指定抢占者线程,时机一成熟就抢占
Thread->State = Standby;//标记目标线程处于抢占态了
KiReleasePrcbLock(Prcb);
if (KeGetCurrentProcessorNumber() != 0)
KiIpiSend(AFFINITY_MASK(Thread->NextProcessor), IPI_DPC);//给0号cpu发一个通知
return;
}
}
//如果目标线程的优先级低于当前的抢占者线程,也低于当前运行中的线程
Thread->State = Ready;//更为就绪态
Thread->WaitTime = KeTickCount.LowPart;//记录上次被切换的时间
//如果目标线程上次是因为被抢占而切出的cpu,现在就挂入队头(平衡怒气)
Preempted ? InsertHeadList(&Prcb->DispatcherReadyListHead[OldPriority],
&Thread->WaitListEntry) :
InsertTailList(&Prcb->DispatcherReadyListHead[OldPriority],
&Thread->WaitListEntry);
Prcb->ReadySummary |= PRIORITY_MASK(OldPriority);//更改相应就绪队列的标志
KiReleasePrcbLock(Prcb);
}
如上,上面这个函数用于将线程挂入0号cpu的就绪队列或者置为抢占者线程。
四、进程、线程的优先级
线程的调度策略是严格按优先级的,因此,优先级,不妨叫做‘调度优先级’。那么优先级是啥,是怎么确定的呢?
先要弄清几个概念:
进程的优先级类:每种优先级类对应一种基本优先级
进程的基本优先级:为各个线程的默认基本优先级
线程的基本优先级:每个线程刚创建时的基本优先级继承它所属进程的基本优先级,但可以人为调整
线程的当前优先级:又叫时机优先级。当前优先级可以浮动,但永远不会降到该线程的基本优先级下面
系统调度线程时,是以线程的当前优先级为准的,它才不管你的基本优先级是什么,你所属的进程的基本优先级又是什么,它只看你的当前优先级。
进程基本优先级与线程基本优先级是一种水涨船高的关系。进程的基本优先级变高了,那么它里面的各个线程的基本优先级也会跟着升高对应的幅度。各个线程初始创建时的基本优先级等于其进程的基本优先级
线程的基本优先级与线程的当前优先级也是一种水涨船高的关系。线程的基本优先级升高了,那么线程的当前优先级也会跟着升高对应的幅度。另外:线程的当前优先级可以随时变化(比如每次一让出cpu时就略微降低那么一点点优先级),但是永远不会降到其基本优先级以下。基本优先级就是它的最低保障!
综上,可理解为:线程基本优先级相对于进程的基本优先级,线程的当前优先级相对于线程的基本优先级
线程1的当前优先级 线程2的当前优先级 线程3的当前优先级
线程1的基本优先级 线程2的基本优先级 线程3的基本优先级
进程的基本优先级
------------------------------------------------------------------------------------------
系统中总共分32个优先级:0到31,其中又分为两段。0到15的是非实时优先级,16-31的表示实时优先级。
#define LOW_PRIORITY 0
#define LOW_RELATIVE_PRIORITY 15 //最低的实时优先级
#define HIGH_PRIORITY 31//最高的实时优先级,也是整个系统最高的优先级
SetPriorityClass这个Win32 API改变的就是一个进程的优先级类,而一种优先级类对应一种基本优先级,所以这个函数实际上改变的是进程的基本优先级。实际上最终调用到下面的函数
KPRIORITY
KeSetPriorityAndQuantumProcess(IN PKPROCESS Process,
IN KPRIORITY Priority,//新的基本优先级
IN UCHAR Quantum OPTIONAL)//新的时间片
{
KLOCK_QUEUE_HANDLE ProcessLock;
KPRIORITY Delta;
PLIST_ENTRY NextEntry, ListHead;
KPRIORITY NewPriority, OldPriority;
PKTHREAD Thread;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
if (Process->BasePriority == Priority) return Process->BasePriority;
if (Priority==0) Priority = 1;//只有空转线程的优先级才能是0
KiAcquireProcessLock(Process, &ProcessLock);//获得自旋锁,同时提升irql到DISPATCH_LEVEL
if (Quantum)
Process->QuantumReset = Quantum;//修改进程的时间片(也即里面各个线程的时间片)
OldPriority = Process->BasePriority;
Process->BasePriority = (SCHAR)Priority;//修改为新的基本优先级
Delta = Priority - OldPriority;//计算提升幅度(注意Delta可以是负数)
ListHead = &Process->ThreadListHead;
NextEntry = ListHead->Flink;
if (Priority >= LOW_REALTIME_PRIORITY)//如果将基本优先级提到了实时级别
{
while (NextEntry != ListHead)//遍历该进程中的每个线程
{
Thread = CONTAINING_RECORD(NextEntry, KTHREAD, ThreadListEntry);
if (Quantum) Thread->QuantumReset = Quantum;//同时设置线程的时间片
KiAcquireThreadLock(Thread);
NewPriority = Thread->BasePriority + Delta;//水涨船高
if (NewPriority < LOW_REALTIME_PRIORITY)
NewPriority = LOW_REALTIME_PRIORITY;// 实时优先级的最小值
else if (NewPriority > HIGH_PRIORITY)
NewPriority = HIGH_PRIORITY;// 实时优先级的最大值
if (!(Thread->Saturation) || (OldPriority < LOW_REALTIME_PRIORITY))
{
Thread->BasePriority = (SCHAR)NewPriority; //水涨船高
Thread->Quantum = Thread->QuantumReset;//当前剩余时间片=初始时间片
Thread->PriorityDecrement = 0;
KiSetPriorityThread(Thread, NewPriority);//提高线程优先级要做的附加工作
}
KiReleaseThreadLock(Thread);
NextEntry = NextEntry->Flink;//下一个线程
}
}
else//如果将基本优先级提到了非实时级别
{
while (NextEntry != ListHead)
{
Thread = CONTAINING_RECORD(NextEntry, KTHREAD, ThreadListEntry);
if (Quantum) Thread->QuantumReset = Quantum;
KiAcquireThreadLock(Thread);
NewPriority = Thread->BasePriority + Delta;
if (NewPriority >= LOW_REALTIME_PRIORITY)
NewPriority = LOW_REALTIME_PRIORITY - 1;//非实时优先级的最大值
else if (NewPriority <= LOW_PRIORITY)
NewPriority = 1;//非实时优先级的最小值
if (!(Thread->Saturation) || (OldPriority >= LOW_REALTIME_PRIORITY))
{
Thread->BasePriority = (SCHAR)NewPriority;//水涨船高
Thread->Quantum = Thread->QuantumReset;//当前剩余时间片=初始的时间片
Thread->PriorityDecrement = 0;
KiSetPriorityThread(Thread, NewPriority); //提高线程优先级要做的附加工作
}
KiReleaseThreadLock(Thread);
NextEntry = NextEntry->Flink;//下一个线程
}
}
KiReleaseDispatcherLockFromDpcLevel();
KiReleaseProcessLockFromDpcLevel(&ProcessLock);
//降低到原irql,同时先检查是否发生了抢占式切换(因为显式改变了线程的优先级,有可能让其他线程的优先级突然高于了当前线程而要发生抢占现象,所以要检测这种情况)
KiExitDispatcher(ProcessLock.OldIrql);
return OldPriority;
}
线程的基本优先级一变了,它的当前优先级就会跟着变,线程的当前优先级一变了,那么就会有很多的附加工作要做,下面的函数就用来做这个工作(如改变就绪队列、置为抢占者等)。
VOID FASTCALL //设置线程的当前优先级
KiSetPriorityThread(IN PKTHREAD Thread,
IN KPRIORITY Priority)//新的当前优先级
{
PKPRCB Prcb;
ULONG Processor;
BOOLEAN RequestInterrupt = FALSE;
KPRIORITY OldPriority;
PKTHREAD NewThread;
if (Thread->Priority != Priority)//if 优先级变了
{
for (;;)
{
if (Thread->State == Ready)//如果目标线程处于就绪态
{
if (!Thread->ProcessReadyQueue)//其实一般都会满足这个条件
{
Processor = Thread->NextProcessor;
Prcb = KiProcessorBlock[Processor];
KiAcquirePrcbLock(Prcb);
//如果现在仍处于就绪态,并且仍在那个cpu上等待
if ((Thread->State == Ready) && (Thread->NextProcessor == Prcb->Number))
{
if (RemoveEntryList(&Thread->WaitListEntry))//从原就绪队列摘下
Prcb->ReadySummary ^= PRIORITY_MASK(Thread->Priority);
Thread->Priority = (SCHAR)Priority;//=更为新的优先级
KiInsertDeferredReadyList(Thread);//挂入新的就绪队列(或置为抢占态)
KiReleasePrcbLock(Prcb);
}
Else …
}
}
else if (Thread->State == Standby) //如果目标线程处于抢占态
{
Processor = Thread->NextProcessor;
Prcb = KiProcessorBlock[Processor];
KiAcquirePrcbLock(Prcb);
if (Thread == Prcb->NextThread)//如果仍处于抢占态
{
OldPriority = Thread->Priority;
Thread->Priority = (SCHAR)Priority;//更改优先级
if (Priority < OldPriority)//如果优先级降了(可能不再成为抢占者线程了)
{
NewThread = KiSelectReadyThread(Priority + 1, Prcb);
if (NewThread)//如果选出了一个比现在的优先级更高的线程
{
NewThread->State = Standby;
Prcb->NextThread = NewThread;//更为新的抢占者线程
KiInsertDeferredReadyList(Thread);//原抢占线程则转入就绪队列
}
}
KiReleasePrcbLock(Prcb);
}
Else …
}
else if (Thread->State == Running) //如果目标线程正在运行
{
Processor = Thread->NextProcessor;
Prcb = KiProcessorBlock[Processor];
KiAcquirePrcbLock(Prcb);
if (Thread == Prcb->CurrentThread)//如果仍在运行
{
OldPriority = Thread->Priority;
Thread->Priority = (SCHAR)Priority;//更改优先级
if ((Priority < OldPriority) && !(Prcb->NextThread))//可能会出现抢占
{
NewThread = KiSelectReadyThread(Priority + 1, Prcb);
if (NewThread)// 如果选出了一个比现在的优先级更高的线程
{
NewThread->State = Standby;
Prcb->NextThread = NewThread;//出现了新的抢占线程
RequestInterrupt = TRUE;//需要立即中断
}
}
KiReleasePrcbLock(Prcb);
if (RequestInterrupt)
{
//通知目标cpu进行抢占切换
if (KeGetCurrentProcessorNumber() != Processor)
KiIpiSend(AFFINITY_MASK(Processor), IPI_DPC);
}
}
Else …
}
Else …
break;
}
}
}
如上,这个函数改变目标线程的优先级为指定优先级,并根据目标线程的当前所处状态,最对应的就绪队列、抢占者线程调整。可见,强行改变某个线程的当前优先级并不是件简单的工作,需要全盘综合考虑各方面因素,做出相应的调整。
下面的函数是一个小型的封装函数:(他还会还原时间片)
KPRIORITY
KeSetPriorityThread(IN PKTHREAD Thread,
IN KPRIORITY Priority)
{
KIRQL OldIrql;
KPRIORITY OldPriority;
OldIrql = KiAcquireDispatcherLock();
KiAcquireThreadLock(Thread);
OldPriority = Thread->Priority;
Thread->PriorityDecrement = 0;
if (Priority != Thread->Priority)//if 优先级变了
{
Thread->Quantum = Thread->QuantumReset;//关键。还原时间片
KiSetPriorityThread(Thread, Priority);//再做真正的修改工作
}
KiReleaseThreadLock(Thread);
KiReleaseDispatcherLock(OldIrql);
return OldPriority;
}
除了修改进程的基本优先级会影响到里面每个线程的基本优先级和当前优先级外,也可以用下面的函数直接修改线程的基本优先级和当前优先级。
NTSTATUS
NtSetInformationThread(IN HANDLE ThreadHandle,
IN THREADINFOCLASS ThreadInformationClass,
IN PVOID ThreadInformation,
IN ULONG ThreadInformationLength)
{
…
switch (ThreadInformationClass)
{
case ThreadPriority://设置当前优先级
Priority = *(PLONG)ThreadInformation;//这个值是相对于进程基本优先级的差值
KeSetPriorityThread(&Thread->Tcb, Priority);
break;
case ThreadBasePriority://设置基本优先级
Priority = *(PLONG)ThreadInformation;
KeSetBasePriorityThread(&Thread->Tcb, Priority);
break;
case …
}//end switch
}//end func
线程的基本优先级(非当前优先级)可以用下面的函数设置:
LONG
KeSetBasePriorityThread(IN PKTHREAD Thread,
IN LONG Increment)//这个是相对于进程基本优先级的差值
{
KIRQL OldIrql;
KPRIORITY OldBasePriority, Priority, BasePriority;
LONG OldIncrement;
PKPROCESS Process;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
Process = Thread->ApcState.Process;
OldIrql = KiAcquireDispatcherLock();
KiAcquireThreadLock(Thread);
OldBasePriority = Thread->BasePriority;
OldIncrement = OldBasePriority - Process->BasePriority;
if (Thread->Saturation) //如果是个饱和增量
OldIncrement = 16 * Thread->Saturation;//16或-16
Thread->Saturation = 0;
if (abs(Increment) >= 16) //饱和增量
Thread->Saturation = (Increment > 0) ? 1 : -1;
BasePriority = Process->BasePriority + Increment;//算得现在的基本优先级
if (Process->BasePriority >= LOW_REALTIME_PRIORITY)
{
Priority = BasePriority;//实时线程例外,当前优先级=基本优先级
}
else
{
Priority = KiComputeNewPriority(Thread, 0);//其实就是当前优先级
//看到没,线程的基本优先级一升高,它的当前优先级跟着升高对应的幅度
Priority += (BasePriority - OldBasePriority);
}
Thread->BasePriority = (SCHAR)BasePriority;//更改线程的基本优先级
Thread->PriorityDecrement = 0;
if (Priority != Thread->Priority)//如果当前优先级变了,做相关的附加工作
{
Thread->Quantum = Thread->QuantumReset;
KiSetPriorityThread(Thread, Priority);
}
KiReleaseThreadLock(Thread);
KiReleaseDispatcherLock(OldIrql);
return OldIncrement;
}
五、线程局部存储:TLS
----对TLS这个概念陌生的朋友请先自己查阅相关资料。
TLS分为两种方法:静态tls、动态tls。两种方法都可以达到tls的目的。
静态tls:
在编写程序时:只需在要声明为tls的全局变量前加上__declspec(thread)关键字即可。如:
__declspec(thread) int g_a = 1;
__declspec(thread) int g_b;
__declspec(thread) int g_c = 0;
__declspec(thread) int g_d;
编译器在遇到这样的变量时,自然会将这种变量当做tls变量看待,编译链接存放到pe文件的.tls节中,
Exe文件中可使用静态tls,动态库文件中使用静态tls则会有很大的缺点,所以动态库文件中一般都使用动态tls来达到tls的目的。为此,Windows专门提供了一组api和相关基础设施来实现动态tls。
DWORD TlsAlloc():为当前线程分配一个tls槽。返回本线程分得的槽号
BOOL TlsSetValue(DWORD idx,void* val):写数据到指定槽中
VOID* TlsGetValue(DWORD idx ):从指定槽中读数据
BOOL TlsFree(DWORD idx);//释放这个槽给进程,使得其他线程可以分得这个槽
相关的结构:
Struct PEB
{
…
RTL_BITMAP* TlsBitmap;//标准的64位动态tls分配标志位图(固定使用下面的64位结构)
DWORD TlsBitmapBits[2];//内置的64bit大小的tls位图(每一位标志表示对应tls槽的分配情况)
…
}
Struct RTL_BITMAP
{
ULONG SizeOfBitmap;//动态tls位图的大小,默认就是8B(64bit)
BYTE* Buffer;//动态tls位图的地址,默认就指向PEB结构中的那个内置的tls位图。当要使用的tls槽个数超过64个时,将使用扩展的tls位图。
}
Struct TEB
{
…
Void* ThreadLocalStoragePointer;//本线程的那片静态tls区的地址
Void* TlsSlots[64];//内置的64个tls槽(每个槽中可以存放4B大小的任意数据)
Void* TlsExpansionSlots;//另外扩展的1024个tls槽
…
}
下面的函数分配一个空闲的tls槽,返回分到的槽号(即索引)
DWORD TlsAlloc()
{
ULONG Index;
RtlAcquirePebLock();
//先从标准的64位tls位图中找到一个空闲的tls槽(也即未被其他线程占用的tls槽)
Index = RtlFindClearBitsAndSet(NtCurrentPeb()->TlsBitmap, 1, 0);
if (Index == -1)//如果找不到
{
//再去扩展的tls槽位图中查找
Index = RtlFindClearBitsAndSet(NtCurrentPeb()->TlsExpansionBitmap, 1, 0);
if (Index != -1)//如果找到了
{
if (NtCurrentTeb()->TlsExpansionSlots == NULL)
{
NtCurrentTeb()->TlsExpansionSlots = HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,1024 * sizeof(PVOID));
}
NtCurrentTeb()->TlsExpansionSlots[Index] = 0;//分到对应的槽后,自动将内容清0
Index += 64;
}
else
SetLastError(ERROR_NO_MORE_ITEMS);
}
else
NtCurrentTeb()->TlsSlots[Index] = 0; //分到对应的槽后,自动将内容清0
RtlReleasePebLock();
return Index;
}
下面的函数将数据写入指定tls槽中
BOOL TlsSetValue(DWORD Index, LPVOID Value)
{
if (Index >= 64) //扩展tls槽中
{
if (NtCurrentTeb()->TlsExpansionSlots == NULL)
{
NtCurrentTeb()->TlsExpansionSlots = HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,1024 *sizeof(PVOID));
}
NtCurrentTeb()->TlsExpansionSlots[Index - 64] = Value;
}
else
NtCurrentTeb()->TlsSlots[Index] = Value;
return TRUE;
}
下面的函数读取指定tls槽中的值
LPVOID TlsGetValue(DWORD Index)
{
if (Index >= 64)
return NtCurrentTeb()->TlsExpansionSlots[Index - 64];
else
return NtCurrentTeb()->TlsSlots[Index];
}
下面的函数用来释放一个tls槽给进程
BOOL TlsFree(DWORD Index)
{
BOOL BitSet;
RtlAcquirePebLock();
if (Index >= 64)
{
//检测该tls槽是否已分配
BitSet = RtlAreBitsSet(NtCurrentPeb()->TlsExpansionBitmap,Index - 64,1);
if (BitSet)//若已分配,现在标记为空闲
RtlClearBits(NtCurrentPeb()->TlsExpansionBitmap,Index - 64,1);
}
else
{
BitSet = RtlAreBitsSet(NtCurrentPeb()->TlsBitmap, Index, 1);
if (BitSet)
RtlClearBits(NtCurrentPeb()->TlsBitmap, Index, 1);
}
if (BitSet)
{
//将所有线程的对应tls槽内容清0
NtSetInformationThread(NtCurrentThread(),ThreadZeroTlsCell,&Index,sizeof(DWORD));
}
else
SetLastError(ERROR_INVALID_PARAMETER);
RtlReleasePebLock();
return BitSet;
}
上面这些关于动态tls的函数都不难理解。动态tls功能强大,但使用起来不方便。静态tls不好用在动态库中,比较局限,但静态tls使用方便。话又说回来,静态的tls的使用方便背后,又包含着较为复杂的初始化流程。下面看静态tls的初始化流程。
回顾一下进程创建时的启动流程:
在进程启动时,初始化主exe文件的函数内部:
PEFUNC LdrPEStartup(…)
{
…
Status = LdrFixupImports(NULL, *Module);//加载子孙dll,修正IAT导入表
Status = LdrpInitializeTlsForProccess();//初始化进程的静态tls
if (NT_SUCCESS(Status))
{
LdrpAttachProcess();//发送一个ProcessAttach消息,调用该模块的DllMain函数
LdrpTlsCallback(*Module, DLL_PROCESS_ATTACH);//调用各模块的tls回调函数
}
…
}
钻进各个函数里面去看一下:
NTSTATUS LdrFixupImports(…)
{
…
if (TlsDirectory)
{
TlsSize = TlsDirectory->EndAddressOfRawData- TlsDirectory->StartAddressOfRawData
+ TlsDirectory->SizeOfZeroFill;
if (TlsSize > 0 && NtCurrentPeb()->Ldr->Initialized)//if 动态加载该模块
TlsDirectory = NULL;// 动态加载的模块不支持静态tls
}
…
if (TlsDirectory && TlsSize > 0)//处理静态加载的dll模块中的静态tls节
LdrpAcquireTlsSlot(Module, TlsSize, FALSE);
…
}
在修正每个exe、dll文件的导入表时,会检查该文件中.tls节的大小。由于这个函数本身也会被LoadLibrary函数在内部调用,所以,这个函数他会检测是不是在动态加载dll,若是,如果发现dll中含有静态tls节,就什么都不做。反之,若dll是在进程启动阶段静态加载的,就会调用LdrpAcquireTlsSlot处理那个模块中的tls节。具体是怎么处理的呢?我们看:
VOID LdrpAcquireTlsSlot(PLDR_DATA_TABLE_ENTRY Module, ULONG Size, BOOLEAN Locked)
{
if (!Locked)
RtlEnterCriticalSection (NtCurrentPeb()->LoaderLock);
Module->TlsIndex = LdrpTlsCount;//记录这个模块tls节的索引(即tls号)
LdrpTlsCount++;//递增进程中的tls节个数
LdrpTlsSize += Size;//递增进程中tls节总大小
if (!Locked)
RtlLeaveCriticalSection(NtCurrentPeb()->LoaderLock);
}
如上,每个模块在进程启动时的静态加载过程中,只是递增一下进程中总的tls节个数与大小,以及分配该模块的tls节编号,以便在进程完全初始化完成(即加载了所有模块)后,统一集中处理各模块中的静态tls节。
下面再看LdrPEStartup函数中调用的LdrpInitializeTlsForProccess函数,显然,这个函数是在LdrFixupImports函数加载了该exe依赖的所有子孙dll文件后才调用的。前面已经统计完了该进程中所有模块的所有tls节的总大小以及tls节总个数,现在就到调用这个函数集中统一处理该进程的静态tls时候了。我们看:
NTSTATUS LdrpInitializeTlsForProccess()
{
PLIST_ENTRY ModuleListHead;
PLIST_ENTRY Entry;
PLDR_DATA_TABLE_ENTRY Module;
PIMAGE_TLS_DIRECTORY TlsDirectory;
PTLS_DATA TlsData;
ULONG Size;
if (LdrpTlsCount > 0) //如果有模块中存在tls节
{
//分配一个tls描述符数组,用来记录各模块的tls节信息(注意分配的只是描述符,并不用来存放tls节体。另外,每个进程的tls描述符数组都记录在ntdll.dll模块中的LdrpTlsArray全局变量中)
LdrpTlsArray = RtlAllocateHeap(RtlGetProcessHeap(),0,
LdrpTlsCount * sizeof(TLS_DATA));
ModuleListHead = &NtCurrentPeb()->Ldr->InLoadOrderModuleList;
Entry = ModuleListHead->Flink;
while (Entry != ModuleListHead)//遍历所有含有tls节的静态加载模块
{
Module = CONTAINING_RECORD(Entry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
if (Module->LoadCount ==-1 && Module->TlsIndex != -1)
{
//获得pe文件中tls目录的信息
TlsDirectory = RtlImageDirectoryEntryToData(Module->DllBase,
TRUE,IMAGE_DIRECTORY_ENTRY_TLS,&Size);
TlsData = &LdrpTlsArray[Module->TlsIndex];//指向该模块对应的描述符
//非0区在原模块中的地址
TlsData->StartAddressOfRawData = TlsDirectory->StartAddressOfRawData;
//非0区的大小
TlsData->TlsDataSize = TlsDirectory->EndAddressOfRawData - TlsDirectory->
StartAddressOfRawData;
//0区的大小(即尚未初始化的tls变量总大小)
TlsData->TlsZeroSize = TlsDirectory->SizeOfZeroFill;
//tls回调函数数组的地址
if (TlsDirectory->AddressOfCallBacks)
TlsData->TlsAddressOfCallBacks = TlsDirectory->AddressOfCallBacks;
else
TlsData->TlsAddressOfCallBacks = NULL;
TlsData->Module = Module;//该tls节所在的原模块
//重要。回填到原模块中,该tls节分得的索引。(写复制机制可确保各进程一份)
*(PULONG)TlsDirectory->AddressOfIndex = Module->TlsIndex;
}
Entry = Entry->Flink;
}
}
return STATUS_SUCCESS;
}
如上,这个函数为进程建立起一个tls描述符数组。
typedef struct _TLS_DATA //tls节描述符
{
PVOID StartAddressOfRawData; //非0区在原模块中的地址
DWORD TlsDataSize;// 非0区的大小
DWORD TlsZeroSize;// 0区大小
PIMAGE_TLS_CALLBACK *TlsAddressOfCallBacks;//回调函数数组
PLDR_DATA_TABLE_ENTRY Module;//所在模块
} TLS_DATA, *PTLS_DATA;
非0区与0区是什么意思呢?tls节中各个变量可能有的没有初值,凡是没有初值的tls的变量都被安排到tls节的末尾,并且不予分配文件空间(这样,可以节省文件体积),只记录他们的总字节数即可。
__declspec(thread) int g_a = 1;//已初始化,被安排到tls节中的非0区
__declspec(thread) int g_b;//被安排到0区
__declspec(thread) int g_c = 0;//已初始化,被安排到tls节中的非0区
__declspec(thread) int g_d; //被安排到0区
所有未予初始化的tls变量都默认赋予初值0。
最后:每当一个线程创建时的初始化工作如下:
NTSTATUS
LdrpAttachThread (VOID)
{
。。。
Status = LdrpInitializeTlsForThread();//关键处。初始化每个线程的静态tls
调用各dll的DllMain,略
return Status;
}
如上,每当一个线程初始运行时,除了会调用进程中各个dll的DllMain函数外,还会初始化自己的静态tls,建立起本线程独立的一份静态tls副本。如下:
NTSTATUS LdrpInitializeTlsForThread(VOID)
{
PVOID* TlsPointers;
PTLS_DATA TlsInfo;
PVOID TlsData;
ULONG i;
PTEB Teb = NtCurrentTeb();
Teb->StaticUnicodeString.Length = 0;
Teb->StaticUnicodeString.MaximumLength = sizeof(Teb->StaticUnicodeBuffer);
Teb->StaticUnicodeString.Buffer = Teb->StaticUnicodeBuffer;
if (LdrpTlsCount > 0)//如果本进程中有包含tls节的静态模块
{
//将各模块内部的tls节提取出来,连成一片,形成一块‘tls片区’
TlsPointers = RtlAllocateHeap(RtlGetProcessHeap(),0,
LdrpTlsCount * sizeof(PVOID) + LdrpTlsSize);//头部指针数组+所有tls块的总大小
//指向头部后面的各tls节体部分
TlsData = (PVOID)((ULONG_PTR)TlsPointers + LdrpTlsCount * sizeof(PVOID));
Teb->ThreadLocalStoragePointer = TlsPointers;//指向本线程自己的那份tls的头部
TlsInfo = LdrpTlsArray;//指向本进程的tls描述符数组
for (i = 0; i < LdrpTlsCount; i++, TlsInfo++)
{
TlsPointers[i] = TlsData;//将数组指针指向对应的tls块
if (TlsInfo->TlsDataSize)
{
//提取对应模块内部的tls节体(非0区部分)到这儿来
memcpy(TlsData, TlsInfo->StartAddressOfRawData, TlsInfo->TlsDataSize);
TlsData = (PVOID)((ULONG_PTR)TlsData + TlsInfo->TlsDataSize);
}
if (TlsInfo->TlsZeroSize)//0区部分
{
memset(TlsData, 0, TlsInfo->TlsZeroSize);//自动初始化为0
TlsData = (PVOID)((ULONG_PTR)TlsData + TlsInfo->TlsZeroSize);//跨过0区部分
}
}
}
return STATUS_SUCCESS;
}
看到没,每个线程诞生之初,就将进程中各模块内部的tls节提取出来,复制到一个集中的地方存放,这样,
吗,每个线程都建立了一份自己连续的tls片区。以后,要访问tls变量时,访问的都是自己的那份tls片区,
当然,如何访问?这离不开编译器对静态tls机制提供的支持。
编译器在遇到__declspec(thread)关键字时,会认为那个变量是tls变量,将之编译链接到pe文件的.tls节中存放,另外每条访问tls变量的高级语句都被做了恰当的编译。每个tls变量都被编译为二级地址:
“Tls节号.节内偏移”,每个模块的tls节号(即索引)保存在那个模块的tls目录中的某个固定字段中(详见: *(PULONG)TlsDirectory->AddressOfIndex = Module->TlsIndex 这条语句),这样,编译器从模块的这个位置取得该模块的tls节分得的节号,以此节号为索引,根据TEB中的保存的那块“tls片区”的头部数组,找到对应于本模块tls节副本的位置,然后加上该tls变量在节内的偏移,就正确找到对应的内存单元了。
六、进程挂靠与跨进程操作
前面总在说:“将一个线程挂靠到其他进程的地址空间”,这是怎么回事?现在就来看一下。
当父进程要创建一个子进程时:会在父进程中调用CreateProcess。这个函数本身是运行在父进程的地址空间中的,但是由它创建了子进程,创建了子进程的地址空间,创建了子进程的PEB。当要初始化子进程的PEB结构时,由于PEB本身位于子进程的地址空间中,如果直接访问PEB那是不对的,那将会映射到不同的物理内存。所以必须挂靠到子进程的地址空间中,去读写PEB结构体中的值。下面的函数就是用来挂靠的
VOID KeAttachProcess(IN PKPROCESS Process) //将当前线程挂靠到指定进程的地址空间
{
KLOCK_QUEUE_HANDLE ApcLock;
PKTHREAD Thread = KeGetCurrentThread();
if (Thread->ApcState.Process == Process) return;//如果已经位于目标进程,返回
if ((Thread->ApcStateIndex != OriginalApcEnvironment) || (KeIsExecutingDpc()))
KeBugCheckEx(~);//蓝屏错误
else
{
KiAcquireApcLock(Thread, &ApcLock);
KiAcquireDispatcherLockAtDpcLevel();//挂靠过程操作过程中禁止线程切换
KiAttachProcess(Thread, Process, &ApcLock, &Thread->SavedApcState);//实质函数
}
}
VOID
KiAttachProcess(IN PKTHREAD Thread,//指定线程
IN PKPROCESS Process,//要挂靠到的目标进程
IN PKLOCK_QUEUE_HANDLE ApcLock,
IN PRKAPC_STATE SavedApcState)//保存原apc队列状态
{
Process->StackCount++;//目标线程的内核栈个数递增(也即增加线程个数)
KiMoveApcState(&Thread->ApcState, SavedApcState);//复制保存原apc队列状态
//每当一挂靠,必然要清空原apc队列
InitializeListHead(&Thread->ApcState.ApcListHead[KernelMode]);
InitializeListHead(&Thread->ApcState.ApcListHead[UserMode]);
Thread->ApcState.Process = Process;//关键。将表示当前进程的字段更为目标进程
Thread->ApcState.KernelApcInProgress = FALSE;
Thread->ApcState.KernelApcPending = FALSE;
Thread->ApcState.UserApcPending = FALSE;
if (SavedApcState == &Thread->SavedApcState)//一般满足
{
//修改指向,但不管怎么修改,ApcState字段总是表示当前apc状态
Thread->ApcStatePointer[OriginalApcEnvironment] = &Thread->SavedApcState;
Thread->ApcStatePointer[AttachedApcEnvironment] = &Thread->ApcState;
Thread->ApcStateIndex = AttachedApcEnvironment;
}
if (Process->State == ProcessInMemory)//if 没被置换出去
{
KiReleaseDispatcherLockFromDpcLevel();
KiReleaseApcLockFromDpcLevel(ApcLock);
KiSwapProcess(Process, SavedApcState->Process);//实质函数
//调用这个函数的目的是检测可能的抢占切换条件是否已发生。(若已发生就赶紧切换)
KiExitDispatcher(ApcLock->OldIrql);//降到指定irql(同时检查是否发生了抢占式切换)
}
Else …
}
实质性的函数是KiSwapProcess,继续看
VOID KiSwapProcess(IN PKPROCESS NewProcess,IN PKPROCESS OldProcess)
{
PKIPCR Pcr = (PKIPCR)KeGetPcr();
//关键。修改cr3(存放进程页目录的物理地址)寄存器为目标进程的页表
__writecr3(NewProcess->DirectoryTableBase[0]);
Ke386SetGs(0);//将gs寄存器清0
Pcr->TSS->IoMapBase = NewProcess->IopmOffset;//修改当前线程的IO权限位图为目标进程的那份
}
看到没,进程挂靠的实质工作,就是将cr3寄存器改为目标寄存器的地址空间,这样,线程的所有有关内存的操作,操作的都是目标进程的地址空间。
明白了进程挂靠后,理解跨进程操作就很容易了。
一个进程可以调用OpenProcess打开另一个进程,取得目标进程的句柄后,就可调用VirtualAllocEx、WriteProcessMemory、ReadProcessMemory、CreateRemoteThread等函数操作那个进程的地址空间。这些跨进程操作的函数功能强大,而且带有破坏性,以至于往往被杀毒软件重点封杀,特别是CreateRemoteThread这个函数,冤啊。相关的示例代码如下图所示:
所有的跨进程操作都必经一步:打开目标进程。(这是一道需要重点把手的关口)
HANDLE
OpenProcess(DWORD dwDesiredAccess,//申请的权限
BOOL bInheritHandle,//指本次打开得到的句柄是否可继承给子进程
DWORD dwProcessId)//目标进程的pid
{
NTSTATUS errCode;
HANDLE ProcessHandle;
OBJECT_ATTRIBUTES ObjectAttributes;
CLIENT_ID ClientId;
ClientId.UniqueProcess = UlongToHandle(dwProcessId);
ClientId.UniqueThread = 0;
InitializeObjectAttributes(&ObjectAttributes,NULL,
(bInheritHandle ? OBJ_INHERIT : 0),NULL,NULL);
//调用系统服务打开进程
errCode = NtOpenProcess(&ProcessHandle,dwDesiredAccess,&ObjectAttributes,&ClientId);
if (!NT_SUCCESS(errCode))
{
SetLastErrorByStatus(errCode);
return NULL;
}
return ProcessHandle;
}
NTSTATUS
NtOpenProcess(OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId)//pid.tid
{
KPROCESSOR_MODE PreviousMode = KeGetPreviousMode();
ULONG Attributes = 0;
BOOLEAN HasObjectName = FALSE;
PETHREAD Thread = NULL;
PEPROCESS Process = NULL;
if (PreviousMode != KernelMode)
{
_SEH2_TRY
{
ProbeForWriteHandle(ProcessHandle);
if (ClientId)
{
ProbeForRead(ClientId, sizeof(CLIENT_ID), sizeof(ULONG));
SafeClientId = *ClientId;
ClientId = &SafeClientId;
}
ProbeForRead(ObjectAttributes,sizeof(OBJECT_ATTRIBUTES),sizeof(ULONG));
HasObjectName = (ObjectAttributes->ObjectName != NULL);
Attributes = ObjectAttributes->Attributes;
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
else
{
HasObjectName = (ObjectAttributes->ObjectName != NULL);
Attributes = ObjectAttributes->Attributes;
}
if ((HasObjectName) && (ClientId))//不能同时给定进程名与id
return STATUS_INVALID_PARAMETER_MIX;
//传递当前令牌以及要求的权限到AccessState中
Status = SeCreateAccessState(&AccessState,&AuxData,DesiredAccess,
&PsProcessType->TypeInfo.GenericMapping);
//检查当前令牌是否具有调试特权(这就是为什么经常在打开目标进程前要启用调试特权)
if (SeSinglePrivilegeCheck(SeDebugPrivilege, PreviousMode))
{
if (AccessState.RemainingDesiredAccess & MAXIMUM_ALLOWED)
AccessState.PreviouslyGrantedAccess |= PROCESS_ALL_ACCESS;
else
AccessState.PreviouslyGrantedAccess |=AccessState.RemainingDesiredAccess;
AccessState.RemainingDesiredAccess = 0;
}
if (HasObjectName) //以对象名的方式查找该进程对象
{
Status = ObOpenObjectByName(ObjectAttributes,PsProcessType,PreviousMode,
&AccessState,0,NULL,&hProcess);
SeDeleteAccessState(&AccessState);
}
else if (ClientId)
{
if (ClientId->UniqueThread)//根据tid查找线程、进程对象
Status = PsLookupProcessThreadByCid(ClientId, &Process, &Thread);
Else //根据pid从获活动进程链表中查找进程对象,最常见
Status = PsLookupProcessByProcessId(ClientId->UniqueProcess,&Process);
if (!NT_SUCCESS(Status))
{
SeDeleteAccessState(&AccessState);
return Status;
}
//在该进程对象上打开一个句柄
Status = ObOpenObjectByPointer(Process,Attributes,&AccessState,0,
PsProcessType,PreviousMode,&hProcess);
SeDeleteAccessState(&AccessState);
if (Thread)
ObDereferenceObject(Thread);
ObDereferenceObject(Process);
}
else
return STATUS_INVALID_PARAMETER_MIX;
if (NT_SUCCESS(Status))
{
_SEH2_TRY
{
*ProcessHandle = hProcess;//返回打开得到的进程句柄
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
}
return Status;
}
如上,这个函数在检测权限满足后,就打开目标进程,返回一个句柄给调用者。
看下面的典型跨进程写数据函数:
NTSTATUS
NtWriteVirtualMemory(IN HANDLE ProcessHandle,//远程进程
IN PVOID BaseAddress,
IN PVOID Buffer,
IN SIZE_T NumberOfBytesToWrite,
OUT PSIZE_T NumberOfBytesWritten OPTIONAL)
{
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
PEPROCESS Process;
NTSTATUS Status = STATUS_SUCCESS;
SIZE_T BytesWritten = 0;
if (PreviousMode != KernelMode)
{
if ((((ULONG_PTR)BaseAddress + NumberOfBytesToWrite) < (ULONG_PTR)BaseAddress) ||
(((ULONG_PTR)Buffer + NumberOfBytesToWrite) < (ULONG_PTR)Buffer) ||
(((ULONG_PTR)BaseAddress + NumberOfBytesToWrite) > MmUserProbeAddress) ||
(((ULONG_PTR)Buffer + NumberOfBytesToWrite) > MmUserProbeAddress))
{
return STATUS_ACCESS_VIOLATION;
}
_SEH2_TRY
{
if (NumberOfBytesWritten) ProbeForWriteSize_t(NumberOfBytesWritten);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
if (NumberOfBytesToWrite)
{
Status = ObReferenceObjectByHandle(ProcessHandle,PROCESS_VM_WRITE,PsProcessType,
PreviousMode, (PVOID*)&Process,NULL);
if (NT_SUCCESS(Status))
{
Status = MmCopyVirtualMemory(PsGetCurrentProcess(),Buffer,Process,
BaseAddress,NumberOfBytesToWrite,
PreviousMode,&BytesWritten);
ObDereferenceObject(Process);
}
}
if (NumberOfBytesWritten)
{
_SEH2_TRY
{
*NumberOfBytesWritten = BytesWritten;
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
}
_SEH2_END;
}
return Status;
}
NTSTATUS
MmCopyVirtualMemory(IN PEPROCESS SourceProcess,
IN PVOID SourceAddress,
IN PEPROCESS TargetProcess,
OUT PVOID TargetAddress,
IN SIZE_T BufferSize,
IN KPROCESSOR_MODE PreviousMode,
OUT PSIZE_T ReturnSize)
{
NTSTATUS Status;
PEPROCESS Process = SourceProcess;
if (SourceProcess == PsGetCurrentProcess()) Process = TargetProcess;
if (BufferSize > 512)//需要使用MDL
{
Status = MiDoMappedCopy(SourceProcess,SourceAddress,TargetProcess,TargetAddress,
BufferSize,PreviousMode,ReturnSize);
}
else
{
Status = MiDoPoolCopy(SourceProcess,SourceAddress,TargetProcess,TargetAddress,
BufferSize,PreviousMode,ReturnSize);
}
return Status;
}
NTSTATUS
MiDoMappedCopy(IN PEPROCESS SourceProcess,
IN PVOID SourceAddress,
IN PEPROCESS TargetProcess,
OUT PVOID TargetAddress,
IN SIZE_T BufferSize,
IN KPROCESSOR_MODE PreviousMode,
OUT PSIZE_T ReturnSize)
{
PFN_NUMBER MdlBuffer[(sizeof(MDL) / sizeof(PFN_NUMBER)) + MI_MAPPED_COPY_PAGES + 1];
PMDL Mdl = (PMDL)MdlBuffer;
SIZE_T TotalSize, CurrentSize, RemainingSize;
volatile BOOLEAN FailedInProbe = FALSE, FailedInMapping = FALSE, FailedInMoving;
volatile BOOLEAN PagesLocked;
PVOID CurrentAddress = SourceAddress, CurrentTargetAddress = TargetAddress;
volatile PVOID MdlAddress;
KAPC_STATE ApcState;
BOOLEAN HaveBadAddress;
ULONG_PTR BadAddress;
NTSTATUS Status = STATUS_SUCCESS;
TotalSize = 14 * PAGE_SIZE;//每次拷贝14个页面大小
if (BufferSize <= TotalSize) TotalSize = BufferSize;
CurrentSize = TotalSize;
RemainingSize = BufferSize;
while (RemainingSize > 0)
{
if (RemainingSize < CurrentSize) CurrentSize = RemainingSize;
KeStackAttachProcess(&SourceProcess->Pcb, &ApcState);//挂靠到源进程
MdlAddress = NULL;
PagesLocked = FALSE;
FailedInMoving = FALSE;
_SEH2_TRY
{
if ((CurrentAddress == SourceAddress) && (PreviousMode != KernelMode))
{
FailedInProbe = TRUE;
ProbeForRead(SourceAddress, BufferSize, sizeof(CHAR));
FailedInProbe = FALSE;
}
MmInitializeMdl(Mdl, CurrentAddress, CurrentSize);
MmProbeAndLockPages(Mdl, PreviousMode, IoReadAccess);
PagesLocked = TRUE;
MdlAddress = MmMapLockedPagesSpecifyCache(Mdl,KernelMode,MmCached, NULL,
FALSE,HighPagePriority);
KeUnstackDetachProcess(&ApcState);//撤销挂靠
KeStackAttachProcess(&TargetProcess->Pcb, &ApcState);//挂靠到目标进程
if ((CurrentAddress == SourceAddress) && (PreviousMode != KernelMode))
{
FailedInProbe = TRUE;
ProbeForWrite(TargetAddress, BufferSize, sizeof(CHAR));
FailedInProbe = FALSE;
}
FailedInMoving = TRUE;
RtlCopyMemory(CurrentTargetAddress, MdlAddress, CurrentSize);//拷贝
}
_SEH2_EXCEPT()。。。
if (Status != STATUS_SUCCESS) return Status;
KeUnstackDetachProcess(&ApcState);
MmUnmapLockedPages(MdlAddress, Mdl);
MmUnlockPages(Mdl);
RemainingSize -= CurrentSize;
CurrentAddress = (PVOID)((ULONG_PTR)CurrentAddress + CurrentSize);
CurrentTargetAddress = (PVOID)((ULONG_PTR)CurrentTargetAddress + CurrentSize);
}
*ReturnSize = BufferSize;
return STATUS_SUCCESS;
}
看到没,要挂靠到目标进程中去复制数据。如果源进程不是当前进程,还要先挂靠到源进程中。
七、线程的挂起与恢复
SuspendThread->NtSuspendThread->PsSuspenThread-> KeSuspendThread,直接看KeSuspendThread函数
ULONG KeSuspendThread(PKTHREAD Thread)
{
KLOCK_QUEUE_HANDLE ApcLock;
ULONG PreviousCount;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
KiAcquireApcLock(Thread, &ApcLock);
PreviousCount = Thread->SuspendCount;
if (Thread->ApcQueueable)
{
Thread->SuspendCount++;//递增挂起计数
if (!(PreviousCount) && !(Thread->FreezeCount))
{
if (!Thread->SuspendApc.Inserted)//if尚未插入那个‘挂起APC’
{
Thread->SuspendApc.Inserted = TRUE;
KiInsertQueueApc(&Thread->SuspendApc, IO_NO_INCREMENT);//插入‘挂起APC’
}
else
{
KiAcquireDispatcherLockAtDpcLevel();
Thread->SuspendSemaphore.Header.SignalState--;
KiReleaseDispatcherLockFromDpcLevel();
}
}
}
KiReleaseApcLockFromDpcLevel(&ApcLock);
KiExitDispatcher(ApcLock.OldIrql);
return PreviousCount;
}
这个专有的‘挂起APC’是一个特殊的APC,我们看他的工作:
VOID
KiSuspendThread(IN PVOID NormalContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2)
{
//等待挂起计数减到0
KeWaitForSingleObject(&KeGetCurrentThread()->SuspendSemaphore,Suspended,KernelMode,
FALSE,NULL);
}
如上,向指定线程插入一个‘挂起APC’后,那个线程下次一得到调度,就会先执行内核中的所有APC,当执行到这个APC的时候,就会一直等到挂起计数降到0。换言之,线程刚一得到调度运行的就会,就又重新进入等待了。因此,‘挂起态’也是一种特殊的‘等待态’。什么时候挂起计数会减到0呢?只有在别的线程恢复这个线程的挂起计数时。
ULONG KeResumeThread(IN PKTHREAD Thread)
{
KLOCK_QUEUE_HANDLE ApcLock;
ULONG PreviousCount;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
KiAcquireApcLock(Thread, &ApcLock);
PreviousCount = Thread->SuspendCount;
if (PreviousCount)
{
Thread->SuspendCount--;//递减挂起计数
if ((Thread->SuspendCount==0) && (!Thread->FreezeCount))
{
KiAcquireDispatcherLockAtDpcLevel();
Thread->SuspendSemaphore.Header.SignalState++;
//当挂起计数减到0时,唤醒目标线程
KiWaitTest(&Thread->SuspendSemaphore.Header, IO_NO_INCREMENT);
KiReleaseDispatcherLockFromDpcLevel();
}
}
KiReleaseApcLockFromDpcLevel(&ApcLock);
KiExitDispatcher(ApcLock.OldIrql);
return PreviousCount;
}
就这样简单。
当一个线程处于等待状态时,可以指示本次睡眠是否可被强制唤醒,不必等到条件满足
如:
DWORD WaitForSingleObjectEx(
HANDLE hHandle,
DWORD dwMilliseconds,
BOOL bAlertable //指示本次等待过程中是否可以被其他线程(或其他线程发来的APC)强制唤醒。
);
BOOLEAN
KeAlertThread(IN PKTHREAD Thread,
IN KPROCESSOR_MODE AlertMode)
{
BOOLEAN PreviousState;
KLOCK_QUEUE_HANDLE ApcLock;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
KiAcquireApcLock(Thread, &ApcLock);
KiAcquireDispatcherLockAtDpcLevel();
PreviousState = Thread->Alerted[AlertMode];//检测是否收到了来自那个模式的强制唤醒要求
if (PreviousState==FALSE)
{
if ((Thread->State == Waiting) && //线程处于等待状态
(Thread->Alertable) && //线程可被强制唤醒
(AlertMode <= Thread->WaitMode)) //模式条件符合
{
//强制唤醒那个线程
KiUnwaitThread(Thread, STATUS_ALERTED, THREAD_ALERT_INCREMENT);
}
Else //仅仅标记已收到过来自那个模式的强制唤醒请求
Thread->Alerted[AlertMode] = TRUE;
}
KiReleaseDispatcherLockFromDpcLevel();
KiReleaseApcLockFromDpcLevel(&ApcLock);
KiExitDispatcher(ApcLock.OldIrql);
return PreviousState;
}
注意AlertMode <= Thread->WaitMode条件指:用户模式的强制唤醒请求不能唤醒内核模式的等待。
八、DLL注入
前面讲过,每个进程在启动的时候会加载主exe文件依赖的所有子孙dll。实际上,一般的Win32 GUI进程
都会加载user32.dll模块。这个模块一加载,就会自动搜索注册表键 HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows 下的值:AppInit_DLLs,该值是一个dll列表,user32.dll会读取这个值,调用LoadLibrary加载里面的每个dll,因此我们可以把我们的dll名称添加到这个列表中,达到dll注入的目的。在ReactOS源码中能看到下面的代码:
INT DllMain( //User32.dll的DllMain
IN PVOID hInstanceDll,
IN ULONG dwReason,
IN PVOID reserved)
{
switch (dwReason)
{
case DLL_PROCESS_ATTACH:
Init();//会调用这个函数
…
…
}
}
BOOL Init(VOID)
{
…
LoadAppInitDlls();//会调用这个函数加载那些dll
…
}
VOID LoadAppInitDlls()
{
szAppInit[0] = UNICODE_NULL;
if (GetDllList())//读取这册表键的值,将要加载的dll列表保存在全局变量szAppInit中
{
WCHAR buffer[KEY_LENGTH];
LPWSTR ptr;
size_t i;
RtlCopyMemory(buffer, szAppInit, KEY_LENGTH);
for (i = 0; i < KEY_LENGTH; ++ i)
{
if(buffer[i] == L' ' || buffer[i] == L',')//dll名称之间必须用空格或逗号隔开
buffer[i] = 0;
}
for (i = 0; i < KEY_LENGTH; )
{
if(buffer[i] == 0)
++ i;
else
{
ptr = buffer + i;
i += wcslen(ptr);
LoadLibraryW(ptr);//加载每个dll
}
}
}
}
BOOL GetDllList()
{
NTSTATUS Status;
OBJECT_ATTRIBUTES Attributes;
BOOL bRet = FALSE;
BOOL bLoad;
HANDLE hKey = NULL;
DWORD dwSize;
PKEY_VALUE_PARTIAL_INFORMATION kvpInfo = NULL;
UNICODE_STRING szKeyName = RTL_CONSTANT_STRING(L"\\Registry\\Machine\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows");
UNICODE_STRING szLoadName = RTL_CONSTANT_STRING(L"LoadAppInit_DLLs");
UNICODE_STRING szDllsName = RTL_CONSTANT_STRING(L"AppInit_DLLs");
InitializeObjectAttributes(&Attributes, &szKeyName, OBJ_CASE_INSENSITIVE, NULL, NULL);
Status = NtOpenKey(&hKey, KEY_READ, &Attributes);
if (NT_SUCCESS(Status))
{
dwSize = sizeof(KEY_VALUE_PARTIAL_INFORMATION) + sizeof(DWORD);
kvpInfo = HeapAlloc(GetProcessHeap(), 0, dwSize);
if (!kvpInfo)
goto end;
//先要在那个键中建立一个DWORD值:LoadAppInit_DLLs,并将数值设为1
Status = NtQueryValueKey(hKey,&szLoadName,KeyValuePartialInformation,
kvpInfo,dwSize,&dwSize);
RtlMoveMemory(&bLoad,kvpInfo->Data,kvpInfo->DataLength);
HeapFree(GetProcessHeap(), 0, kvpInfo);
kvpInfo = NULL;
if (bLoad)//if 需要加载初始列表的那些dll
{
Status = NtQueryValueKey(hKey,&szDllsName,KeyValuePartialInformation,
NULL,0,&dwSize);
kvpInfo = HeapAlloc(GetProcessHeap(), 0, dwSize);
Status = NtQueryValueKey(hKey, &szDllsName,KeyValuePartialInformation,
kvpInfo,dwSize,&dwSize);
if (NT_SUCCESS(Status))
{
LPWSTR lpBuffer = (LPWSTR)kvpInfo->Data;
if (*lpBuffer != UNICODE_NULL)
{
INT bytesToCopy, nullPos;
bytesToCopy = min(kvpInfo->DataLength, KEY_LENGTH * sizeof(WCHAR));
if (bytesToCopy != 0)
{
//dll列表拷到全局变量
RtlMoveMemory(szAppInit,kvpInfo->Data,bytesToCopy);
nullPos = (bytesToCopy / sizeof(WCHAR)) - 1;
szAppInit[nullPos] = UNICODE_NULL;
bRet = TRUE;
}
}
}
}
}
end:
if (hKey)
NtClose(hKey);
if (kvpInfo)
HeapFree(GetProcessHeap(), 0, kvpInfo);
return bRet;
}
因此,只需在那个键下面添加一个DWORD值:LoadAppInit_DLLs,设为1,然后在AppInit_DLLs值中添加我们的dll即可达到将我们的dll加载到任意GUI进程的地址空间中。