背景
之前我们在《NT驱动程序与用户层程序通信》这篇文章中讲了,用户层程序使用 DeviceIoControl 将 IOCTL 控制码、输入缓冲区、输出缓冲区传入到内核;内核响应 IRP_MJ_DEVICE_CONTRL 消息,并从 IRP 中获取传入的 IOCTL 控制码、输入缓冲区、输出缓冲区,以此实现数据的交互。
但是,当内核层想主动传递数据到用户层,用户层又怎样才能知道呢?因为只有用户层知道内核层有数据输出的时候,它才会调用 DeviceIoControl 函数去获取数据。所以,本文要介绍的就是基于事件 EVENT 实现的同步框架,可以解决这个的问题。现在,我就把实现思路和原理整理成文档,分享给大家。
函数介绍
CreateEvent 函数CreateEvent是一个Windows API函数。它用来创建或打开一个命名的或无名的事件对象。如果想为对象指定一个访问掩码,应当使用CreateEventEx函数。
函数声明
HANDLECreateEvent(
LPSECURITY_ATTRIBUTESlpEventAttributes,// 安全属性
BOOLbManualReset,// 复位方式
BOOLbInitialState,// 初始状态
LPCTSTRlpName// 对象名称
);
参数
lpEventAttributes[in]
一个指向SECURITY_ATTRIBUTES结构的指针,确定返回的句柄是否可被子进程继承。如果lpEventAttributes是NULL,此句柄不能被继承。
bManualReset[in]
指定将事件对象创建成手动复原还是自动复原。如果是TRUE,那么必须用ResetEvent函数来手工将事件的状态复原到无信号状态。如果设置为FALSE,当一个等待线程被释放以后,系统将会自动将事件状态复原为无信号状态。
bInitialState[in]
指定事件对象的初始状态。如果为TRUE,初始状态为有信号状态;否则为无信号状态。
lpName[in]
指定事件的对象的名称,是一个以0结束的字符串指针。名称的字符格式限定在MAX_PATH之内。名字是对大小写敏感的。如果lpName为NULL,将创建一个无名的事件对象。
返回值
如果函数调用成功,函数返回事件对象的句柄。
如果函数失败,函数返回值为NULL,如果需要获得详细的错误信息,需要调用GetLastError。
ObReferenceObjectByHandle 函数提供对象句柄访问许可。
如果访问被允许,返回相应的对象体的指针。
函数声明
NTSTATUSObReferenceObjectByHandle(
_In_HANDLEHandle,
_In_ACCESS_MASKDesiredAccess,
_In_opt_POBJECT_TYPEObjectType,
_In_KPROCESSOR_MODEAccessMode,
_Out_PVOID*Object,
_In_opt_POBJECT_HANDLE_INFORMATIONHandleInformation
);
参数
Handle [in]
为一个对象指定一个打开的句柄。
DesiredAccess [in]
指定访问对象的类型。其中,EVENT_MODIFY_STATE 表示允许使用 KeSetEvent 和 KeResetEvent 函数。
ObjectType [in, optional]
表明指向对象是什么类型的。其中,*ExEventObjectType 表示对象指针类型为 PKEVENT。
AccessMode [in]
访问模式分UserMode 和KernelMode。其中,KernelMode 表示内核模式。
Object [out]
指向映射句柄对象的指针。
HandleInformation [out, optional]
驱动程序设置为 NULL。
返回值
成功,则返回 STATUS_SUCCESS;否则,返回其它 NTSTATUS 错误码。
KeSetEvent 函数如果事件尚未发出信号,则 KeSetEvent 函数将事件对象设置为信号状态,并返回事件对象的先前状态。
函数声明
LONGKeSetEvent(
_Inout_PRKEVENTEvent,
_In_KPRIORITYIncrement,
_In_BOOLEANWait
);
参数
Event[in,out]
指向调用者为其提供存储的初始化事件对象的指针。
Increment[in]
如果设置事件导致等待满足,则指定要应用的优先级增量。其中,IO_NO_INCREMENT 表示不增加优先级。
Wait
指定是否通过调用 KeWaitXxx 函数之一来立即跟踪对KeSetEvent的调用。 如果为TRUE,则KeSetEvent调用之后必须调用KeWaitForMultipleObjects,KeWaitForMutexObject或KeWaitForSingleObject。 有关详细信息,请参阅以下备注部分。
返回值
如果事件对象的先前状态发出信号,则返回非零值。
实现原理
我们通过事件 EVENT 实现用户层与内核层的同步操作,具体的实现原理如下:
首先,我们在用户层程序中调用 CreateEvent 函数创建事件 EVENT 并获取事件 EVENT 的句柄。事件初始状态为无信号,而且自动复原。
然后,用户层程序调用 DeviceIoControl 函数将上述创建的 EVENT 句柄传递到内核层驱动程序中,并调用 WaitForSingleObject 函数等待事件 EVENT 的响应。直到事件 EVENT 响应后,程序才会进行下一步操作。
接着,内核层程序就通过 IRP_MJ_DEVICE_CONTROL 消息响应函数获取从用户层传入的事件 EVENT 的句柄。调用 ObReferenceObjectByHandle 内核函数获取内核事件 EVENT 对象。
然后,内核驱动程序可以调用 PsCreateSystemThread 创建多线程,继续执行操作。要想通知用户层程序进行下一步操作,只需调用 KeSetEvent 内核函数,将事件 EVENT 对象的状态设置为信号状态。那么,用户层程序中的事件 EVENT 就是一个有信号状态,WaitForSingleObject 函数就不会阻塞,而是继续往下执行。这样,就可以成功从内核层通知到用户层进行操作了。
最后,用户层调用 CloseHandle 关闭事件 EVENT 句柄;内核层调用 ObDereferenceObject 释放内核事件 EVENT 对象。
这个框架的核心原理就是,将用户层的事件句柄传入内核层的驱动程序中,并有内核驱动程序设置事件对象的信号状态,以此触发用户层的响应。
编码实现
用户层代码int_tmain(intargc,_TCHAR*argv[])
{
HANDLE hEvent=NULL;
HANDLE hDevice=NULL;
charszOutput[MAX_PATH]={0};
DWORD dwOutput=MAX_PATH;
DWORD dwRet=0;
BOOL bRet=FALSE;
// 创建事件, 设置自动复位,初始状态为无信号
hEvent=::CreateEvent(NULL,FALSE,FALSE,NULL);
if(NULL==hEvent)
{
printf("CreateEvent Error[%d]\n",::GetLastError());
}
// 打开设备
hDevice=::CreateFile(SYM_NAME,GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ|FILE_SHARE_WRITE,NULL,OPEN_EXISTING,
FILE_ATTRIBUTE_ARCHIVE,NULL);
if(INVALID_HANDLE_VALUE==hDevice)
{
printf("CreateFile Error[%d]\n",::GetLastError());
}
// 数据交互, 向内核层中传入事件句柄
bRet=::DeviceIoControl(hDevice,IOCTL_MY_TEST,&hEvent,sizeof(hEvent),szOutput,dwOutput,&dwRet,NULL);
if(FALSE==bRet)
{
printf("DeviceIoControl Error[%d]\n",::GetLastError());
}
// 一直等待事件的响应
::WaitForSingleObject(hEvent,INFINITE);
// 数据交互, 从内核层中获取数据
bRet=::DeviceIoControl(hDevice,IOCTL_MY_OUTPUT,NULL,0,szOutput,dwOutput,&dwRet,NULL);
if(FALSE==bRet)
{
printf("DeviceIoControl Error[%d]\n",::GetLastError());
}
// 显示
printf("[From Kernel Output]%s\n",szOutput);
// 关闭设备句柄
::CloseHandle(hEvent);
::CloseHandle(hDevice);
system("pause");
return0;
}
内核层代码
IRP_MJ_DEVICECONTROL 消息处理函数// IRP_MJ_DEVICE_CONTROL 消息处理函数
NTSTATUSDriverControlHandle(PDEVICE_OBJECT pDevObj,PIRP pIrp)
{
NTSTATUS status=STATUS_SUCCESS;
// 获取当前 IRP 栈空间数据
PIO_STACK_LOCATION pIoStackLocation=IoGetCurrentIrpStackLocation(pIrp);
// 获取输入/输出缓冲区
PVOID pBuffer=pIrp->AssociatedIrp.SystemBuffer;
// 获取输入缓冲区数据长度
ULONG ulInputLength=pIoStackLocation->Parameters.DeviceIoControl.InputBufferLength;
// 获取输出缓冲区数据长度
ULONG ulOutputLength=pIoStackLocation->Parameters.DeviceIoControl.OutputBufferLength;
// 实际输出数据长度
ULONG ulInfo=0;
// 获取控制码
ULONG ulControlCode=pIoStackLocation->Parameters.DeviceIoControl.IoControlCode;
// 根据操作码分别进行操作
switch(ulControlCode)
{
caseIOCTL_MY_TEST:
{
// 获取传入的事件句柄
HANDLE hUserEvent=*(HANDLE*)pBuffer;
// 处理类型32位、64位下类型不匹配的情况
if(4==ulInputLength)
{
hUserEvent=(HANDLE)((SIZE_T)hUserEvent&0x0FFFFFFFF);
}
// 根据事件句柄获取内核事件对象
status=ObReferenceObjectByHandle(hUserEvent,EVENT_MODIFY_STATE,*ExEventObjectType,KernelMode,(PVOID)(&g_pKernelEvent),NULL);
if(!NT_SUCCESS(status))
{
DbgPrint("ObReferenceObjectByHandle Error[0x%X]\n",status);
g_pKernelEvent=NULL;
break;
}
// 创建多线程, 执行操作, 执行完毕后, 发送事件通知用户层
HANDLE hThread=NULL;
PsCreateSystemThread(&hThread,0,NULL,NtCurrentProcess(),NULL,ThreadProc,g_pKernelEvent);
break;
}
caseIOCTL_MY_OUTPUT:
{
RtlCopyMemory(pBuffer,g_szOutputBuffer,50);
ulInfo=50;
break;
}
default:
break;
}
pIrp->IoStatus.Status=status;
pIrp->IoStatus.Information=ulInfo;
IoCompleteRequest(pIrp,IO_NO_INCREMENT);
returnstatus;
}
多线程处理函数// 多线程处理函数
VOIDThreadProc(PVOIDStartContext)
{
DbgPrint("Enter ThreadProc\n");
// 获取内核对象
PKEVENT pKernelEvent=(PKEVENT)StartContext;
// 设置输出缓冲区
RtlCopyMemory(g_szOutputBuffer,"I am DemonGan From Kernel Event.",(1+strlen("I am DemonGan From Kernel Event.")));
// 发送事件, 将事件对象设置为信号状态
if(NULL!=pKernelEvent)
{
KeSetEvent(pKernelEvent,IO_NO_INCREMENT,FALSE);
}
// 释放事件对象
ObDereferenceObject(pKernelEvent);
pKernelEvent=NULL;
DbgPrint("Leave ThreadProc\n");
}
程序测试
在 Win7 32 位系统下,驱动程序正常执行:
在 Win10 64 位系统下,驱动程序正常执行:
总结
这个框架理解起来不是很复杂,关键是理解事件 EVENT 的同步处理,实现操作的先后顺序。
参考