天天看點

windows 核心原理與實作讀書筆記之APC(異步過程調用)

APC(異步過程調用)

APC_LEVEL ,APC(異步過程調用,Asynchronous Procedure Call)的軟體中斷而保留的IRQL。

DPC是系統全局的,每個處理器都有DPC連結清單;APC是針對線程的,每個線程都有自己特有的APC連結清單。APC線程優先于普通的線程代碼。

APC 對象定義如下:

typedef struct _KAPC {

            CSHORT Type;

            CSHORT Size;

            ULONG Spare0;

            struct _KTHREAD *Thread;

            LIST_ENTRY ApcListEntry;                  // 插入線程APC連結清單

            PKKERNEL_ROUTINE KernelRoutine;           //核心模式中執行

            PKRUNDOWN_ROUTINE RundownRoutine;         // 線程終止時還有APC沒執行會調用這個函數

            PKNORMAL_ROUTINE NormalRoutine; //這個為0 表示是一個特殊核心APC,否則是一個普通的(又分為核心态的和使用者态的)。特殊的位于連結清單前部,普通的位于後部。 普通的APC,normal和kernel例程都将被調用

            PVOID NormalContext;

            //

            // N.B. The following two members MUST be together.

            //

            PVOID SystemArgument1;

            PVOID SystemArgument2;

            CCHAR ApcStateIndex;                          //APC環境狀态

            KPROCESSOR_MODE ApcMode;                      // 核心态or使用者态

            BOOLEAN Inserted;

} KAPC, *PKAPC, *RESTRICTED_POINTER PRKAPC;

Type :類型。在Windows裡,任何一種核心對象都有一個編号,這個編号用來辨別你是屬于哪一種類型,APC本身也是一種核心對象,它也有一個編号,是0x12

Size:這個成員指的是目前的KAPC的結構體的大小

Thread:每一個線程都有自己的APC隊列,這個成員指定了APC屬于哪一個線程

ApcListEntry:APC隊列挂的位置,是一個雙向連結清單,通過這個雙向連結清單可以找到下一個APC

KernelRoutine:指向一個函數(調用ExFreePoolWithTag 釋放APC)。當我們的APC執行完畢以後,目前的KAPC本身的這塊記憶體,會由KernelRoutine指定的函數來釋放

NormalRoutine:如果目前是核心APC,通過這個值找到的就是真正的核心APC函數;如果目前的APC是使用者APC,那麼這個位置指向的是使用者APC總入口,通過這個總入口可以找到所有使用者提供的APC函數

NormalContext:如果目前是核心APC,通過這個值為空;如果目前的APC是使用者APC,那麼這個值指向的是真正的使用者APC函數

SystemArgument1 SystemArgument2 APC函數的參數

ApcStateIndex:目前的APC要挂到哪個隊列

ApcMode:目前的APC是使用者APC還是核心APC

Inserted:目前的APC結構體是否已經插入到APC隊列。

Windows 支援普通和特殊的兩種核心模式APC。特殊APC是指NormalRoutine 成員為NULL的APC對象,普通APC是指NormalRoutine 成員不為NULL,但ApcMode 為KernelMode。

KAPC_ENVIRONMENT 、KAPC_STATE 結構定義:

typedef enum _KAPC_ENVIRONMENT {

  OriginalApcEnvironment, //原始的程序環境

  AttachedApcEnvironment, //挂靠後的程序環境

  CurrentApcEnvironment, // 目前環境

   InsertApcEnvironment   //被插入時的環境

} KAPC_ENVIRONMENT;

typedef struct _KAPC_STATE {

        LIST_ENTRY ApcListHead[MaximumMode];  //線程的apc連結清單隻有兩個核心态和使用者态

        struct _KPROCESS *Process;      //目前線程的程序體   PsGetCurrentProcess()

        BOOLEAN KernelApcInProgress;              //核心APC正在執行

        BOOLEAN KernelApcPending;                 //核心APC正在等待執行

        BOOLEAN UserApcPending;                  //使用者APC正在等待執行

} KAPC_STATE, *PKAPC_STATE, *PRKAPC_STATE;

APC 對象的用法和實作:

APC對象是通過KeInitializeApc 實作,通過KeInsertQueueAPC 來插入。

NTKERNELAPI

    VOID

    KeInitializeApc (

    IN PRKAPC Apc,

    IN PKTHREAD Thread,

    IN KAPC_ENVIRONMENT Environment,

    IN PKKERNEL_ROUTINE KernelRoutine,

    IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,

    IN PKNORMAL_ROUTINE NormalRoutine OPTIONAL,     //并非真正的目标函數,相當于門戶作用

    IN KPROCESSOR_MODE ApcMode,

    IN PVOID Context                        //真正我們實作的函數

    )

