天天看点

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。