时间:2025-09-01 18:32
人气:
作者:admin
前几天我通过改造微软的vhidmini2这个驱动示例,写了一个umdf的虚拟hid键盘,然后我发现,微软还提供了一个叫Virtual Hid Framework(VHF)的框架,专门用来实现虚拟hid设备,在kmdf和umdf上都支持(文档这么说的),所以就想着用VHF来重写一下上次的那个虚拟hid键盘。
使用VHF开发的驱动程序叫做源驱动程序,源驱动程序的作用是控制VHF设备对象的生命周期,以及为VHF设备对象提供数据。下面这张官方文档中的设备树显示了它们之间的层次关系。

绿色框的FDO指的是源驱动程序,就是我们要开发的部分,开发时需要引用Vhfkm.lib(在umdf中是Vhfum.lib)来使用vhf提供的api。PDO是物理设备对象,通常是上级设备的FDO枚举出来的设备,对于虚拟设备来说,一般是通过devgen等方式生成的设备。PDO一般会显示在设备管理器中,未安装驱动时显示为Unknown Device,源驱动程序就是安装在这个设备上的。Vhf.sys是VHF框架的核心,作为LowerFilter安装在源驱动程序上,过滤PDO到源驱动程序之间的请求,这些请求一般是设备生命周期相关的请求,例如PNP事件等,与HID功能无关。Vhf.sys会枚举出一个PDO来,这个是实现虚拟HID设备功能的PDO,系统会在它上面安装HidClass的驱动。
VHF之于虚拟hid设备的开发,就像WPF之于桌面应用开发,虽然实现的自由度会稍微受限,但确实方便很多。使用VHF,不需要自己去处理复杂的IRP请求,其缓冲策略也有默认的实现,只需要设置好几个回调函数就行了,给我的感觉真的是像开发WPF应用一样。
虽然VHF用起来很方便,但是官方文档较少,示例代码也不完整,所以用起来还是遇到不少困难。我最初还是想用umdf的驱动来实现虚拟hid键盘,但实在是调不通,文档和示例代码大多是基于kmdf的,搞不懂到底是哪里有问题,所以就先实现了一个kmdf的。
使用VHF的步骤非常简单,就分为两步:1.编写初始化代码;2.编写处理请求的回调函数。
下面先介绍一下可能会用到的主要函数和数据结构,然后再说明如何编写实例代码。
初始化需要依次使用VHF_CONFIG_INIT、VhfCreate、VhfStart三个函数,这三个函数是不是光看名称就很容易理解?
FORCEINLINE
VOID
VHF_CONFIG_INIT(
_Out_
PVHF_CONFIG Config,
#ifdef _KERNEL_MODE
_In_
PDEVICE_OBJECT DeviceObject,
#else
_In_
HANDLE FileHandle,
#endif
_In_
USHORT ReportDescriptorLength,
_In_reads_bytes_(ReportDescriptorLength)
PUCHAR ReportDescriptor
)
VHF_CONFIG_INIT函数的作用是初始化一个VHF_CONFIG的结构体,VHF_CONFIG结构体用来指定VHF框架对象的一些属性,例如PID、VID、回调函数指针等。
DeviceObject指定一个WDM设备对象与VHF关联,通常就是当前的设备对象。在kdmf中,可以通过WdfDeviceWdmGetDeviceObject来获取与WDF设备对象关联的WDM设备对象。
NTSTATUS VhfCreate(
[in] PVHF_CONFIG VhfConfig,
[out] VHFHANDLE *VhfHandle
);
VhfCreate函数的作用是使用刚刚初始化的VHF_CONFIG指定的配置,去创建一个VHF设备对象,调用成功的话,VhfHandle就是新创建的VHF设备对象的句柄。
NTSTATUS VhfStart(
[in] VHFHANDLE VhfHandle
);
VhfStart函数的作用就是启动刚刚创建的VHF设备对象。
VOID VhfDelete(
[in] VHFHANDLE VhfHandle,
[in] BOOLEAN Wait
);
在设备或驱动卸载之前,需要调用VhfDelete方法删除掉VHF设备对象。未正常删除VHF设备对象的话,系统会提示设备已更改,需要重启系统。
源驱动程序可以支持这些异步请求:GetFeature、 SetFeature、 WriteReport、 GetInputReport。在VHF_CONFIG结构体中设置相应的回调函数:EvtVhfAsyncOperationGetFeature、EvtVhfAsyncOperationSetFeature、EvtVhfAsyncOperationWriteReport、EvtVhfAsyncOperationGetInputReport,然后在VHF处理这些请求时,就会调用这些回调。
这些回调的类型都是EVT_VHF_ASYNC_OPERATION,定义如下:
EVT_VHF_ASYNC_OPERATION EvtVhfAsyncOperation;
VOID EvtVhfAsyncOperation(
[in] PVOID VhfClientContext,
[in] VHFOPERATIONHANDLE VhfOperationHandle,
[in, optional] PVOID VhfOperationContext,
[in] PHID_XFER_PACKET HidTransferPacket
)
{...}
VhfClientContext是回调的上下文参数,是在初始化时通过VHF_CONFIG结构体设置的。VhfOperationHandle是这次异步操作的句柄,通常用于设置异步操作的结果。HidTransferPacket是请求报告的数据包。
NTSTATUS VhfAsyncOperationComplete(
[in] VHFOPERATIONHANDLE VhfOperationHandle,
[in] NTSTATUS CompletionStatus
);
当源驱动程序处理完异步请求之后,必须要用回调传入的VhfOperationHandle参数,调用VhfAsyncOperationComplete函数来设置此次异步请求的结果。
NTSTATUS VhfReadReportSubmit(
[in] VHFHANDLE VhfHandle,
[in] PHID_XFER_PACKET HidTransferPacket
);
源驱动程序可以通过VhfReadReportSubmit函数向VHF提交一个输入报告,然后由VHF决定何时将该报告提交给系统。
源驱动程序也可以自己决定何时将输入报告提交给系统。可以通过VHF_CONFIG的EvtVhfReadyForNextReadReport字段来设置一个EVT_VHF_READY_FOR_NEXT_READ_REPORT类型的回调,它的定义如下:
EVT_VHF_READY_FOR_NEXT_READ_REPORT EvtVhfReadyForNextReadReport;
VOID EvtVhfReadyForNextReadReport(
[in] PVOID VhfClientContext
)
{...}
如果设置了EvtVhfReadyForNextReadReport回调,则当VHF准备好将缓冲区提交给系统时调用这个回调,然后由源驱动程序决定何时向缓冲区中填充输入报告。
源驱动程序仍然通过调用VhfReadReportSubmit来填充输入报告,一旦调用VhfReadReportSubmit后,VHF会尽快提交缓冲区,然后,直到下一次VHF调用EvtVhfReadyForNextReadReport回调后,源驱动程序才可以再次提交输入报告。
如果实现的是键盘、鼠标、触摸这类输入设备的话,一般而言,在启动VHF设备对象后EvtVhfReadyForNextReadReport会立即被调用。
还是以虚拟HID键盘为例,下面会从项目创建开始,完整演示一下用VHF框架实现虚拟HID设备的过程。
这里通过VS2022来创建项目,在创建项目前需要先完整地安装好WDK,WDK怎么安装官方有详细的文档,这里就不讲了。
项目模板就选择Kernel Mode Driver(KMDF)或者Kernel Mode Driver, Empty(KMDF),如果选择空模板的话,就要自己实现DriverEntry等函数,这里我选了Kernel Mode Driver(KMDF)模板。

