设计类专业就业前景怎么样,天津seo代理商,内网网站搭建设,可视化运维管理平台二、线程调度与切换
众所周知#xff1a;Windows系统是一个分时抢占式系统#xff0c;分时指每个线程分配时间片#xff0c;抢占指时间片到期前#xff0c;中途可以被其他更高优先级的线程强制抢占。
背景知识#xff1a;每个cpu都有一个TSS#xff0c;叫‘任务状态段’…二、线程调度与切换
众所周知Windows系统是一个分时抢占式系统分时指每个线程分配时间片抢占指时间片到期前中途可以被其他更高优先级的线程强制抢占。
背景知识每个cpu都有一个TSS叫‘任务状态段’。这个TSS内部中的一些字段记录着该cpu上当前正在运行的那个线程的一些信息如ESP0记录着该线程的内核栈位置IO权限位图记录着当前线程的IO空间权限
IO空间有64KBIO权限位图中的每一位记录着对应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 KiSwapContexKTHREAD* Currentthread*, KTHREAD* NewThread;
返回值表示下次切换回来时是否需要手动扫描执行内核APC。这个函数的汇编代码为
KiSwapContext8: //开头的表示fastcall调用约定
{
sub esp, 4 * 4 //腾出局部变量空间
//保存这4个寄存器因为KiSwapContextInternal函数内部要使用这几个寄存器 mov [esp12], ebx mov [esp8], esi mov [esp4], edi mov [esp0], 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 [ediKTHREAD_WAIT_IRQL] //ecx当前线程的WaitIrql
call KiSwapContextInternal0 //调用真正的切换工作函数
这中间已经被切换到新线程去了当前线程已经让出cpu挂入了就绪队列。需要等到下次重新被调度运行时才又从这儿的断点处继续向下执行下去
mov ebp, [esp0] //这条指令就是断点处以后切换回来时就从这个断点处继续执行 mov edi, [esp4] mov esi, [esp8] mov ebx, [esp12 add esp, 4 * 4 ret
}
下面的函数完成真正的切换工作返回值表示切换回来后是否需要手动扫描执行内核apc
KiSwapContextInternal0: //edi指向当前线程esi指向要切换到的新线程ebx指向当前KPCR*
{
inc dword ptr es:[ebxKPCR_CONTEXT_SWITCHES] //递增当前cpu上发生的历史线程切换计数 push ecx //保存本线程切换时的WaitIrql
push [ebxKPCR_EXCEPTION_LIST] //保存本线程切换时的内核seh链表
-------------------------至此上面的两条push连同本函数的返回地址即断点地址就构成了一个切换帧。当前线程切换时的内核栈顶位置就在此处-----------------------------
AfterTrace: mov ebp, cr0 mov edx, ebp //将cr0寄存器保存在edx中cr0的Bit3位“TaskSwitched”标志位与浮点运算相关
SetStack: mov [ediKTHREAD_KERNEL_STACK], esp //保存本线程切换时的内核栈顶位置
mov eax, [esiKTHREAD_INITIAL_STACK] //eax新线程的内核栈底地址
-------------------------------------------------------------------------------- cli //下面检查Npx浮点寄存器要关中断 movzx ecx, byte ptr [esiKTHREAD_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, [esiKTHREAD_KERNEL_STACK] //关键。恢复成新线程当初被切换时的内核栈顶 mov ebp, [esiKTHREAD_APCSTATE_PROCESS] //ebp目标进程 mov eax, [ediKTHREAD_APCSTATE_PROCESS] //eax当前进程 cmp ebp, eax //检查是否是切换到同一个进程中的其他线程若是。就不用切换LDT和cr3
jz SameProcess
//若切换到其他进程中的线程则要同时修改LDT和CR3 mov ecx, [ebpKPROCESS_LDT_DESCRIPTOR0] or ecx, [eaxKPROCESS_LDT_DESCRIPTOR0] jnz LdtReload //如果两个进程的LDT不同就要换用不同的LDT
UpdateCr3: mov eax, [ebpKPROCESS_DIRECTORY_TABLE_BASE] mov cr3, eax //关键。将cr3换成目标进程的页目录
SameProcess: xor eax, eax mov gs, ax mov eax, [esiKTHREAD_TEB] //新线程的TEB地址 mov [ebxKPCR_TEB], eax //当前KPCR中的TEB指向新线程的TEB
mov ecx, [ebxKPCR_GDT]
//修改GDT中的TEB描述符指向新线程的TEB mov [ecx0x3A], ax shr eax, 16 mov [ecx0x3C], al mov [ecx0x3F], ah mov eax, [esiKTHREAD_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, [ebxKPCR_TSS] mov [ecxKTSS_ESP0], eax //关键修改TSS中的ESP0指向新线程的内核栈底 mov ax, [ebpKPROCESS_IOPM_OFFSET] mov [ecxKTSS_IOMAPBASE], ax //修改TSS中的IO权限位图偏移指向新进程中的IO权限位图 inc dword ptr [esiKTHREAD_CONTEXT_SWITCHES] //递增线程的切换次数也即历史调度次数 pop [ebxKPCR_EXCEPTION_LIST] //将当前KPCR中记录的seh链表恢复成新线程的seh链表 pop ecx //ecx新线程原来切换前的WaitIrql cmp byte ptr [ebxKPCR_PRCB_DPC_ROUTINE_ACTIVE], 0 //检查当前是否有DPC函数处于活动状态 jnz BugCheckDpc //蓝屏
//至此cpu中的寄存器内容全部换成了新线程的那些寄存器从这个意思上说此时就已完成了全部切换工作下面的代码都是在新线程的环境中运行了。
--------------------------------新线程环境--------------------------------------- cmp byte ptr [esiKTHREAD_PENDING_KERNEL_APC], 0 jnz CheckApc //看到没每次线程得到重新调度运行前都会扫描执行内核apc队列中的函数 xor eax, eax ret //此处返回值表示没有内核apc
CheckApc: cmp word ptr [esiKTHREAD_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 HalRequestSoftwareInterrupt4 //发出一个apc中断 or eax, esp //既然发出apc中断了那么就return FALSE表示无需手动扫描执行apc
ApcReturn: setz al
ret //此处返回值表示切回来后是否需要手动扫描执行apc
//当这个函数返回时之前已经换成新线程的内核栈了。当函数返回后将回到KiSwapContext中当KiSwapContext返回到调用方时那个调用方就是新线程当初调用的KiSwapContext的函数这样就沿着新线程的内核栈逐级向上回溯到新线程中了。因此可以说切换内核栈即是切换线程。
LdtReload: mov eax, [ebpKPROCESS_LDT_DESCRIPTOR0] test eax, eax //检测目标进程有没有LDT jz LoadLdt mov ecx, [ebxKPCR_GDT] mov [ecxKGDT_LDT], eax //改指目标进程的LDT mov eax, [ebpKPROCESS_LDT_DESCRIPTOR1] mov [ecxKGDT_LDT4], eax//改指目标进程的LDT /* Write the INT21 handler */ mov ecx, [ebxKPCR_IDT] mov eax, [ebpKPROCESS_INT21_DESCRIPTOR0] mov [ecx0x108], eax mov eax, [ebpKPROCESS_INT21_DESCRIPTOR1] mov [ecx0x10C], eax mov eax, KGDT_LDT
LoadLdt: lldt ax jmp UpdateCr3
NewCr0: mov cr0, ecx jmp StackOk
BugCheckDpc: mov eax, [ediKTHREAD_INITIAL_STACK] push 0 push eax push esi push edi push ATTEMPTED_SWITCH_FROM_DPC call _KeBugCheckEx20 //蓝屏提示“尝试从活动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-ReadySummary0) 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;//可以肯定OldIrqlAPC_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 (Priority0) 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-SuspendCount0) (!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 (PreviousStateFALSE) { 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(LLoadAppInit_DLLs); UNICODE_STRING szDllsName RTL_CONSTANT_STRING(LAppInit_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进程的地址空间中。