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);

}

[6]Windows内核情景分析 --APC的更多相关文章

  1. windows内核情景分析之—— KeRaiseIrql函数与KeLowerIrql()函数

    windows内核情景分析之—— KeRaiseIrql函数与KeLowerIrql()函数 1.KeRaiseIrql函数 这个 KeRaiseIrql() 只是简单地调用 hal 模块的 KfRa ...

  2. 几个常用内核函数(《Windows内核情景分析》)

    参考:<Windows内核情景分析> 0x01  ObReferenceObjectByHandle 这个函数从句柄得到对应的内核对象,并递增其引用计数. NTSTATUS ObRefer ...

  3. [1]windows 内核情景分析---说明

    本文说明:这一系列文章(笔记)是在看雪里面下载word文档,现转帖出来,希望更多的人能看到并分享,感谢原作者的分享精神. 说明 本文结合<Windows内核情景分析>(毛德操著).< ...

  4. [7] Windows内核情景分析---线程同步

    基于同步对象的等待.唤醒机制: 一个线程可以等待一个对象或多个对象而进入等待状态(也叫睡眠状态),另一个线程可以触发那个等待对象,唤醒在那个对象上等待的所有线程. 一个线程可以等待一个对象或多个对象, ...

  5. [14]Windows内核情景分析 --- 文件系统

    文件系统 一台机器上可以安装很多物理介质来存放资料(如磁盘.光盘.软盘.U盘等).各种物理介质千差万别,都配备有各自的驱动程序,为了统一地访问这些物理介质,windows设计了文件系统机制.应用程序要 ...

  6. [11]Windows内核情景分析---设备驱动

    设备驱动 设备栈:从上层到下层的顺序依次是:过滤设备.类设备.过滤设备.小端口设备[过.类.过滤.小端口] 驱动栈:因设备堆栈原因而建立起来的一种堆栈 老式驱动:指不提供AddDevice的驱动,又叫 ...

  7. [4]Windows内核情景分析---内核对象

    写过Windows应用程序的朋友都常常听说"内核对象"."句柄"等术语却无从得知他们的内核实现到底是怎样的, 本篇文章就揭开这些技术的神秘面纱. 常见的内核对象 ...

  8. [2]windows内核情景分析--系统调用

    Windows的地址空间分用户模式与内核模式,低2GB的部分叫用户模式,高2G的部分叫内核模式,位于用户空间的代码不能访问内核空间,位于内核空间的代码却可以访问用户空间 一个线程的运行状态分内核态与用 ...

  9. [15]Windows内核情景分析 --- 权限管理

    Windows系统是支持多用户的.每个文件可以设置一个访问控制表(即ACL),在ACL中规定每个用户.每个组对该文件的访问权限.不过,只有Ntfs文件系统中的文件才支持ACL. (Ntfs文件系统中, ...

随机推荐

  1. [Day2]变量、数据类型转换以及运算符

    1.变量 变量是内存中装载数据的小盒子,你只能用它来存取数据 2.计算机存储单元 (1)计算机存储设备的最小信息单元叫“位(bit)”,“比特位” (2)8个比特位表示一个数据,是计算机的最小存储单元 ...

  2. Ubuntu 18.04 安装中文输入法ibus

    在安装ubuntu时,如果选择英文安装默认会不启用中文输入法的.可以通过下述方法开启 安装 pingyin 输入法 sudo apt-get install ibus-pinyin 然后在 setti ...

  3. 阿里创新自动化测试工具平台--Doom

    摘要: 阿里内部诞生一了个依赖真实流量用于自动回归的自动化测试平台,通过创新的自动mock机制不仅支持读接口的回归验证,同时支持了写接口验证,在内部产生了极大价值,有价值的东西就应该分享,目前该工具已 ...

  4. Android抓包方法 之Fiddler代理

    1.抓包原理 Fiddler是类似代理服务器的形式工作,它能够记录所有你的电脑和互联网之间的http(S)通讯,可以查看.修改所有的“进出”的数据.使用代理地址:127.0.0.1, 默认端口:888 ...

  5. 1、python接口测试requests

    import requestsimport jsonr=requests.get('http://www.baidu.com')                        #get 请求方式r=r ...

  6. C++ 调用 opencv 读取视频文件列表并处理

    //g++ trans_video.cpp -o trans_video `pkg-config opencv --libs --cflags` -L/usr/lib/x86_64-linux-gnu ...

  7. Vue的双向数据绑定原理是什么?

    vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调. ...

  8. 第四章:初识CSS3

    1.CSS规则由两部分构成,即选择器和声明器 声明必须放在{}中并且声明可以是一条或者多条 每条声明由一个属性和值构成,属性和值用冒号分开,每条语句用英文冒号分开 注意: css的最后一条声明,用以结 ...

  9. Ubuntu上Xilinx ARM交叉编译器安装

    1,Windows中下载交叉编译器 2,在ubuntu中创建zedboard目录,并将交叉编译器复制进来 3,将该交叉编译器设置成可执行程序 chmod  a+x  xilinx-2011.09-50 ...

  10. 3 jmeter的两种录制方法

    录制1-badboy(推荐) badboy是一款自动化测试工具,它可以完成简单的功能测试和性能测试.其实它是一款独立的测试工具,只不过它录制东西导出的格式适用于jmeter,所以我们经常把jmeter ...