异步过程调用(APCs) 是NT异步处理体系结构中的一个基础部分,理解了它,对于了解NT怎样操作和执行几个核心的系统操作很有帮助。
1) APCs允许用户程序和系统元件在一个进程的地址空间内某个线程的上下文中执行代码。
2) I/O管理器使用APCs来完成一个线程发起的异步的I/O操作。例如:当一个设备驱动调用IoCompleteRequest来通知I/O管理器,它已经结束处理一个异步I/O请求时,I/O管理器排队一个apc到发起请求的线程。然后线程在一个较低IRQL级别,来执行APC. APC的作用是从系统空间拷贝I/O操作结果和状态信息到线程虚拟内存空间的一个缓冲中。
3) 使用APC可以得到或者设置一个线程的上下文和挂起线程的执行。
上面是网上找来的,下面是MSDN上的说明:
An asynchronous procedure call (APC) is a function that executes asynchronously in the context of a particular thread. When an APC is queued to a thread, the system issues a software interrupt. The next time the thread is scheduled, it will run the APC function. An APC generated by the system is called a kernel-mode APC. An APC generated by an application is called a user-mode APC. A thread must be in an alertable state to run a user-mode APC.
Each thread has its own APC queue. An application queues an APC to a thread by calling the QueueUserAPC function. The calling thread specifies the address of an APC function in the call to QueueUserAPC. The queuing of an APC is a request for the thread to call the APC function.
还先看一下那些重要结构:
kd> dt KTHREAD
nt!KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x010 MutantListHead : _LIST_ENTRY
+0x018 InitialStack : Ptr32 Void
+0x01c StackLimit : Ptr32 Void
+0x020 KernelStack : Ptr32 Void
+0x024 ThreadLock : Uint4B
+0x028 ApcState : _KAPC_STATE
+0x028 ApcStateFill : [23] UChar
+0x03f ApcQueueable : UChar
+0x040 NextProcessor : UChar
+0x041 DeferredProcessor : UChar
+0x042 AdjustReason : UChar
+0x043 AdjustIncrement : Char
+0x044 ApcQueueLock : Uint4B
+0x048 ContextSwitches : Uint4B
+0x04c State : UChar
+0x04d NpxState : UChar
+0x04e WaitIrql : UChar
+0x04f WaitMode : Char
+0x050 WaitStatus : Int4B
+0x054 WaitBlockList : Ptr32 _KWAIT_BLOCK
+0x054 GateObject : Ptr32 _KGATE
+0x058 Alertable : UChar
+0x059 WaitNext : UChar
+0x05a WaitReason : UChar
+0x05b Priority : Char
+0x05c EnableStackSwap : UChar
+0x05d SwapBusy : UChar
+0x05e Alerted : [2] UChar
+0x060 WaitListEntry : _LIST_ENTRY
+0x060 SwapListEntry : _SINGLE_LIST_ENTRY
+0x068 Queue : Ptr32 _KQUEUE
+0x06c WaitTime : Uint4B
+0x070 KernelApcDisable : Int2B
+0x072 SpecialApcDisable : Int2B
+0x070 CombinedApcDisable : Uint4B
+0x074 Teb : Ptr32 Void
+0x078 Timer : _KTIMER
+0x078 TimerFill : [40] UChar
+0x0a0 AutoAlignment : Pos 0, 1 Bit
+0x0a0 DisableBoost : Pos 1, 1 Bit
+0x0a0 ReservedFlags : Pos 2, 30 Bits
+0x0a0 ThreadFlags : Int4B
+0x0a8 WaitBlock : [4] _KWAIT_BLOCK
+0x0a8 WaitBlockFill0 : [23] UChar
+0x0bf SystemAffinityActive : UChar
+0x0a8 WaitBlockFill1 : [47] UChar
+0x0d7 PreviousMode : Char
+0x0a8 WaitBlockFill2 : [71] UChar
+0x0ef ResourceIndex : UChar
+0x0a8 WaitBlockFill3 : [95] UChar
+0x107 LargeStack : UChar
+0x108 QueueListEntry : _LIST_ENTRY
+0x110 TrapFrame : Ptr32 _KTRAP_FRAME
+0x114 CallbackStack : Ptr32 Void
+0x118 ServiceTable : Ptr32 Void
+0x11c ApcStateIndex : UChar
+0x11d IdealProcessor : UChar
+0x11e Preempted : UChar
+0x11f ProcessReadyQueue : UChar
+0x120 KernelStackResident : UChar
+0x121 BasePriority : Char
+0x122 PriorityDecrement : Char
+0x123 Saturation : Char
+0x124 UserAffinity : Uint4B
+0x128 Process : Ptr32 _KPROCESS
+0x12c Affinity : Uint4B
+0x130 ApcStatePointer : [2] Ptr32 _KAPC_STATE
+0x138 SavedApcState : _KAPC_STATE
+0x138 SavedApcStateFill : [23] UChar
+0x14f FreezeCount : Char
+0x150 SuspendCount : Char
+0x151 UserIdealProcessor : UChar
+0x152 CalloutActive : UChar
+0x153 Iopl : UChar
+0x154 Win32Thread : Ptr32 Void
+0x158 StackBase : Ptr32 Void
+0x15c SuspendApc : _KAPC
+0x15c SuspendApcFill0 : [1] UChar
…………
…………
上面红色部分是APC机制用到的几个字段!!
kd> dt _KAPC_STATE
nt!_KAPC_STATE
+0x000 ApcListHead : [2] _LIST_ENTRY,每个指针指向_KAPC结构
+0x010 Process : Ptr32 _KPROCESS
+0x014 KernelApcInProgress : UChar
+0x015 KernelApcPending : UChar
+0x016 UserApcPending : UChar
显然,这里的 ApcListHead 就是 APC 队列头。不过这是个大小为 2 的数组,说明实际
上(每个线程)有两个 APC 队列。这是因为 APC 函数分为用户 APC 和内核 APC 两种,各有
各的队列。所谓用户 APC,是指相应的 APC 函数位于用户空间、在用户空间执行;而内核
APC,则相应的 APC 函数为内核函数。
SavedApcState也是个_KAPC_STATE结构,当当前程暂时“挂靠(Attach)”到另一个进程的地址空间的时侯,ApcState就拷贝到SavedApcState暂时存放!
当然,还要有状态信息说明本线程当前是处于“原始环境”还是“挂靠环境”,这就是 ApcStateIndex 的作用,代码中为 ApcStateIndex的值定义了一种枚举类型:
typedef enum _KAPC_ENVIRONMENT {
OriginalApcEnvironment,
AttachedApcEnvironment,
CurrentApcEnvironment,
InsertApcEnvironment
} KAPC_ENVIRONMENT;
实际可用于 ApcStateIndex 的只是 OriginalApcEnvironment和 AttachedApcEnvironment。
KAPC_STATE 指针数组 ApcStatePointer[2],就用ApcStateIndex 的当前值作为下标,而数组中的指针则根据情况可以分别指向两个APC_STATE 数据结构中的一个。
kd> dt _KAPC ;APC对象
nt!_KAPC
+0x000 Type : UChar
+0x001 SpareByte0 : UChar
+0x002 Size : UChar
+0x003 SpareByte1 : UChar
+0x004 SpareLong0 : Uint4B
+0x008 Thread : Ptr32 _KTHREAD
+0x00c ApcListEntry : _LIST_ENTRY
+0x014 KernelRoutine : Ptr32 void
+0x018 RundownRoutine : Ptr32 void
+0x01c NormalRoutine : Ptr32 void
+0x020 NormalContext : Ptr32 Void
+0x024 SystemArgument1 : Ptr32 Void
+0x028 SystemArgument2 : Ptr32 Void
+0x02c ApcStateIndex : Char
+0x02d ApcMode : Char
+0x02e Inserted : UChar
KernelRoutine、RundownRoutine、NormalRoutine。其中只有 NormalRoutine才指向(执行)APC 函数的请求者所提供的函数,其余两个都是辅助性的!
NTKERNELAPI
VOID
KeInitializeApc (
__out PRKAPC Apc,
__in PRKTHREAD Thread,
__in KAPC_ENVIRONMENT Environment,
__in PKKERNEL_ROUTINE KernelRoutine,
__in_opt PKRUNDOWN_ROUTINE RundownRoutine,
__in_opt PKNORMAL_ROUTINE NormalRoutine,
__in_opt KPROCESSOR_MODE ProcessorMode,
__in_opt PVOID NormalContext
);
这个函数主要用来初始化Apc(_KAPC)这个结构的,如果Environment==CurrentApcEnvironment,Apc->ApcStateIndex就由KTHREAD中的ApcStateIndex决定,但Environment不能大于KTHREAD中的ApcStateIndex!
最后,APC 请求的模式ProcessorMode,但是有个例外,那就是:如果指针NormalRoutine 为 0,那么实际的模式变成了 KernelMode。这是因为在这种情况下没有用户空间APC函数可以执行, 唯一将得到执行的是KernelRoutine!
最后,KeInitializeApc 设置Inserted域为FALSE。然而初始化APC对象,并没有把它存放到相应的APC队列中。
NTKERNELAPI
BOOLEAN
KeInsertQueueApc (
__inout PRKAPC Apc,
__in_opt PVOID SystemArgument1,
__in_opt PVOID SystemArgument2,
__in KPRIORITY Increment
);
据APC请求的具体情况,有时候要插在队列的前头,一般则挂在队列的尾部。
_KiServiceExit:
cli ; disable interrupts
DISPATCH_USER_APC ebp, ReturnCurrentEax
;
; Exit from SystemService
;
EXIT_ALL NoRestoreSegs, NoRestoreVolatile ;这个宏以后再讲
DISPATCH_USER_APC macro TFrame, ReturnCurrentEax
local a, b, c
c:
.errnz (EFLAGS_V86_MASK AND 0FF00FFFFh)
test byte ptr [TFrame]+TsEflags+2, EFLAGS_V86_MASK/010000h ; is previous mode v86?
jnz short b ; if nz, yes, go check for APC
test byte ptr [TFrame]+TsSegCs,MODE_MASK ; is previous mode user mode?
jz a ; No, previousmode=Kernel, jump out
b: mov ebx, PCR[PcPrcbData+PbCurrentThread]; get addr of current thread
mov byte ptr [ebx]+ThAlerted, 0 ; clear kernel mode alerted
cmp byte ptr [ebx]+ThApcState.AsUserApcPending, 0
je a ; if eq, no user APC pending
mov ebx, TFrame
ifnb
;DISPATCH_USER_APC ebp, ReturnCurrentEax,显然这里是编译的!
mov [ebx].TsEax, eax ; Store return code in trap frame
mov dword ptr [ebx]+TsSegFs, KGDT_R3_TEB OR RPL_MASK
mov dword ptr [ebx]+TsSegDs, KGDT_R3_DATA OR RPL_MASK
mov dword ptr [ebx]+TsSegEs, KGDT_R3_DATA OR RPL_MASK
mov dword ptr [ebx]+TsSegGs, 0
endif
;
; Save previous IRQL and set new priority level
;
RaiseIrql APC_LEVEL
push eax ; Save OldIrql
sti ; Allow higher priority ints
;
; call the APC delivery routine.
;
; ebx - Trap frame
; 0 - Null exception frame
; 1 - Previous mode
;
; call APC deliver routine
;
stdCall _KiDeliverApc, <1, 0, ebx> ;1就是UserMode
pop ecx ; (ecx) = OldIrql
LowerIrql ecx
ifnb
mov eax, [ebx].TsEax ; Restore eax, just in case
endif
cli
jmp b ; 注意这个循环!!
ALIGN 4
a:
endm
这段代码主要检查:
条件符合才用KiDeliverApc真正投递APC。注意代码jmp b,好像在返回用户空间前KiDeliverApc会被循环调用直到没有user APC,其实不是的,KiDeliverApc每处理完一个User APC就把UserApcPending清零,所以User APCs在返回用户空间时还是只能投递一次!KiDeliverApc中用while处理完所有Kernel Mode APCs,但User Mode APC却只处理一个!事实上User APC的投递是很特殊的,以后会讲到,而且每次只能投递一次!
前面讲过,KTHREAD 中有两个 KAPC_STATE 数据结构,一个是 ApcState,另一个是SavedApcState,二者都有APC 队列,但是要投递的只是ApcState 中的队列。
KiDeliverApc (
IN KPROCESSOR_MODE PreviousMode,//写成DeliverMode不是更好
IN PKEXCEPTION_FRAME ExceptionFrame,//这个参数几乎就是0
IN PKTRAP_FRAME TrapFrame
)
这个函数里面还比较复杂,代码不帖了。
参数PreviousMode表示需要“投递”哪一种 APC,可以是UserMode,也可以是KernelMode。不过,KernelMode 确实表示只要求执行内核 APC,而UserMode 却表示在执行内核 APC 之外再执行用户APC。
The Windows operating system uses three kinds of APCs:
从代码的角度看是这样的:
User APCs
_KAPC.ApcMode==UserMode,_KAPC.KernelRoutine!=NULL,_KAPC.NormolRoutine!=NULL
Normal kernel APCs
_KAPC.ApcMode==KernelMode,_KAPC.KernelRoutine!=NULL,_KAPC.NormolRoutine!=NULL
Special kernel APCs
_KAPC.ApcMode==KernelMode,_KAPC.KernelRoutine!=NULL,_KAPC.NormolRoutine==NULL
有一点它们的_KAPC.KernelRoutine肯定不为空。并且,如果NormolRoutine也不为空,那么KernelRoutine都在NormolRoutine被调用前被调用!!
未完待续……