APC:異步過程調用。這是一種常見的技術。前面進程啓動的初始過程就是:主線程在內核構造好運行環境後,從KiThreadStartup開始運行,而後調用PspUserThreadStartup,在該線程的apc隊列中插入一個APC:LdrInitializeThunk,這樣,當PspUserThreadStartup返回後,正式退回用戶空間的總入口BaseProcessStartThunk前,會執行中途插入的那個apc,完成進程的用戶空間初始化工做(連接dll的加載等)程序員
可見:APC的執行時機之一就是從內核空間返回用戶空間的前夕。也即在返回用戶空間前,會「中斷」那麼一下。所以,APC就是一種軟中斷。數組
除了這種APC用途外,應用程序中也常用APC。如Win32 API ReadFileEx就可使用APC機制來實現異步讀寫文件的功能。數據結構
BOOL //源碼app
ReadFileEx(IN HANDLE hFile,異步
IN LPVOID lpBuffer,函數
IN DWORD nNumberOfBytesToRead OPTIONAL,ui
IN LPOVERLAPPED lpOverlapped,//完成結果spa
IN LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine)//預置APC將調用的完成例程線程
{指針
LARGE_INTEGER Offset;
NTSTATUS Status;
Offset.u.LowPart = lpOverlapped->Offset;
Offset.u.HighPart = lpOverlapped->OffsetHigh;
lpOverlapped->Internal = STATUS_PENDING;
Status = NtReadFile(hFile,
NULL, //Event=NULL
ApcRoutine,//這個是內部預置的APC例程
lpCompletionRoutine,//APC的Context
(PIO_STATUS_BLOCK)lpOverlapped,
lpBuffer,
nNumberOfBytesToRead,
&Offset,
NULL);//Key=NULL
if (!NT_SUCCESS(Status))
{
SetLastErrorByStatus(Status);//
return FALSE;
}
return TRUE;
}
VOID ApcRoutine(PVOID ApcContext,//指向用戶提供的完成例程
_IO_STATUS_BLOCK* IoStatusBlock,//完成結果
ULONG Reserved)
{
LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine = ApcContext;
DWORD dwErrorCode = RtlNtStatusToDosError(IoStatusBlock->Status);
//調用用戶提供的完成例程
lpCompletionRoutine(dwErrorCode,
IoStatusBlock->Information,
(LPOVERLAPPED)IoStatusBlock);
}
所以,應用層的用戶提供的完成例程其實是做爲APC函數進行的,它運行在APC_LEVEL irql
NTSTATUS
NtReadFile(IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,//內置的APC
IN PVOID ApcContext OPTIONAL,//應用程序中用戶提供的完成例程
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID Buffer,
IN ULONG Length,
IN PLARGE_INTEGER ByteOffset OPTIONAL,
IN PULONG Key OPTIONAL)
{
…
Irp = IoAllocateIrp(DeviceObject->StackSize, FALSE);//分配一個irp
Irp->Overlay.AsynchronousParameters.UserApcRoutine = ApcRoutine;//記錄
Irp->Overlay.AsynchronousParameters.UserApcContext = ApcContext;//記錄
…
Status = IoCallDriver(DeviceObject, Irp);//把這個構造的irp發給底層驅動
…
}
當底層驅動完成這個irp後,會調用IoCompleteRequest完成掉這個irp,這個IoCompleteRequest實際上內部最終調用IopCompleteRequest來作一些完成時的工做
VOID
IopCompleteRequest(IN PKAPC Apc,
IN PKNORMAL_ROUTINE* NormalRoutine,
IN PVOID* NormalContext,
IN PVOID* SystemArgument1,
IN PVOID* SystemArgument2)
{
…
if (Irp->Overlay.AsynchronousParameters.UserApcRoutine)//上面傳入的APC
{
//構造一個APC
KeInitializeApc(&Irp->Tail.Apc,KeGetCurrentThread(),CurrentApcEnvironment,
IopFreeIrpKernelApc,
IopAbortIrpKernelApc,
(PKNORMAL_ROUTINE)Irp->Overlay.AsynchronousParameters.UserApcRoutine,
Irp->RequestorMode,
Irp->Overlay.AsynchronousParameters.UserApcContext);//應用層的完成例程
//插入到APC隊列
KeInsertQueueApc(&Irp->Tail.Apc, Irp->UserIosb, NULL, 2);
}//end if
…
}
如上,ReadFileEx函數的異步APC機制是:在這個請求完成後,IO管理器會將一個APC插入隊列中,而後
在返回用戶空間前夕調用那個內置APC,最終調用應用層用戶提供的完成例程。
明白了APC大體原理後,如今詳細看一下APC的工做原理。
APC分兩種,用戶APC、內核APC。前者指在用戶空間執行的APC,後者指在內核空間執行的APC。
先看一下內核爲支持APC機制提供的一些基礎結構設施。
Typedef struct _KTHREAD
{
…
KAPC_STATE ApcState;//表示本線程當前使用的APC狀態(即apc隊列的狀態)
KAPC_STATE SavedApcState;//表示保存的原apc狀態,備份用
KAPC_STATE* ApcStatePointer[2];//狀態數組,包含兩個指向APC狀態的指針
UCHAR ApcStateIndex;//0或1,指當前的ApcState在ApcStatePointer數組中的索引位置
UCHAR ApcQueueable;//指本線程的APC隊列是否可插入apc
ULONG KernelApcDisable;//禁用標誌
//專用於掛起操做的APC(這個函數在線程一獲得調度就從新進入等待態,等待掛起計數減到0)
KAPC SuspendApc;
…
}KTHREAD;
Typedef struct _KAPC_STATE //APC隊列的狀態描述符
{
LIST_EBTRY ApcListHead[2];//每一個線程有兩個apc隊列
PKPROCESS Process;//當前線程所在的進程
BOOL KernelApcInProgress;//指示本線程是否當前正在 內核apc
BOOL KernelApcPending;//表示內核apc隊列中是否有apc
BOOL UserApcPending;//表示用戶apc隊列中是否apc
}
Typedef enum _KAPC_ENVIRONMENT
{
OriginalApcEnvironment,//0,狀態數組索引
AttachedApcEnvironment;//1,狀態數組索引
CurrentApc Environment;//2,表示使用當前apc狀態
CurrentApc Environment;//3,表示使用插入apc時那時的線程的apc狀態
}
一個線程能夠掛靠到其餘進程的地址空間中,所以,一個線程的狀態分兩種:常態、掛靠態。
常態下,狀態數組中0號元素指向ApcState(即當前apc狀態),1號元素指向SavedApcState(非當前apc狀態);掛靠態下,兩個元素的指向恰好相反。但不管如何,KTHREAD結構中的ApcStateIndex老是指當前狀態的位置,ApcState則老是表示線程當前使用的apc狀態。
因而有:
#define PsGetCurrentProcess IoGetCurrentProces
PEPROCESS IoGetCurrentProces()
{
Return PsGetCurrentThread()->Tcb.ApcState.Process;//ApcState中的進程字段老是表示當前進程
}
無論當前線程是處於常態仍是掛靠態下,它都有兩個apc隊列,一個內核,一個用戶。把apc插入對應的隊列後就能夠在恰當的時機獲得執行。注意:每當一個線程掛靠到其餘進程時,掛靠初期,兩個apc隊列都會變空。下面看下每一個apc自己的結構
typedef struct _KAPC
{
UCHAR Type;//結構體的類型
UCHAR Size;//結構體的大小
struct _KTHREAD *Thread;//目標線程
LIST_ENTRY ApcListEntry;//用來掛入目標apc隊列
PKKERNEL_ROUTINE KernelRoutine;//該apc的內核總入口
PKRUNDOWN_ROUTINE RundownRoutine;
PKNORMAL_ROUTINE NormalRoutine;//該apc的用戶空間總入口或者用戶真正的內核apc函數
PVOID NormalContext;//真正用戶提供的用戶空間apc函數或者用戶真正的內核apc函數的context*
PVOID SystemArgument1;//掛入時的附加參數1。真正用戶apc的context*
PVOID SystemArgument2;//掛入時的附加參數2
CCHAR ApcStateIndex;//指要掛入目標線程的哪一個狀態時的apc隊列
KPROCESSOR_MODE ApcMode;//指要掛入用戶apc隊列仍是內核apc隊列
BOOLEAN Inserted;//表示本apc是否已掛入隊列
} KAPC, *PKAPC;
注意:
若這個apc是內核apc,那麼NormalRoutine表示用戶本身提供的內核apc函數,NormalContext則是該apc函數的context*,SystemArgument1與SystemArgument2表示插入隊列時的附加參數
若這個apc是用戶apc,那麼NormalRoutine表示該apc的用戶空間總apc函數,NormalContext纔是真正用戶本身提供的用戶空間apc函數,SystemArgument1則表示該真正apc的context*。(一切錯位了)
//下面這個Win32 API能夠用來手動插入一個apc到指定線程的用戶apc隊列中
DWORD
QueueUserAPC(PAPCFUNC pfnAPC, HANDLE hThread, ULONG_PTR dwData)
{
NTSTATUS Status;
//調用對應的系統服務
Status = NtQueueApcThread(hThread,//目標線程
IntCallUserApc,//用戶空間中的總apc入口
pfnAPC,//用戶本身真正提供的apc函數
(PVOID)dwData,//SysArg1=context*
NULL);//SysArg2=NULL
if (!NT_SUCCESS(Status))
{
SetLastErrorByStatus(Status);
return 0;
}
return 1;
}
NTSTATUS
NtQueueApcThread(IN HANDLE ThreadHandle,//目標線程
IN PKNORMAL_ROUTINE ApcRoutine,//用戶空間中的總apc
IN PVOID NormalContext,//用戶本身真正的apc函數
IN PVOID SystemArgument1,//用戶本身apc的context*
IN PVOID SystemArgument2)//其它
{
PKAPC Apc;
PETHREAD Thread;
NTSTATUS Status = STATUS_SUCCESS;
Status = ObReferenceObjectByHandle(ThreadHandle,THREAD_SET_CONTEXT,PsThreadType,
ExGetPreviousMode(), (PVOID)&Thread,NULL);
//分配一個apc結構,這個結構最終在PspQueueApcSpecialApc中釋放
Apc = ExAllocatePoolWithTag(NonPagedPool |POOL_QUOTA_FAIL_INSTEAD_OF_RAISE,
sizeof(KAPC),TAG_PS_APC);
//構造一個apc
KeInitializeApc(Apc,
&Thread->Tcb,//目標線程
OriginalApcEnvironment,//目標apc狀態(此服務固定爲OriginalApcEnvironment)
PspQueueApcSpecialApc,//內核apc總入口
NULL,//Rundown Rounine=NULL
ApcRoutine,//用戶空間的總apc
UserMode,//此係統服務固定插入到用戶apc隊列
NormalContext);//用戶本身真正的apc函數
//插入到目標線程的用戶apc隊列
KeInsertQueueApc(Apc,
SystemArgument1,//插入時的附加參數1,此處爲用戶本身apc的context*
SystemArgument2, //插入時的附加參數2
IO_NO_INCREMENT)//表示不予調整目標線程的調度優先級
return Status;
}
//這個函數用來構造一個要插入指定目標隊列的apc對象
VOID
KeInitializeApc(IN PKAPC Apc,
IN PKTHREAD Thread,//目標線程
IN KAPC_ENVIRONMENT TargetEnvironment,//目標線程的目標apc狀態
IN PKKERNEL_ROUTINE KernelRoutine,//內核apc總入口
IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,
IN PKNORMAL_ROUTINE NormalRoutine,//用戶空間的總apc
IN KPROCESSOR_MODE Mode,//要插入用戶apc隊列仍是內核apc隊列
IN PVOID Context) //用戶本身真正的apc函數
{
Apc->Type = ApcObject;
Apc->Size = sizeof(KAPC);
if (TargetEnvironment == CurrentApcEnvironment)//CurrentApcEnvironment表示使用當前apc狀態
Apc->ApcStateIndex = Thread->ApcStateIndex;
else
Apc->ApcStateIndex = TargetEnvironment;
Apc->Thread = Thread;
Apc->KernelRoutine = KernelRoutine;
Apc->RundownRoutine = RundownRoutine;
Apc->NormalRoutine = NormalRoutine;
if (NormalRoutine)//if 提供了用戶空間總apc入口
{
Apc->ApcMode = Mode;
Apc->NormalContext = Context;
}
Else//若沒提供,確定是內核模式
{
Apc->ApcMode = KernelMode;
Apc->NormalContext = NULL;
}
Apc->Inserted = FALSE;//表示初始構造後,還沒有掛入apc隊列
}
BOOLEAN
KeInsertQueueApc(IN PKAPC Apc,IN PVOID SystemArgument1,IN PVOID SystemArgument2,
IN KPRIORITY PriorityBoost)
{
PKTHREAD Thread = Apc->Thread;
KLOCK_QUEUE_HANDLE ApcLock;
BOOLEAN State = TRUE;
KiAcquireApcLock(Thread, &ApcLock);//插入過程須要獨佔隊列
if (!(Thread->ApcQueueable) || (Apc->Inserted))//檢查隊列是否能夠插入apc
State = FALSE;
else
{
Apc->SystemArgument1 = SystemArgument1;//記錄該apc的附加插入時的參數
Apc->SystemArgument2 = SystemArgument2; //記錄該apc的附加插入時的參數
Apc->Inserted = TRUE;//標記爲已插入隊列
//插入目標線程的目標apc隊列(若是目標線程正處於睡眠狀態,可能會喚醒它)
KiInsertQueueApc(Apc, PriorityBoost);
}
KiReleaseApcLockFromDpcLevel(&ApcLock);
KiExitDispatcher(ApcLock.OldIrql);//可能引起一次線程切換,以當即切換到目標線程執行apc
return State;
}
VOID FASTCALL
KiInsertQueueApc(IN PKAPC Apc,IN KPRIORITY PriorityBoost)//喚醒目標線程後的優先級增量
{
PKTHREAD Thread = Apc->Thread;
BOOLEAN RequestInterrupt = FALSE;
if (Apc->ApcStateIndex == InsertApcEnvironment) //if要動態插入到當前的apc狀態隊列
Apc->ApcStateIndex = Thread->ApcStateIndex;
ApcState = Thread->ApcStatePointer[(UCHAR)Apc->ApcStateIndex];//目標狀態
ApcMode = Apc->ApcMode;
//先插入apc到指定位置
/* 插入位置的肯定:分三種情形
* 1) Kernel APC with Normal Routine or User APC : Put it at the end of the List
* 2) User APC which is PsExitSpecialApc : Put it at the front of the List
* 3) Kernel APC without Normal Routine : Put it at the end of the No-Normal Routine Kernel APC list
*/
if (Apc->NormalRoutine)//有NormalRoutine的APC都插入尾部(用戶模式發來的線程終止APC除外)
{
if ((ApcMode == UserMode) && (Apc->KernelRoutine == PsExitSpecialApc))
{
Thread->ApcState.UserApcPending = TRUE;
InsertHeadList(&ApcState->ApcListHead[ApcMode],&Apc->ApcListEntry);
}
else
InsertTailList(&ApcState->ApcListHead[ApcMode],&Apc->ApcListEntry);
}
Else //無NormalRoutine的特殊類APC(內核APC),少見
{
ListHead = &ApcState->ApcListHead[ApcMode];
NextEntry = ListHead->Blink;
while (NextEntry != ListHead)
{
QueuedApc = CONTAINING_RECORD(NextEntry, KAPC, ApcListEntry);
if (!QueuedApc->NormalRoutine) break;
NextEntry = NextEntry->Blink;
}
InsertHeadList(NextEntry, &Apc->ApcListEntry);//插在這兒
}
//插入到相應的位置後,下面檢查Apc狀態是否匹配
if (Thread->ApcStateIndex == Apc->ApcStateIndex)//if 插到了當前apc狀態的apc隊列中
{
if (Thread == KeGetCurrentThread())//if就是給當前線程發送的apc
{
ASSERT(Thread->State == Running);//當前線程確定沒有睡眠,這不廢話嗎?
if (ApcMode == KernelMode)
{
Thread->ApcState.KernelApcPending = TRUE;
if (!Thread->SpecialApcDisable)//發出一個apc中斷,待下次下降irql時將執行apc
HalRequestSoftwareInterrupt(APC_LEVEL); //關鍵
}
}
Else //給其餘線程發送的內核apc
{
KiAcquireDispatcherLock();
if (ApcMode == KernelMode)
{
Thread->ApcState.KernelApcPending = TRUE;
if (Thread->State == Running)
RequestInterrupt = TRUE;//須要給它發出一個apc中斷
else if ((Thread->State == Waiting) && (Thread->WaitIrql == PASSIVE_LEVEL) &&
!(Thread->SpecialApcDisable) && (!(Apc->NormalRoutine) ||
(!(Thread->KernelApcDisable) &&
!(Thread->ApcState.KernelApcInProgress))))
{
Status = STATUS_KERNEL_APC;
KiUnwaitThread(Thread, Status, PriorityBoost);//臨時喚醒目標線程執行apc
}
else if (Thread->State == GateWait) …
}
else if ((Thread->State == Waiting) && (Thread->WaitMode == UserMode) &&
((Thread->Alertable) || (Thread->ApcState.UserApcPending)))
{
Thread->ApcState.UserApcPending = TRUE;
Status = STATUS_USER_APC;
KiUnwaitThread(Thread, Status, PriorityBoost);//強制喚醒目標線程
}
KiReleaseDispatcherLockFromDpcLevel();
KiRequestApcInterrupt(RequestInterrupt, Thread->NextProcessor);
}
}
}
如上,這個函數既能夠給當前線程發送apc,也能夠給目標線程發送apc。若給當前線程發送內核apc時,會當即請求發出一個apc中斷。若給其餘線程發送apc時,可能會喚醒目標線程。
APC函數的執行時機:
回顧一下從內核返回用戶時的流程:
KiSystemService()//int 2e的isr,內核服務函數總入口,注意這個函數能夠嵌套、遞歸!!!
{
SaveTrap();//保存trap現場
Sti //開中斷
---------------上面保存完寄存器等現場後,開始查SST表調用系統服務------------------
FindTableCall();
---------------------------------調用完系統服務函數後------------------------------
Move esp,kthread.TrapFrame; //將棧頂回到trap幀結構體處
Cli //關中斷
If(上次模式==UserMode)
{
Call KiDeliverApc //遍歷執行本線程的內核APC和用戶APC隊列中的全部APC函數
清理Trap幀,恢復寄存器現場
Iret //返回用戶空間
}
Else
{
返回到原call處後面的那條指令處
}
}
不光是從系統調用返回用戶空間要掃描執行apc,從異常和中斷返回用戶空間也一樣須要掃描執行。
如今咱們只看從系統調用返回時apc的執行過程。
上面是僞代碼,實際的從Cli後面的代碼,是下面這樣的。
Test dword ptr[ebp+KTRAP_FRAME_EFLAGS], EFLAGS_V86_MASK //檢查eflags是否標誌運行在V86模式
Jnz 1 //若運行在V86模式,那麼上次模式確定是從用戶空間進入內核的,跳過下面的檢查
Test byte ptr[ebp+KTRAP_FRAME_CS],1
Je 2 //若上次模式不是用戶模式,跳過下面的流程,不予掃描apc
1:
Mov ebx,PCR[KPCR_CURRENT_THREAD] //ebx=KTHREAD*(當前線程對象的地址)
Mov byte ptr[ebx+KTHREAD_ALERTED],0 //kthread.Alert修改成不可提醒
Cmp byte ptr[ebx+KTHREAD_PENDING_USER_APC],0
Je 2 //若是當前線程的用戶apc隊列爲空,直接跳過
Mov ebx,ebp //ebx=TrapFrame幀的地址
Mov [ebx,KTRAP_FRAME_EAX],eax //保存
Mov ecx,APC_LEVEL
Call KfRaiseIrql //call KfRaiseIrql(APC_LEVEL)
Push eax //保存提高irql以前的irql
Sti
Push ebx //TrapFrame幀的地址
Push NULL
Push UserMode
Call KiDeliverApc //call KiDeliverApc(UserMode, NULL, TrapFrame*)
Pop ecx // ecx=以前的irql
Call KfLowerIrql //call KfLowerIrql(以前的irql)
Move eax, [ebx,KTRAP_FRAME_EAX] //恢復eax
Cli
Jmp 1 //再次跳回1處循環,掃描apc隊列
…
關鍵的函數是KiDeliverApc,這個函數用來真正掃描apc隊列執行全部apc,咱們看:
VOID
KiDeliverApc(IN KPROCESSOR_MODE DeliveryMode,//指要執行哪一個apc隊列中的函數
IN PKEXCEPTION_FRAME ExceptionFrame,//傳入的是NULL
IN PKTRAP_FRAME TrapFrame)//即將返回用戶空間前的Trap現場幀
{
PKTHREAD Thread = KeGetCurrentThread();
PKPROCESS Process = Thread->ApcState.Process;
OldTrapFrame = Thread->TrapFrame;
Thread->TrapFrame = TrapFrame;
Thread->ApcState.KernelApcPending = FALSE;
if (Thread->SpecialApcDisable) goto Quickie;
//先固定執行掉內核apc隊列中的全部apc函數
while (!IsListEmpty(&Thread->ApcState.ApcListHead[KernelMode]))
{
KiAcquireApcLockAtApcLevel(Thread, &ApcLock);//鎖定apc隊列
ApcListEntry = Thread->ApcState.ApcListHead[KernelMode].Flink;//隊列頭部中的apc
Apc = CONTAINING_RECORD(ApcListEntry, KAPC, ApcListEntry);
KernelRoutine = Apc->KernelRoutine;//內核總apc函數
NormalRoutine = Apc->NormalRoutine;//用戶本身真正的內核apc函數
NormalContext = Apc->NormalContext;//真正內核apc函數的context*
SystemArgument1 = Apc->SystemArgument1;
SystemArgument2 = Apc->SystemArgument2;
if (NormalRoutine==NULL) //稱爲Special Apc,少見
{
RemoveEntryList(ApcListEntry);//關鍵,移除隊列
Apc->Inserted = FALSE;
KiReleaseApcLock(&ApcLock);
//執行內核中的總apc函數
KernelRoutine(Apc,&NormalRoutine,&NormalContext,
&SystemArgument1,&SystemArgument2);
}
Else //典型,通常程序員都會提供一個本身的內核apc函數
{
if ((Thread->ApcState.KernelApcInProgress) || (Thread->KernelApcDisable))
{
KiReleaseApcLock(&ApcLock);
goto Quickie;
}
RemoveEntryList(ApcListEntry); //關鍵,移除隊列
Apc->Inserted = FALSE;
KiReleaseApcLock(&ApcLock);
//執行內核中的總apc函數
KernelRoutine(Apc,
&NormalRoutine,//注意,內核中的總apc可能會在內部修改NormalRoutine
&NormalContext,
&SystemArgument1,
&SystemArgument2);
if (NormalRoutine)//若是內核總apc沒有修改NormalRoutine成NULL
{
Thread->ApcState.KernelApcInProgress = TRUE;//標記當前線程正在執行內核apc
KeLowerIrql(PASSIVE_LEVEL);
//直接調用用戶提供的真正內核apc函數
NormalRoutine(NormalContext, SystemArgument1, SystemArgument2);
KeRaiseIrql(APC_LEVEL, &ApcLock.OldIrql);
}
Thread->ApcState.KernelApcInProgress = FALSE;
}
}
//上面的循環,執行掉全部內核apc函數後,下面開始執行用戶apc隊列中的第一個apc
if ((DeliveryMode == UserMode) &&
!(IsListEmpty(&Thread->ApcState.ApcListHead[UserMode])) &&
(Thread->ApcState.UserApcPending))
{
KiAcquireApcLockAtApcLevel(Thread, &ApcLock);//鎖定apc隊列
Thread->ApcState.UserApcPending = FALSE;
ApcListEntry = Thread->ApcState.ApcListHead[UserMode].Flink;//隊列頭
Apc = CONTAINING_RECORD(ApcListEntry, KAPC, ApcListEntry);
KernelRoutine = Apc->KernelRoutine; //內核總apc函數
NormalRoutine = Apc->NormalRoutine; //用戶空間的總apc函數
NormalContext = Apc->NormalContext;//用戶真正的用戶空間apc函數
SystemArgument1 = Apc->SystemArgument1;//真正apc的context*
SystemArgument2 = Apc->SystemArgument2;
RemoveEntryList(ApcListEntry);//關鍵,移除隊列
Apc->Inserted = FALSE;
KiReleaseApcLock(&ApcLock);
KernelRoutine(Apc,
&NormalRoutine,// 注意,內核中的總apc可能會在內部修改NormalRoutine
&NormalContext,
&SystemArgument1,
&SystemArgument2);
if (!NormalRoutine)
KeTestAlertThread(UserMode);
Else //典型,準備提早回到用戶空間調用用戶空間的總apc函數
{
KiInitializeUserApc(ExceptionFrame,//NULL
TrapFrame,//Trap幀的地址
NormalRoutine, //用戶空間的總apc函數
NormalContext, //用戶真正的用戶空間apc函數
SystemArgument1, //真正apc的context*
SystemArgument2);
}
}
Quickie:
Thread->TrapFrame = OldTrapFrame;
}
如上,這個函數既能夠用來投遞處理內核apc函數,也能夠用來投遞處理用戶apc隊列中的函數。
特別的,當要調用這個函數投遞處理用戶apc隊列中的函數時,它每次只處理一個用戶apc。
因爲正式回到用戶空間前,會循環調用這個函數。所以,實際的處理順序是:
掃描執行內核apc隊列全部apc->執行用戶apc隊列中一個apc->再次掃描執行內核apc隊列全部apc->執行用戶apc隊列中下一個apc->再次掃描執行內核apc隊列全部apc->再次執行用戶apc隊列中下一個apc如此循環,直到將用戶apc隊列中的全部apc都執行掉。
執行用戶apc隊列中的apc函數與內核apc不一樣,由於用戶apc隊列中的apc函數天然是要在用戶空間中執行的,而KiDeliverApc這個函數自己位於內核空間,所以,不能直接調用用戶apc函數,須要‘提早’回到用戶空間去執行隊列中的每一個用戶apc,而後從新返回內核,再次掃描整個內核apc隊列,再執行用戶apc隊列中遺留的下一個用戶apc。如此循環,直至執行完全部用戶apc後,才‘正式’返回用戶空間。
下面的函數就是用來爲執行用戶apc作準備的。
VOID
KiInitializeUserApc(IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame,//原真正的斷點現場幀
IN PKNORMAL_ROUTINE NormalRoutine,
IN PVOID NormalContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2)
{
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
//將原真正的Trap幀打包保存在一個Context結構中
KeTrapFrameToContext(TrapFrame, ExceptionFrame, &Context);
_SEH2_TRY
{
AlignedEsp = Context.Esp & ~3;//對齊4B
//爲用戶空間中KiUserApcDisatcher函數的參數騰出空間(4個參數+ CONTEXT + 8B的seh節點)
ContextLength = CONTEXT_ALIGNED_SIZE + (4 * sizeof(ULONG_PTR));
Stack = ((AlignedEsp - 8) & ~3) - ContextLength;//8表示seh節點的大小
//模擬壓入KiUserApcDispatcher函數的4個參數
*(PULONG_PTR)(Stack + 0 * sizeof(ULONG_PTR)) = (ULONG_PTR)NormalRoutine;
*(PULONG_PTR)(Stack + 1 * sizeof(ULONG_PTR)) = (ULONG_PTR)NormalContext;
*(PULONG_PTR)(Stack + 2 * sizeof(ULONG_PTR)) = (ULONG_PTR)SystemArgument1;
*(PULONG_PTR)(Stack + 3 * sizeof(ULONG_PTR)) = (ULONG_PTR)SystemArgument2;
//將原真正trap幀保存在用戶棧的一個CONTEXT結構中,方便之後還原
RtlCopyMemory( (Stack + (4 * sizeof(ULONG_PTR))),&Context,sizeof(CONTEXT));
//強制修改當前Trap幀中的返回地址與用戶棧地址(偏離原來的返回路線)
TrapFrame->Eip = (ULONG)KeUserApcDispatcher;//關鍵,新的返回斷點地址
TrapFrame->HardwareEsp = Stack;//關鍵,新的用戶棧頂
TrapFrame->SegCs = Ke386SanitizeSeg(KGDT_R3_CODE, UserMode);
TrapFrame->HardwareSegSs = Ke386SanitizeSeg(KGDT_R3_DATA, UserMode);
TrapFrame->SegDs = Ke386SanitizeSeg(KGDT_R3_DATA, UserMode);
TrapFrame->SegEs = Ke386SanitizeSeg(KGDT_R3_DATA, UserMode);
TrapFrame->SegFs = Ke386SanitizeSeg(KGDT_R3_TEB, UserMode);
TrapFrame->SegGs = 0;
TrapFrame->ErrCode = 0;
TrapFrame->EFlags = Ke386SanitizeFlags(Context.EFlags, UserMode);
if (KeGetCurrentThread()->Iopl) TrapFrame->EFlags |= EFLAGS_IOPL;
}
_SEH2_EXCEPT((RtlCopyMemory(&SehExceptRecord, _SEH2_GetExceptionInformation()->ExceptionRecord, sizeof(EXCEPTION_RECORD)), EXCEPTION_EXECUTE_HANDLER))
{
SehExceptRecord.ExceptionAddress = (PVOID)TrapFrame->Eip;
KiDispatchException(&SehExceptRecord,ExceptionFrame,TrapFrame,UserMode,TRUE);
}
_SEH2_END;
}
至於爲何要放在一個try塊中保護,是由於用戶空間中的棧地址,誰也沒法保證會不會出現崩潰。
如上,這個函數修改返回地址,回到用戶空間中的KiUserApcDisatcher函數處去。而後把原trap幀保存在用戶棧中。因爲KiUserApcDisatcher這個函數有參數,因此須要模擬壓入這個函數的參數,這樣,當返回到用戶空間時,就彷彿是在調用這個函數。看下那個函數的代碼:
KiUserApcDisatcher(NormalRoutine,
NormalContext,
SysArg1,
SysArg2
)
{
Lea eax,[esp+ CONTEXT_ALIGNED_SIZE+16] //eax指向seh異常節點的地址
Mov ecx,fs:[TEB_EXCEPTION_LIST]
Mov edx,offset KiUserApcExceptionHandler
--------------------------------------------------------------------------------------
Mov [eax],ecx //seh節點的next指針成員
Mov [eax+4],edx //she節點的handler函數指針成員
Mov fs:[TEB_EXCEPTION_LIST],eax
--------------------上面三條指令在棧中構造一個8B的標準seh節點-----------------------
Pop eax //eax=NormalRoutine(即IntCallUserApc這個總apc函數)
Lea edi,[esp+12] //edi=棧中保存的CONTEXT結構的地址
Call eax //至關於call IntCallUserApc(NormalContext,SysArg1,SysArg2)
Mov ecx,[edi+ CONTEXT_ALIGNED_SIZE]
Mov fs:[ TEB_EXCEPTION_LIST],ecx //撤銷棧中的seh節點
Push TRUE //表示回到內核後須要繼續檢測執行用戶apc隊列中的apc函數
Push edi //傳入原棧幀的CONTEXT結構的地址給這個函數,以作恢復工做
Call NtContinue //調用這個函數從新進入內核(注意這個函數正常狀況下是不會返回到下面的)
----------------------------------華麗的分割線-------------------------------------------
Mov esi,eax
Push esi
Call RtlRaiseStatus //若ZwContinue返回了,那必定是內部出現了異常
Jmp StatusRaiseApc
Ret 16
}
如上,每當要執行一個用戶空間apc時,都會‘提早’偏離原來的路線返回用戶空間的這個函數處去執行用戶的apc。在執行這個函數前,會先構造一個seh節點,也即至關於把這個函數的調用放在try塊中保護。這個函數內部會調用IntCallUserApc,執行完真正的用戶apc函數後,調用ZwContinue重返內核。
Void CALLBACK //用戶空間的總apc函數
IntCallUserApc(void* RealApcFunc, void* SysArg1,void* SysArg2)
{
(*RealApcFunc)(SysArg1);//也即調用RealApcFunc(void* context)
}
NTSTATUS NtContinue(CONTEXT* Context, //原真正的TraFrame
BOOL TestAlert //指示是否繼續執行用戶apc隊列中的apc函數
)
{
Push ebp //此時ebp=本系統服務自身的TrapFrame地址
Mov ebx,PCR[KPCR_CURRENT_THREAD] //ebx=當前線程的KTHREAD對象地址
Mov edx,[ebp+KTRAP_FRAME_EDX] //注意TrapFrame中的這個edx字段不是用來保存edx的
Mov [ebx+KTHREAD_TRAP_FRAME],edx //將當前的TrapFrame改成上一個TrapFrame的地址
Mov ebp,esp
Mob eax,[ebp] //eax=本系統服務自身的TrapFrame地址
Mov ecx,[ebp+8] /本函數的第一個參數,即Context
Push eax
Push NULL
Push ecx
Call KiContinue //call KiContinue(Context*,NULL,TrapFrame*)
Or eax,eax
Jnz error
Cmp dword ptr[ebp+12],0 //檢查TestAlert參數的值
Je DontTest
Mov al,[ebx+KTHREAD_PREVIOUS_MODE]
Push eax
Call KeTestAlertThread //檢測用戶apc隊列是否爲空
DontTest:
Pop ebp
Mov esp,ebp
Jmp KiServiceExit2 //返回用戶空間(返回前,又會去掃描執行apc隊列中的下一個用戶apc)
}
NTSTATUS
KiContinue(IN PCONTEXT Context,//原來的斷點現場
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame) //NtContinue自身的TrapFrame地址
{
NTSTATUS Status = STATUS_SUCCESS;
KIRQL OldIrql = APC_LEVEL;
KPROCESSOR_MODE PreviousMode = KeGetPreviousMode();
if (KeGetCurrentIrql() < APC_LEVEL)
KeRaiseIrql(APC_LEVEL, &OldIrql);
_SEH2_TRY
{
if (PreviousMode != KernelMode)
KiContinuePreviousModeUser(Context,ExceptionFrame,TrapFrame);//恢復成原TrapFrame
else
{
KeContextToTrapFrame(Context,ExceptionFrame,TrapFrame,Context->ContextFlags,
KernelMode); //恢復成原TrapFrame
}
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
if (OldIrql < APC_LEVEL)
KeLowerIrql(OldIrql);
return Status;
}
VOID
KiContinuePreviousModeUser(IN PCONTEXT Context,//原來的斷點現場
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame)//NtContinue自身的TrapFrame地址
{
CONTEXT LocalContext;
ProbeForRead(Context, sizeof(CONTEXT), sizeof(ULONG));
RtlCopyMemory(&LocalContext, Context, sizeof(CONTEXT));
Context = &LocalContext;
//看到沒,將原Context中的成員填寫到NtContinue系統服務的TrapFrame幀中(也即修改爲原來的TrapFrame)
KeContextToTrapFrame(&LocalContext,ExceptionFrame,TrapFrame,
LocalContext.ContextFlags,UserMode);
}
如上,上面的函數,就把NtContinue的TrapFrame強制還原成原來的TrapFrame,以好‘正式’返回到用戶空間的真正斷點處(不過在返回用戶空間前,又要去掃描用戶apc隊列,若仍有用戶apc函數,就先執行掉內核apc隊列中的全部apc函數,而後又偏離原來的返回路線,‘提早’返回到用戶空間的KiUserApcDispatcher函數去執行用戶apc,這是一個不斷循環的過程。可見,NtContinue這個函數不只含有繼續回到原真正用戶空間斷點處的意思,還含有繼續執行用戶apc隊列中下一個apc函數的意思)
BOOLEAN KeTestAlertThread(IN KPROCESSOR_MODE AlertMode)
{
PKTHREAD Thread = KeGetCurrentThread();
KiAcquireApcLock(Thread, &ApcLock);
OldState = Thread->Alerted[AlertMode];
if (OldState)
Thread->Alerted[AlertMode] = FALSE;
else if ((AlertMode != KernelMode) &&
(!IsListEmpty(&Thread->ApcState.ApcListHead[UserMode])))
{
Thread->ApcState.UserApcPending = TRUE;//關鍵。又標記爲不空,從而又去執行用戶apc
}
KiReleaseApcLock(&ApcLock);
return OldState;
}
上面這個函數的關鍵工做是檢測到用戶apc隊列不爲空,就又將UserApcPending標誌置於TRUE。
前面咱們看到的是用戶apc隊列的執行機制與時機,那是用戶apc惟一的執行時機。內核apc隊列中的apc執行時機是不相同的,並且有不少執行時機。
內核apc的執行時機主要有:
一、 每次返回用戶空間前,每執行一個用戶apc前,就會掃描執行整個內核apc隊列
二、 每當調用KeLowerIrql,從APC_LEVEL以上(不包括APC_LEVEL) 降到 APC_LEVEL如下(不包括APC_LEVEL)前,中途會檢查是否有阻塞的apc中斷請求,如有就掃描執行內核apc隊列
三、 每當線程從新獲得調度,開始運行前,會掃描執行內核apc隊列 或者 發出apc中斷請求
內核apc的執行時機:【調度、返、降】apc
KeLowerIrql實質上是下面的函數:
VOID FASTCALL
KfLowerIrql(IN KIRQL OldIrql)
{
ULONG EFlags;
ULONG PendingIrql, PendingIrqlMask;
PKPCR Pcr = KeGetPcr();
PIC_MASK Mask;
EFlags = __readeflags();//保存原eflags
_disable();//關中斷
Pcr->Irql = OldIrql;//降到目標irql
//檢測是否有高於目標irql的阻塞中的軟中斷
PendingIrqlMask = Pcr->IRR & FindHigherIrqlMask[OldIrql];
if (PendingIrqlMask)//如有
{
BitScanReverse(&PendingIrql, PendingIrqlMask);//找到最高級別的軟中斷
if (PendingIrql > DISPATCH_LEVEL)
{
Mask.Both = Pcr->IDR;
__outbyte(PIC1_DATA_PORT, Mask.Master);
__outbyte(PIC2_DATA_PORT, Mask.Slave);
Pcr->IRR ^= (1 << PendingIrql);
}
SWInterruptHandlerTable[PendingIrql]();//處理阻塞的軟中斷(即掃描執行隊列中的函數)
}
__writeeflags(EFlags);//恢復原eflags
}
這個函數在從當前irql降到目標irql時,會按irql高低順序執行各個軟中斷的isr。
軟中斷是用來模擬硬件中斷的一種中斷。
#define PASSIVE_LEVEL 0
#define APC_LEVEL 1
#define DISPATCH_LEVEL 2
#define CMCI_LEVEL 5
好比,當調用KfLowerIrql要將cpu的irql從CMCI_LEVEL下降到PASSIVE_LEVEL時,這個函數中途會先看看當前cpu是否收到了CMCI_LEVEL級的軟中斷,如有,就調用那個軟中斷的isr處理之。而後,再檢查是否收到有DISPATCH_LEVEL級的軟中斷,如有,調用那個軟中斷的isr處理之,而後,檢查是否有APC中斷,如有,一樣處理之。最後,降到目標irql,即PASSIVE_LEVEL。
換句話說,在irql的下降過程當中會一路檢查、處理中途的軟中斷。Cpu數據結構中有一個IRR字段,即表示當前cpu累積收到了哪些級別的軟中斷。
下面的函數可用於模擬硬件,向cpu發出任意irql級別的軟中斷,請求cpu處理執行那種中斷。
VOID FASTCALL
HalRequestSoftwareInterrupt(IN KIRQL Irql)//Irql通常是APC_LEVEL/DPC_LEVEL
{
ULONG EFlags;
PKPCR Pcr = KeGetPcr();
KIRQL PendingIrql;
EFlags = __readeflags();//保存老的eflags寄存器
_disable();//關中斷
Pcr->IRR |= (1 << Irql);//關鍵。標誌向cpu發出了一個對應irql級的軟中斷
PendingIrql = SWInterruptLookUpTable[Pcr->IRR & 3];//IRR後兩位表示是否有阻塞的apc中斷
//如有阻塞的apc中斷,而且當前irql是PASSIVE_LEVEL,當即執行apc。也即在PASSIVE_LEVEL級時發出任意軟中斷後,會當即檢查執行現有的apc中斷。
if (PendingIrql > Pcr->Irql)
SWInterruptHandlerTable[PendingIrql]();//調用執行apc中斷的isr,處理apc中斷
__writeeflags(EFlags);//恢復原eflags寄存器
}
那麼何時,系統會調用這個函數,向cpu發出apc中斷呢?
典型的情形1:
在切換線程時,若將線程的WaitIrql置爲APC_LEVEL,將致使KiSwapContextInternal函數內部在從新切回來後,當即自動發出一個apc中斷,以在下次下降irql到PASSIVE_LEVEL時處理執行隊列中那些阻塞的apc。反之,若將線程的WaitIrql置爲PASSIVE_LEVEL,將致使KiSwapContextInternal函數內部在從新切回來後,不會發出apc中斷,而後系統會自行顯式調用KiDeliverApc給予掃描執行
典型情形2:
在給自身線程發送一個內核apc時,在apc進隊的同時,會發出apc中斷,以請求cpu在下次下降irql時,掃描執行apc。
Apc是一種軟中斷,既然是中斷,他也有相似的isr。Apc中斷的isr最終進入 HalpApcInterruptHandler
VOID FASTCALL
HalpApcInterruptHandler(IN PKTRAP_FRAME TrapFrame)
{
//模擬硬件中斷壓入保存的寄存器
TrapFrame->EFlags = __readeflags();
TrapFrame->SegCs = KGDT_R0_CODE;
TrapFrame->Eip = TrapFrame->Eax;
KiEnterInterruptTrap(TrapFrame);//構造Trap現場幀
掃描執行當前線程的內核apc隊列,略…
KiEoiHelper(TrapFrame);
}