[6]Windows內核情景分析 --APC

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

}

相關文章
相關標籤/搜索