[6]Windows内核情景分析 --APC
APC:异步过程调用。这是一种常见的技术。前面进程启动的初始过程就是:主线程在内核构造好运行环境后,从KiThreadStartup开始运行,然后调用PspUserThreadStartup,在该线程的apc队列中插入一个APC:LdrInitializeThunk,这样,当PspUserThreadStartup返回后,正式退回用户空间的总入口BaseProcessStartThunk前,会执行中途插入的那个apc,完成进程的用户空间初始化工作(链接dll的加载等)
可见:APC的执行时机之一就是从内核空间返回用户空间的前夕。也即在返回用户空间前,会“中断”那么一下。因此,APC就是一种软中断。
除了这种APC用途外,应用程序中也经常使用APC。如Win32 API ReadFileEx就可以使用APC机制来实现异步读写文件的功能。
BOOL //源码
ReadFileEx(IN HANDLE hFile,
IN LPVOID lpBuffer,
IN DWORD nNumberOfBytesToRead OPTIONAL,
IN LPOVERLAPPED lpOverlapped,//完成结果
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的执行时机主要有:
1、 每次返回用户空间前,每执行一个用户apc前,就会扫描执行整个内核apc队列
2、 每当调用KeLowerIrql,从APC_LEVEL以上(不包括APC_LEVEL) 降到 APC_LEVEL以下(不包括APC_LEVEL)前,中途会检查是否有阻塞的apc中断请求,若有就扫描执行内核apc队列
3、 每当线程重新得到调度,开始运行前,会扫描执行内核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);
}