最近有个需求,windows上实现虚拟声卡驱动,要包括虚拟扬声器和虚拟麦克风。windows提供了几个不同的开发框架(微软提供了示例代码),说明如下:

1. msvad

优点:支持win7

缺点:进行win7的hck测试时,失败用例较多,不知道怎么修改

说明:hck是win7提供的测试套件,win7能用且获得签名的驱动,正常win10也能用

2. AVStream\avshws

优点:支持win7, hck通过机率大

缺点:开发难度大,示例代码是camera,不知道怎么改成声卡

3. sysvad(simple)

微软示例代码中sysvad目录下有多个子目录,其中有个simpleaudiosimple子目录仅实现了最简单的扬声器和麦克风,符合我的需求

优点:开发难度效,示例代码能通过hlk用例,获取证书签名

缺点:不支持win7

说明:hlk是win10提供的测试套件

==============================================================

进入正题:

1. 选定了sysvad(simple)后,经过简单的适配修改,驱动可在打开了测试模式的win10云桌面中进行安装。

2. 驱动示例代码中生成了一段正弦波的pcm数据,模拟麦克风硬件的输入。

3. 我的场景是云桌面中安装一个虚拟的麦克风设备,然后将客户机真实的麦克风语音数据发送到云桌面,云桌面中的应用程序将接收的语音数据注入到虚拟麦克风设备中。

至此我需要修改驱动代码,使得应用程序可以与驱动通信,向其中注入语音数据。要解决这个问题,想到的思路是为设备对象创建一个用户态可以访问symbolicLink, 用户态程序调CreateFile打开这个设备符号,拿到设备句柄,然后调 DeviceIoControl 与设备通信,向其传输数据。

真正实施时遇到了一些错误,在此做个整理与大家分享。

概括:sysvad(simple) 使用了微软的PortCls框架,其内部会创建一个设备对象,为该对象创建symbolicLink的思路是不对的。应该是调 IoCreateDevice 创建一个设备对象,该对象用于与应用程序通信,便于应用程序注入音频数据,PortCls框架内部创建的设备对象只用于处理音频相关的业务。

------------------------------------------------------------------------------------------------------------------

1. 最开始使用NT的接口创建symboliclink,但用户态打开文件失败,GetLastError()返回2。

说明:NT创建接口的关键字: IoCreateDevice ,IoCreateSymbolicLink

2. 怀疑不能用NT的接口,于是换用 wdm的接口, 还是一样的现象

说明:wdm创建接口的关键字: IoRegisterDeviceInterface,IoSetDeviceInterfaceState

3. 网上找答案,有个帖子提到,驱动需要挂IRP_MJ_CREATE 和 IRP_MJ_CLOSE 例程,例程中完成irp即可, 代码如下:

DriverEntry
( 
    _In_  PDRIVER_OBJECT          DriverObject,
    _In_  PUNICODE_STRING         RegistryPathName
)
{
    ...
    DriverObject->MajorFunction[IRP_MJ_CREATE] = SimpleAudioSampleCreate;
    ...
}

