同步异步的两种用法

  1. FILE_FLAG_OVERLAPPED异步打开参数

在CreateFile打开设备对象时

HANDLE CreateFile(
  LPCSTR                lpFileName,
  DWORD                 dwDesiredAccess,
  DWORD                 dwShareMode,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  DWORD                 dwCreationDisposition,
  DWORD                 dwFlagsAndAttributes,   //FILE_FLAG_OVERLAPPED
  HANDLE                hTemplateFile
);

加入FILE_FLAG_OVERLAPPED参数就是以异步的方式打开驱动的设备对象

与驱动通讯时如下的调用界面及异步结构,需要设置一个OVERLAPPED的参数,参数中的hEvent事件需要初始化一个事件传入内核层,应用层等待事件完成做为通知。

BOOL ReadFile(
  HANDLE                        hFile,
  LPVOID                        lpBuffer,
  DWORD                         nNumberOfBytesToRead,
  LPDWORD                       lpNumberOfBytesRead,
  LPOVERLAPPED                  lpOverlapped    //异步处理事件
);

BOOL WINAPI DeviceIoControl(
  _In_        HANDLE       hDevice,
  _In_        DWORD        dwIoControlCode,
  _In_opt_    LPVOID       lpInBuffer,
  _In_        DWORD        nInBufferSize,
  _Out_opt_   LPVOID       lpOutBuffer,
  _In_        DWORD        nOutBufferSize,
  _Out_opt_   LPDWORD      lpBytesReturned,
  _Inout_opt_ LPOVERLAPPED lpOverlapped     //异步处理事件
);

typedef struct _OVERLAPPED {
  ULONG_PTR Internal;
  ULONG_PTR InternalHigh;
  union {
    struct {
      DWORD Offset;
      DWORD OffsetHigh;
    } DUMMYSTRUCTNAME;
    PVOID Pointer;
  } DUMMYUNIONNAME;
  HANDLE    hEvent;
} OVERLAPPED, *LPOVERLAPPED;

总结如下:

  • 应用层以FILE_FLAG_OVERLAPPED方式打开驱动设备对象
  • 应用层创建事件,传入到Overlapped参数结构中
  • 等待事件句柄至signaled状态

伪码如下:

HANDEL hDriver = CreateFile("\\.\\\\DeviceSymbolName",...,FILE_FLAG_OVERLAPPED,...);
..
OVERLAPPED overlap;
AsyncEvent = CreateEvent(NULL, TRUE, TRUE, "");
..
overlap.hEvent = AsyncEvent;  
overlap.Offset = 0;  
overlap.OffsetHigh = 0;  
..
DeviceIoControl (hDriver, ... ,&overlap);
..  
if (GetLastError() == ERROR_IO_PENDING)
{
    WaitForSingleObject(AsyncEvent, INFINTE);
}
  • 驱动层在IRP例程处理中返回STATUS_PENDING,标记IoMarkIrpPending(pIrp);
  • 异步处理完成后才最终调用IoCompleteRequest完成驱动处理
pIrp->IoStatus.Information = 0;  
pIrp->IoStatus.Status = STATUS_SUCCESS;  
pIrpStack = IoGetCurrentIrpStackLocation(pIrp);  
IoCode = pIrpStack->Parameters.DeviceIoControl.IoControlCode;  
switch (IoCode)  
{  
    case ... 
        status = STATUS_PENDING;  
        IoMarkIrpPending(pIrp);  
        pIrp->IoStatus.Status = status;  
        return status;  
        break;
}
  1. 另一种方式ReadFileEx及WriteFileEx
BOOL ReadFileEx(
  HANDLE                          hFile,
  LPVOID                          lpBuffer,
  DWORD                           nNumberOfBytesToRead,
  LPOVERLAPPED                    lpOverlapped,
  LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
  • ReadFileEx的LPOVERLAPPED不需要应用层提供并初始化Event
  • lpCompletionRoutine参数为驱动IRP结束处理完后的回调处理
  • 回调的原理就是插入的APC丢给应用层处理
  • APC会在线程进入警惕模式后执行
  • 线程在调用SleepEx,WaitForSingleObject等API时会进入警惕模式
  • 驱动层在IRP_MJ_CREATE中处理,处理跟第一种方式类似,都是返回STATUS_PENDING,标记IoMarkIrpPending(pIrp)

IoCompleteRequest结束处理,并返回到应用层,这里可以看一下WRK的代码处理 处理流程如下 IoCompleteRequest->IopCompleteRequest->IopfCompleteRequest 在结束的代码中第一种方式用了事件置位通知,第二种方式用了APC回调 IopfCompleteRequest

异步调用过程APC

APC是一种软中断过程,用于异步处理一些不是特别紧急的任务 调用接口界面如下

应用层

DWORD WINAPI QueueUserAPC(
  _In_  PAPCFUNC pfnAPC,
  _In_  HANDLE hThread,
  _In_  ULONG_PTR dwData
);

VOID CALLBACK APCProc(
  _In_  ULONG_PTR dwParam
);

内核层

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

NTKERNELAPI
BOOLEAN
KeInsertQueueApc (
    __inout PRKAPC Apc,
    __in_opt PVOID SystemArgument1,
    __in_opt PVOID SystemArgument2,
    __in KPRIORITY Increment
    );

KTHREAD的结构里保存了APC的一些关键结构

 dt nt!_KTHREAD
   ...
   +0x034 ApcState         : _KAPC_STATE
   ...
   +0x138 ApcStatePointer  : [2] Ptr32 _KAPC_STATE
   ...
   +0x14c SavedApcState    : _KAPC_STATE
   +0x164 Alertable        : UChar
   +0x165 ApcStateIndex    : UChar
   ...
  • ApcState 当前需要执行的APC就放在ApcState这里,当Attach到其他进程的APC也保存在这里,表示的就是当前的APC处理队列
  • SavedApcState Attached后保存的当前线程的ApcState(处理代码在KeAttachProcess->KiAttachProcess)
  • ApcStatePointer[2] 两个ApcState的指针
  • ApcStateIndex 表示一个状态当前是挂靠进程还是正常的进程,一般只用到正常状态及另一个其他进程的挂靠状态,靠这个值才能区分开APCState这个值当前队列是否为本进程

再看一下_KAPC_STATE

typedef struct _KAPC_STATE
{
    LIST_ENTRY ApcListHead[2];
    struct _KPROCESS *Process;
    BOOLEAN KernelApcInProgress;
    BOOLEAN KernelApcPending;
    BOOLEAN UserApcPending;
} KAPC_STATE, *PKAPC_STATE, *RESTRICTED_POINTER PRKAPC_STATE;

头部包含了两个APC链表,分别表示了内核及用户层的两种APC

APC调用时机

  • 过程KiDeliverApc在_KiServiceExit时被调用,也就是调用时机是在从Ring0退回到Ring3的过程中

KiDeliverApc处理的流程得参照代码来讲,过程如下

  • 从内核APC队列循环拿出APC,先处理只有KernelRoutine的APC,再处理普通的APC,包含了KernelRoutine及NormalRoutine,循环处理全部的内核APC
  • 再处理用户队列,只处理第一个,先执行KernelRoutine,NormalRoutine是要放在用户空间执行的,需要调用一个API处理KiInitializeUserApc()

另外在警惕模式下才会执行用户态的APC,具体是线程在调用特定API时会设置Thread->ApcState.UserApcPending这个判断条件