http://www.aichengxu.com/article/%E7%B3%BB%E7%BB%9F%E4%BC%98%E5%8C%96/17531_12.html
windows内核情景分析 --APC,有需要的朋友可以参考下。
APC:异步过程调用。这是一种常见的技术。前面进程启动的初始过程就是:主线程在内核构造好运行环境后,从KiThreadStartup开始运行,然后调用PspUserThreadStartup,在该线程的apc队列中插入一个APC:LdrInitializeThunk,这样,当PspUserThreadStartup返回后,正式退回用户空间的总入口BaseProcessStartThunk前,会执行中途插入的那个apc,完成进程的用户空间初始化工作(链接dll的加载等)
可见:APC的执行时机之一就是从内核空间返回用户空间的前夕。也即在返回用户空间前,会“中断”那么一下。因此,APC就是一种软中断。
除了这种APC用途外,应用程序中也经常使用APC。如Win32APIReadFileEx就可以使用APC机制来实现异步读写文件的功能。
BOOL//源码
ReadFileEx(INHANDLEhFile,
INLPVOIDlpBuffer,
INDWORDnNumberOfBytesToReadOPTIONAL,
INLPOVERLAPPEDlpOverlapped,//完成结果
INLPOVERLAPPED_COMPLETION_ROUTINElpCompletionRoutine)//预置APC将调用的完成例程
{
LARGE_INTEGEROffset;
NTSTATUSStatus;
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);//
returnFALSE;
}
returnTRUE;
}
VOIDApcRoutine(PVOIDApcContext,//指向用户提供的完成例程
_IO_STATUS_BLOCK*IoStatusBlock,//完成结果
ULONGReserved)
{
LPOVERLAPPED_COMPLETION_ROUTINElpCompletionRoutine=ApcContext;
DWORDdwErrorCode=RtlNtStatusToDosError(IoStatusBlock->Status);
//调用用户提供的完成例程
lpCompletionRoutine(dwErrorCode,
IoStatusBlock->Information,
(LPOVERLAPPED)IoStatusBlock);
}
因此,应用层的用户提供的完成例程实际上是作为APC函数进行的,它运行在APC_LEVELirql
NTSTATUS
NtReadFile(INHANDLEFileHandle,
INHANDLEEventOPTIONAL,
INPIO_APC_ROUTINEApcRoutineOPTIONAL,//内置的APC
INPVOIDApcContextOPTIONAL,//应用程序中用户提供的完成例程
OUTPIO_STATUS_BLOCKIoStatusBlock,
OUTPVOIDBuffer,
INULONGLength,
INPLARGE_INTEGERByteOffsetOPTIONAL,
INPULONGKeyOPTIONAL)
{
…
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(INPKAPCApc,
INPKNORMAL_ROUTINE*NormalRoutine,
INPVOID*NormalContext,
INPVOID*SystemArgument1,
INPVOID*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);
}//endif
…
}
如上,ReadFileEx函数的异步APC机制是:在这个请求完成后,IO管理器会将一个APC插入队列中,然后
在返回用户空间前夕调用那个内置APC,最终调用应用层用户提供的完成例程。
明白了APC大致原理后,现在详细看一下APC的工作原理。
APC分两种,用户APC、内核APC。前者指在用户空间执行的APC,后者指在内核空间执行的APC。
先看一下内核为支持APC机制提供的一些基础结构设施。
Typedefstruct_KTHREAD
{
…
KAPC_STATEApcState;//表示本线程当前使用的APC状态(即apc队列的状态)
KAPC_STATESavedApcState;//表示保存的原apc状态,备份用
KAPC_STATE*ApcStatePointer[2];//状态数组,包含两个指向APC状态的指针
UCHARApcStateIndex;//0或1,指当前的ApcState在ApcStatePointer数组中的索引位置
UCHARApcQueueable;//指本线程的APC队列是否可插入apc
ULONGKernelApcDisable;//禁用标志
//专用于挂起操作的APC(这个函数在线程一得到调度就重新进入等待态,等待挂起计数减到0)
KAPCSuspendApc;
…
}KTHREAD;
Typedefstruct_KAPC_STATE//APC队列的状态描述符
{
LIST_EBTRYApcListHead[2];//每个线程有两个apc队列
PKPROCESSProcess;//当前线程所在的进程
BOOLKernelApcInProgress;//指示本线程是否当前正在内核apc
BOOLKernelApcPending;//表示内核apc队列中是否有apc
BOOLUserApcPending;//表示用户apc队列中是否apc
}
Typedefenum_KAPC_ENVIRONMENT
{
OriginalApcEnvironment,//0,状态数组索引
AttachedApcEnvironment;//1,状态数组索引
CurrentApcEnvironment;//2,表示使用当前apc状态
CurrentApcEnvironment;//3,表示使用插入apc时那时的线程的apc状态
}
一个线程可以挂靠到其他进程的地址空间中,因此,一个线程的状态分两种:常态、挂靠态。
常态下,状态数组中0号元素指向ApcState(即当前apc状态),1号元素指向SavedApcState(非当前apc状态);挂靠态下,两个元素的指向刚好相反。但无论如何,KTHREAD结构中的ApcStateIndex总是指当前状态的位置,ApcState则总是表示线程当前使用的apc状态。
于是有:
#define PsGetCurrentProcess IoGetCurrentProces
PEPROCESSIoGetCurrentProces()
{
Return PsGetCurrentThread()->Tcb.ApcState.Process;//ApcState中的进程字段总是表示当前进程
}
不管当前线程是处于常态还是挂靠态下,它都有两个apc队列,一个内核,一个用户。把apc插入对应的队列后就可以在恰当的时机得到执行。注意:每当一个线程挂靠到其他进程时,挂靠初期,两个apc队列都会变空。下面看下每个apc本身的结构
typedefstruct_KAPC
{
UCHARType;//结构体的类型
UCHARSize;//结构体的大小
struct_KTHREAD*Thread;//目标线程
LIST_ENTRYApcListEntry;//用来挂入目标apc队列
PKKERNEL_ROUTINEKernelRoutine;//该apc的内核总入口
PKRUNDOWN_ROUTINERundownRoutine;
PKNORMAL_ROUTINENormalRoutine;//该apc的用户空间总入口或者用户真正的内核apc函数
PVOIDNormalContext;//真正用户提供的用户空间apc函数或者用户真正的内核apc函数的context*
PVOIDSystemArgument1;//挂入时的附加参数1。真正用户apc的context*
PVOIDSystemArgument2;//挂入时的附加参数2
CCHARApcStateIndex;//指要挂入目标线程的哪个状态时的apc队列
KPROCESSOR_MODEApcMode;//指要挂入用户apc队列还是内核apc队列
BOOLEANInserted;//表示本apc是否已挂入队列
}KAPC,*PKAPC;
注意:
若这个apc是内核apc,那么NormalRoutine表示用户自己提供的内核apc函数,NormalContext则是该apc函数的context*,SystemArgument1与SystemArgument2表示插入队列时的附加参数
若这个apc是用户apc,那么NormalRoutine表示该apc的用户空间总apc函数,NormalContext才是真正用户自己提供的用户空间apc函数,SystemArgument1则表示该真正apc的context*。(一切错位了)
//下面这个Win32API可以用来手动插入一个apc到指定线程的用户apc队列中
DWORD
QueueUserAPC(PAPCFUNCpfnAPC,HANDLEhThread,ULONG_PTRdwData)
{
NTSTATUSStatus;
//调用对应的系统服务
Status=NtQueueApcThread(hThread,//目标线程
IntCallUserApc,//用户空间中的总apc入口
pfnAPC,//用户自己真正提供的apc函数
(PVOID)dwData,//SysArg1=context*
NULL);//SysArg2=NULL
if(!NT_SUCCESS(Status))
{
SetLastErrorByStatus(Status);
return0;
}
return1;
}
NTSTATUS
NtQueueApcThread(INHANDLEThreadHandle,//目标线程
INPKNORMAL_ROUTINEApcRoutine,//用户空间中的总apc
INPVOIDNormalContext,//用户自己真正的apc函数
INPVOIDSystemArgument1,//用户自己apc的context*
INPVOIDSystemArgument2)//其它
{
PKAPCApc;
PETHREADThread;
NTSTATUSStatus=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,//RundownRounine=NULL
ApcRoutine,//用户空间的总apc
UserMode,//此系统服务固定插入到用户apc队列
NormalContext);//用户自己真正的apc函数
//插入到目标线程的用户apc队列
KeInsertQueueApc(Apc,
SystemArgument1,//插入时的附加参数1,此处为用户自己apc的context*
SystemArgument2,//插入时的附加参数2
IO_NO_INCREMENT)//表示不予调整目标线程的调度优先级
returnStatus;
}
//这个函数用来构造一个要插入指定目标队列的apc对象
VOID
KeInitializeApc(INPKAPCApc,
INPKTHREADThread,//目标线程
INKAPC_ENVIRONMENTTargetEnvironment,//目标线程的目标apc状态
INPKKERNEL_ROUTINEKernelRoutine,//内核apc总入口
INPKRUNDOWN_ROUTINERundownRoutineOPTIONAL,
INPKNORMAL_ROUTINENormalRoutine,//用户空间的总apc
INKPROCESSOR_MODEMode,//要插入用户apc队列还是内核apc队列
INPVOIDContext)//用户自己真正的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(INPKAPCApc,INPVOIDSystemArgument1,INPVOIDSystemArgument2,
INKPRIORITYPriorityBoost)
{
PKTHREADThread=Apc->Thread;
KLOCK_QUEUE_HANDLEApcLock;
BOOLEANState=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
returnState;
}
VOIDFASTCALL
KiInsertQueueApc(INPKAPCApc,INKPRIORITYPriorityBoost)//唤醒目标线程后的优先级增量
{
PKTHREADThread=Apc->Thread;
BOOLEANRequestInterrupt=FALSE;
if(Apc->ApcStateIndex==InsertApcEnvironment)//if要动态插入到当前的apc状态队列
Apc->ApcStateIndex=Thread->ApcStateIndex;
ApcState=Thread->ApcStatePointer[(UCHAR)Apc->ApcStateIndex];//目标状态
ApcMode=Apc->ApcMode;
//先插入apc到指定位置
/*插入位置的确定:分三种情形
*1)KernelAPCwithNormalRoutineorUserAPC:PutitattheendoftheList
*2)UserAPCwhichisPsExitSpecialApc:PutitatthefrontoftheList
*3)KernelAPCwithoutNormalRoutine:PutitattheendoftheNo-NormalRoutineKernelAPClist
*/
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中断
elseif((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
}
elseif(Thread->State==GateWait)…
}
elseif((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()//int2e的isr,内核服务函数总入口,注意这个函数可以嵌套、递归!!!
{
SaveTrap();//保存trap现场
Sti//开中断
---------------上面保存完寄存器等现场后,开始查SST表调用系统服务------------------
FindTableCall();
---------------------------------调用完系统服务函数后------------------------------
Moveesp,kthread.TrapFrame;//将栈顶回到trap帧结构体处
Cli//关中断
If(上次模式==UserMode)
{
CallKiDeliverApc//遍历执行本线程的内核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:
Movebx,PCR[KPCR_CURRENT_THREAD]//ebx=KTHREAD*(当前线程对象的地址)
Movbyteptr[ebx+KTHREAD_ALERTED],0//kthread.Alert修改为不可提醒
Cmpbyteptr[ebx+KTHREAD_PENDING_USER_APC],0
Je2//如果当前线程的用户apc队列为空,直接跳过
Move bx,ebp//ebx=TrapFrame帧的地址
Mov [ebx,KTRAP_FRAME_EAX],eax//保存
Mov ecx,APC_LEVEL
Call KfRaiseIrql//callKfRaiseIrql(APC_LEVEL)
Push eax//保存提升irql之前的irql
Sti
Push ebx//TrapFrame帧的地址
Push NULL
Push UserMode
Call KiDeliverApc//callKiDeliverApc(UserMode,NULL,TrapFrame*)
Pop ecx//ecx=之前的irql
Call KfLowerIrql//callKfLowerIrql(之前的irql)
Mov eax,[ebx,KTRAP_FRAME_EAX]//恢复eax
Cli
Jmp1//再次跳回1处循环,扫描apc队列
…
关键的函数是KiDeliverApc,这个函数用来真正扫描apc队列执行所有apc,我们看:
VOID
KiDeliverApc(INKPROCESSOR_MODEDeliveryMode,//指要执行哪个apc队列中的函数
INPKEXCEPTION_FRAMEExceptionFrame,//传入的是NULL
INPKTRAP_FRAMETrapFrame)//即将返回用户空间前的Trap现场帧
{
PKTHREADThread=KeGetCurrentThread();
PKPROCESSProcess=Thread->ApcState.Process;
OldTrapFrame=Thread->TrapFrame;
Thread->TrapFrame=TrapFrame;
Thread->ApcState.KernelApcPending=FALSE;
if(Thread->SpecialApcDisable)gotoQuickie;
//先固定执行掉内核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)//称为SpecialApc,少见
{
RemoveEntryList(ApcListEntry);//关键,移除队列
Apc->Inserted=FALSE;
KiReleaseApcLock(&ApcLock);
//执行内核中的总apc函数
KernelRoutine(Apc,&NormalRoutine,&NormalContext,
&SystemArgument1,&SystemArgument2);
}
Else//典型,一般程序员都会提供一个自己的内核apc函数
{
if((Thread->ApcState.KernelApcInProgress)||(Thread->KernelApcDisable))
{
KiReleaseApcLock(&ApcLock);
gotoQuickie;
}
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(INPKEXCEPTION_FRAMEExceptionFrame,
INPKTRAP_FRAMETrapFrame,//原真正的断点现场帧
INPKNORMAL_ROUTINENormalRoutine,
INPVOIDNormalContext,
INPVOIDSystemArgument1,
INPVOIDSystemArgument2)
{
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,offsetKiUserApcExceptionHandler
--------------------------------------------------------------------------------------
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//相当于callIntCallUserApc(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
Ret16
}
如上,每当要执行一个用户空间apc时,都会‘提前’偏离原来的路线返回用户空间的这个函数处去执行用户的apc。在执行这个函数前,会先构造一个seh节点,也即相当于把这个函数的调用放在try块中保护。这个函数内部会调用IntCallUserApc,执行完真正的用户apc函数后,调用ZwContinue重返内核。
VoidCALLBACK//用户空间的总apc函数
IntCallUserApc(void*RealApcFunc,void*SysArg1,void*SysArg2)
{
(*RealApcFunc)(SysArg1);//也即调用RealApcFunc(void*context)
}
NTSTATUSNtContinue(CONTEXT*Context,//原真正的TraFrame
BOOLTestAlert//指示是否继续执行用户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//callKiContinue(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(INPCONTEXTContext,//原来的断点现场
INPKEXCEPTION_FRAMEExceptionFrame,
INPKTRAP_FRAMETrapFrame)//NtContinue自身的TrapFrame地址
{
NTSTATUSStatus=STATUS_SUCCESS;
KIRQLOldIrql=APC_LEVEL;
KPROCESSOR_MODEPreviousMode=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);
returnStatus;
}
VOID
KiContinuePreviousModeUser(INPCONTEXTContext,//原来的断点现场
INPKEXCEPTION_FRAMEExceptionFrame,
INPKTRAP_FRAMETrapFrame)//NtContinue自身的TrapFrame地址
{
CONTEXTLocalContext;
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函数的意思)
BOOLEANKeTestAlertThread(INKPROCESSOR_MODEAlertMode)
{
PKTHREADThread=KeGetCurrentThread();
KiAcquireApcLock(Thread,&ApcLock);
OldState=Thread->Alerted[AlertMode];
if(OldState)
Thread->Alerted[AlertMode]=FALSE;
elseif((AlertMode!=KernelMode)&&
(!IsListEmpty(&Thread->ApcState.ApcListHead[UserMode])))
{
Thread->ApcState.UserApcPending=TRUE;//关键。又标记为不空,从而又去执行用户apc
}
KiReleaseApcLock(&ApcLock);
returnOldState;
}
上面这个函数的关键工作是检测到用户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实质上是下面的函数:
VOIDFASTCALL
KfLowerIrql(INKIRQLOldIrql)
{
ULONGEFlags;
ULONGPendingIrql,PendingIrqlMask;
PKPCRPcr=KeGetPcr();
PIC_MASKMask;
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处理执行那种中断。
VOIDFASTCALL
HalRequestSoftwareInterrupt(INKIRQLIrql)//Irql一般是APC_LEVEL/DPC_LEVEL
{
ULONGEFlags;
PKPCRPcr=KeGetPcr();
KIRQLPendingIrql;
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
VOIDFASTCALL
HalpApcInterruptHandler(INPKTRAP_FRAMETrapFrame)
{
//模拟硬件中断压入保存的寄存器
TrapFrame->EFlags=__readeflags();
TrapFrame->SegCs=KGDT_R0_CODE;
TrapFrame->Eip=TrapFrame->Eax;
KiEnterInterruptTrap(TrapFrame);//构造Trap现场帧
扫描执行当前线程的内核apc队列,略…
KiEoiHelper(TrapFrame);
}