NTSTATUS
SimpleAudioSampleCreate(
    _In_ PDEVICE_OBJECT DeviceObject,
    _In_ PIRP Irp
    )
{
    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

按上面修改后,CreateFile打开文件成功,但打开系统的“声音设置”界面,看不到虚拟声卡对应的扬声器和麦克风设备。

为什么会这样呢?

示例代码,在AddDevice函数中有调用
     ntStatus = 
        PcAddAdapterDevice
        ( 
            DriverObject,
            PhysicalDeviceObject,
            PCPFNSTARTDEVICE(StartDevice),
            maxObjects,
            sizeof(DEVICE_CONTEXT) + PORT_CLASS_DEVICE_EXTENSION_SIZE
        );

框架函数PcAddAdapterDevice内部会设置: DriverObject->MajorFunction[IRP_MJ_CREATE] = 框架内部的例程。

当应用程序 CreateFile时,会发送irp给PortCls框架内部的例程,这个流程不认识这个irp,返回相应错误,则表现为CreateFile返回2;

当我们自己挂载了IRP_MJ_CREATE例程且直接返回,会使得PortCls框架内部对应的例程没有被调用,少了一些操作,导致“声音设置”界面看不到虚拟声卡对应的扬声器和麦克风设备。

4. 网上说应该自己调 IoCreateDevice 和 IoCreateSymbolicLink 创建一个控制设备并在用户空间暴露符号连接。即一个驱动绑定两个设备对象,PortCls框架创建的设备对象处理与音频相关的业务,自己创建的设备对象,用于与私有的应用程序通信,注入音频数据。

此外 IRP_MJ_CREATE 例程需按如下修改:

NTSTATUS
SimpleAudioSampleCreate(
    _In_ PDEVICE_OBJECT DeviceObject,
    _In_ PIRP Irp
    )
{
    if (g_CtrlDeviceObj == DeviceObject)  // g_CtrlDeviceObj 是调IoCreateDevice 返回的控制设备对象的指针
    {
        // 来自于对控制设备的create操作,irp返回success,使得CreateFile可以返回成功
        Irp->IoStatus.Status = STATUS_SUCCESS;
        Irp->IoStatus.Information = 0;
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
        return STATUS_SUCCESS;
    }
    else
    {
        // 来自于对音频设备的create操作,交给PortCls框架处理
        return PcDispatchIrp(DeviceObject, Irp);
    }
}

背后的逻辑:

自己调IoCreateDevice 创建的控制设备对象(用DeviceObject_ctrl表示)中会有如下关系:

DeviceObject_ctrl->DriverObject = DriverObject;

PortCls框架中创建的音频设备对象(用DeviceObject_portcls_audio表示)也有如下的关系:

DeviceObject_portcls_audio->DriverObject = DriverObject;

系统处理音频相关的请求时,会打开对应的interface(wdm创建的symbolicLink称为interface)与驱动通信,会发送请求给内核中的 DeviceObject_portcls_audio 对象,

当是Create请求时,则会进入IRP_MJ_CREATE例程,即SimpleAudioSampleCreate函数,此时第一个入参为DeviceObject_portcls_audio ,其值不等于 g_CtrlDeviceObj,则会将irp发给PortCls框架处理;

同理:应用程序打开 DeviceObject_ctrl 对应的symbolicLink时,也会进入 SimpleAudioSampleCreate 函数,此时第一个入参为DeviceObject_ctrl,其值等于 g_CtrlDeviceObj,则会将irp立即完成,使得应用程序成功打开设备文件。

补充:驱动中也有变量,用于指向关联的设备对象

DriverObject->DeviceObject

DeviceObject指向的是一个链表,DeviceObject_ctrl 和 DeviceObject_portcls_audio 都挂载在该链表中

出现了新的问题:

应用程序打开 DeviceObject_ctrl 对应的symbolicLink时,CreateFile文件返回失败,GetLastError()的值为433: ERROR_INVALID_ACCESS

5.继续问AI,得到的反馈是不能在AddDevice函数中创建控制设备对象,而应在DriverEntry中创建

//修改前的代码

DriverEntry
( 
    _In_  PDRIVER_OBJECT          DriverObject,
    _In_  PUNICODE_STRING         RegistryPathName
)
{
    ...
    ntStatus =  PcInitializeAdapterDriver(DriverObject,
                                          RegistryPathName,
                                          (PDRIVER_ADD_DEVICE)AddDevice);
}

NTSTATUS AddDevice
( 
    _In_  PDRIVER_OBJECT    DriverObject,
    _In_  PDEVICE_OBJECT    PhysicalDeviceObject 
)
{
    PAGED_CODE();

    NTSTATUS        ntStatus;
    ULONG           maxObjects;

    DPF(D_TERSE, ("[AddDevice]"));

    maxObjects = g_MaxMiniports;

    // Tell the class driver to add the device.
    //
    ntStatus = 
        PcAddAdapterDevice
        ( 
            DriverObject,
            PhysicalDeviceObject,
            PCPFNSTARTDEVICE(StartDevice),
            maxObjects,
            sizeof(DEVICE_CONTEXT) + PORT_CLASS_DEVICE_EXTENSION_SIZE
        );
    if (!NT_SUCCESS(ntStatus)) {
        DbgPrint("[hsraudio:AddDevice] AddDevice PcAddAdapterDevice fail\n");
        return ntStatus;
    }

    PDEVICE_OBJECT fdo = DriverObject->DeviceObject;
    PDEVICE_CONTEXT deviceCtx =
        (PDEVICE_CONTEXT)((PUCHAR)fdo->DeviceExtension + PORT_CLASS_DEVICE_EXTENSION_SIZE);

    deviceCtx->pRingBuffer = NULL;

    // <== 开始是在这个位置调IoCreateDevice 和 IoCreateSymbolicLink 创建控制设备对应的设备对象以及在用户空间暴露符号连接的
    
    return ntStatus;
} // AddDevice

=======================================================================

// 修改后的代码:

DriverEntry
( 
    _In_  PDRIVER_OBJECT          DriverObject,
    _In_  PUNICODE_STRING         RegistryPathName
)
{
    UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\HsrVdiAudio");
    UNICODE_STRING symName = RTL_CONSTANT_STRING(L"\\DosDevices\\HsrVdiAudio");
    
    ...
    ntStatus =  PcInitializeAdapterDriver(DriverObject,
                                          RegistryPathName,
                                          (PDRIVER_ADD_DEVICE)AddDevice);
    ...
    // 改到 DriverEntry中创建了
    // create control device and symbolicLink
    ntStatus = IoCreateDevice(DriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &g_CtrlDeviceObj);
    IF_FAILED_ACTION_JUMP(
        ntStatus,
        DPF(D_ERROR, ("[hsraudio:DriverEntry] IoCreateDevice fail 0x%x", ntStatus)),
        Done);

    ntStatus = IoCreateSymbolicLink(&symName, &devName);
    IF_FAILED_ACTION_JUMP(
        ntStatus,
        DPF(D_ERROR, ("[hsraudio:DriverEntry] IoCreateSymbolicLink fail 0x%x", ntStatus)),
        Done);
}

IoCreateDevice ,IoCreateSymbolicLink 移到DriverEntry函数后,问题解决,系统“声音设置”中可以看到虚拟声卡对应的扬声器和麦克风设备;应用程序中也可以正常打开设备文件了。

附上应用程序相关打开代码:

#include <windows.h>

int main()
{
    // 1. 打开设备接口
    HANDLE hDev = CreateFileW(
        L"\\\\.\\HsrVdiAudio",
        GENERIC_WRITE,                 // 仅写权限
        0,
        NULL,                          // 默认安全
        OPEN_EXISTING,                 // 必须已存在
        FILE_ATTRIBUTE_NORMAL,         // 普通文件属性
        NULL
    );
    if (hDev == INVALID_HANDLE_VALUE) {
        std::cerr << "打开设备失败,错误码3:" << GetLastError() << std::endl;
        return 1;
    }
    ...
}

为什么会这样呢,AI给出的解释如下:

AddDevice 是 Pnp 框架调用驱动处理“真实或虚拟硬件”的入口,此处应调用 PcAddAdapterDevice()、PcRegisterSubdevice(),创建 PortCls 所需的内部设备对象;
这里再创建额外设备(如你那样的控制设备)就变成了多余或错误注册,且此设备未参与 Plug-and-Play,行为异常,系统可能无法正确分发 IRP

看不懂,只能先这样理解了。

6. 其它注意点

DriverEntry
( 
    _In_  PDRIVER_OBJECT          DriverObject,
    _In_  PUNICODE_STRING         RegistryPathName
)
{
    ...
    DriverObject->MajorFunction[IRP_MJ_PNP] = PnpHandler;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = SimpleAudioSampleDeviceControl;
    DriverObject->MajorFunction[IRP_MJ_CREATE] = SimpleAudioSampleCreate;
    DriverObject->MajorFunction[IRP_MJ_CLOSE]  = SimpleAudioSampleClose;  
    ...
}
    
如前所述,驱动中挂载的例程是公共的,不同设备对象会进入相同的例程函数,函数中需要通过入参来判断是哪个设备对象产生的调用,进行不同的处理,举例说明:
PnpHandler 不需要做判断,因为控制设备对象不受PNP管理;
SimpleAudioSampleCreate 需要进行判断,上面已经提供代码。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