项目创建后,右键项目,点击属性,在项目属性面板中,选择链接器-输入,在附加依赖项中添加vhfkm.lib,然后在头文件中包含vhf.h

Vhf.sys需要安装为源驱动程序的Lower Filter驱动,这可以通过INF文件来指定(仅限通过INF文件安装的情况)。模板中包含默认的INF文件,我们需要在INF文件的DDInstall.HW部分中添加一个AddReg指令(如果没有DDInstall.HW部分则添加一个),再添加一个对应的AddReg部分。类似下面这样:
[vhfkeyboardkm_Device.NT.HW]
AddReg = vhfkeyboardkm_Device.NT.AddReg
[vhfkeyboardkm_Device.NT.AddReg]
HKR,,"LowerFilters",0x00010000,"vhf"
模板实现的是一个PNP样式的驱动程序,它在EvtDriverDeviceAdd事件中完成WDF设备对象的创建和初始化,我们在它创建WDF设备对象后初始化VHF设备对象。模板的代码如下:
NTSTATUS
vhfkeyboardkmEvtDeviceAdd(
_In_ WDFDRIVER Driver,
_Inout_ PWDFDEVICE_INIT DeviceInit
)
{
NTSTATUS status;
UNREFERENCED_PARAMETER(Driver);
PAGED_CODE();
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Entry");
status = vhfkeyboardkmCreateDevice(DeviceInit);
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Exit");
return status;
}
NTSTATUS
vhfkeyboardkmCreateDevice(
_Inout_ PWDFDEVICE_INIT DeviceInit
)
{
WDF_OBJECT_ATTRIBUTES deviceAttributes;
PDEVICE_CONTEXT deviceContext;
WDFDEVICE device;
NTSTATUS status;
VHF_CONFIG vhfConfig;
PDEVICE_OBJECT pdo;
PAGED_CODE();
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&deviceAttributes, DEVICE_CONTEXT);
deviceAttributes.EvtCleanupCallback = EVT_CONTEXT_CLEANUP;
status = WdfDeviceCreate(&DeviceInit, &deviceAttributes, &device);
if (NT_SUCCESS(status)) {
deviceContext = DeviceGetContext(device);
RtlZeroMemory(deviceContext, sizeof(DEVICE_CONTEXT));
//status = WdfDeviceCreateDeviceInterface(
// device,
// &GUID_DEVINTERFACE_vhfkeyboardkm,
// NULL // ReferenceString
// );
//if (NT_SUCCESS(status)) {
// //
// // Initialize the I/O Package and any Queues
// //
// status = vhfkeyboardkmQueueInitialize(device);
/ 下一篇:HLK测试入门