{

    RtlZeroMemory(Apc,sizeof(KAPC));

    Apc->Type = ApcObject;  // APC是類型為ApcObejct的核心對象

    Apc->Size = sizeof(KAPC);

    if (Environment == CurrentApcEnvironment) { //目前環境,那Index就是線程的

        Apc->ApcStateIndex = Thread->ApcStateIndex;

    } else {

        ASSERT((Environment <= Thread->ApcStateIndex) || (Environment == InsertApcEnvironment));

        Apc->ApcStateIndex = (CCHAR)Environment;

    }

    Apc->Thread = Thread;

    Apc->KernelRoutine = KernelRoutine;

    Apc->RundownRoutine = RundownRoutine;

    Apc->NormalRoutine = NormalRoutine;

    if(NormalRoutine)

    {

        //NormalRoutine非空,是需要在使用者空間執行的APC函數

        Apc->ApcMode = Mode;

        Apc->NormalContext = Context;   //我們真正認為的APC執行函數

    }

    else

    {

        //沒有需要在使用者空間執行的NormalRoutine

        Apc->ApcMode = KernelMode;

        Apc->NormalContext = NULL;

    }   

Apc->Inserted = FALSE;

}

KeInitializeApc 首先設定APC對象的Type和Size域一個适當的值,然後檢查參數Environment的值,如果是CurrentApcEnvironment,那麼ApcStateIndex域設定為目标線程的ApcStateIndex域。否則,ApcStateIndex域設定為參數Environment的值。

随後,函數直接用參數設定APC對象Thread,RundownRoutine,KernelRoutine域的值。為了正确地确定APC的類型,KeInitializeApc 檢查參數NORMAL_ROUTINE的值,如果是NULL,ApcMode域的值設定為KernelMode,NormalContext域設定為NULL。

  如果NORMAL_ROUTINE的值不是NULL,這時候它一定指向一個有效的例程,就用相應的參數來設定ApcMode域和NormalContext域。最後,KeInitializeApc 設定Inserted域為FALSE.然而初始化APC對象,并沒有把它存放到相應的APC隊列中。

   從代碼可以看出,APCs對象如果缺少有效的NORMAL_ROUTINE,就會被當作核心模式APCs.尤其是它們會被認為是特殊的核心模式APCs.

任意類型的APC都可以定義一個有效的RundownRoutine,這個例程必須在核心記憶體區域,并且僅僅當系統需要釋放APC隊列的内容時,才被調用。例如線程退出時,在這種情況下,KernelRoutine和NormalRoutine都不執行,隻有RundownRoutine執行。沒有這個例程的APC對象會被删除。

在KeInsertQueueApc 将APC對象存放到目标線程相應的APC隊列之前,它首先檢查目标線程是否是APC queueable。如果不是,函數立即傳回FALSE.如果是,函數直接用參數設定SystemArgument1域和SystemArgument2 域,随後,函數調用KiInsertQueueApc來将APC對象存放到相應的APC隊列。

  KiInsertQueueApc 僅僅接受一個APC對象和一個優先級增量。這個函數首先得到線程APC隊列的spinlock并且持有它,防止其他線程修改目前線程的APC結構。随後,檢查APC對象的Inserted 域。如果是TRUE,表明這個APC對象已經存放到APC隊列中了,函數立即傳回FALSE.如果APC對象的Inserted 域是FALSE.函數通過ApcStateIndex域來确定目标APC環境,然後把APC對象存放到相應的APC隊列中,即将APC對象中的ApcListEntry 域鍊入到APC環境的ApcListHead域中。鍊入的位置由APC的類型決定。正常的核心模式APC,使用者模式APC都是存放到相應的APC隊列的末端。相反的,如果隊列中已經存放了一些APC對象,特殊的核心模式APC存放到隊列中第一個正常核心模式APC對象的前面。如果是核心定義的一個當線程退出時使用的使用者APC,它也會被放在相應的隊列的前面。然後,線程的主APC環境中的UserApcPending域設定為TRUE。這時KiInsertQueueApc 設定APC對象的Inserted 域為TRUE,表明這個APC對象已經存放到APC隊列中了。接下來,檢查這個APC對象是否被排隊到線程的目前程序上下文APC環境中,如果不是,函數立即傳回TRUE。如果這是一個核心模式APC,線程主APC環境中的KernelApcPending域設定為TRUE。

APC的調用時機

1.核心代碼離開臨界區或守護區,調用KiCheckForKernelApcDelivery或者請求APC級别中斷

2.KiSwapThread傳回以前調用一次KiDeliverApc

3.從系統服務或者異常傳回時 調用KiDeliverApc 設定使用者态apc的傳遞

4.APC_LEVEL的中斷和irql降到passive時KiDeliverApc都會被調用調用KiDeliverApc 來傳遞核心模式APC。