windows内核情景分析---进程线程2
二、線程調度與切換
眾所周知: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節所在的原模塊
//重要?;靥畹皆K中,該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進程的地址空間中。
總結
以上是生活随笔為你收集整理的windows内核情景分析---进程线程2的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 治疗不孕不育价格
- 下一篇: java信息管理系统总结_java实现科