关键思路:为驱动创建一个设备对象
usbnwifi 例程中,
MPInitialize
函数通过NICRegisterDevice
函数,注册了NICDispatch
函数入口(该函数实现IRP的分发处理),从而为小端口驱动注册了一个IOCTL接口。这一过程根本上是调用了NDIS的NdisRegisterDeviceEx
函数,传入了包含分发函数入口的_NDIS_DEVICE_OBJECT_ATTRIBUTES
数据结构。
最近在编写 NDIS 小端口驱动程序时,需要利用 IOCTL 对网卡进行配置,但翻遍 NDIS 文档也没有找到 NDIS 的 IOCTL 实现。有幸在 WDK 7600 提供的 usbnwifi 例程中看到了实现思路,看起来基本上也就是 WDM 框架下 IOCTL 的简单封装。现在把思路写下来供思考,不全面的地方,建议直接参考 usbnwifi 例程的实现。
NDIS框架提供的NdisRegisterDeviceEx
函数,可以为 NDIS 小端口驱动创建一个设备对象。函数原型如下:
NDIS_STATUS NdisRegisterDeviceEx(
_In_ NDIS_HANDLE NdisHandle,
_In_ PNDIS_DEVICE_OBJECT_ATTRIBUTES DeviceObjectAttributes,
_Out_ PDEVICE_OBJECT *pDeviceObject,
_Out_ PNDIS_HANDLE NdisDeviceHandle
);
第一个参数是 NDIS 句柄不提。
第二个参数是 IRP 的关键参数。NDIS_DEVICE_OBJECT_ATTRIBUTES
结构定义了设备对象的若干属性。
先创建一个分发表,将分发函数入口点传入:
DispatchTable[IRP_MJ_CREATE] = NICDispatch_Create;
DispatchTable[IRP_MJ_CLEANUP] = NICDispatch_Cleanup;
DispatchTable[IRP_MJ_CLOSE] = NICDispatch_Close;
DispatchTable[IRP_MJ_DEVICE_CONTROL] = NICDispatch_IOCTL; // 也可将这四种传入同一个分发函数,在函数内做区分
将分发表和一些其他属性一起传给NDIS_DEVICE_OBJECT_ATTRIBUTES
结构:
DeviceObjectAttributes.Header.Type = NDIS_OBJECT_TYPE_DEFAULT; // type implicit from the context
DeviceObjectAttributes.Header.Revision = NDIS_DEVICE_OBJECT_ATTRIBUTES_REVISION_1;
DeviceObjectAttributes.Header.Size = sizeof(NDIS_DEVICE_OBJECT_ATTRIBUTES);
DeviceObjectAttributes.MajorFunctions = &DispatchTable[0];
DeviceObjectAttributes.ExtensionSize = sizeof(CONTROL_DEVICE_EXTENSION);
DeviceObjectAttributes.DefaultSDDLString = NULL;
DeviceObjectAttributes.DeviceClassGuid = 0;
有两个属性需要特别留意,DeviceName
和SymbolicName
。因为无法使用 GUID 在应用程序和驱动程序间作为沟通的凭据,此处的办法是创建命名设备。
RtlUnicodeStringPrintf(&DeviceName, L"%s%d", L"\\Device\\xxxxxx", i);
RtlUnicodeStringPrintf(&DeviceLinkUnicodeString, L"%s%d", L"\\DosDevices\\xxxxxx", i++);
DeviceObjectAttributes.DeviceName = &DeviceName;
DeviceObjectAttributes.SymbolicName = &DeviceLinkUnicodeString;
细心的同学可能会发现了,我们把我们想要的设备名和数字i拼接在一起构成了设备名,这样做的原因 usbnwifi 给出的解释是:
Repeatedly try to create a named device object until we run out of buffer space or we succeed.
个人认为主要是为了提高可靠性。
第三个参数是设备对象,PDEVICE_OBJECT
类型。
第四个参数用于保存 NDIS 句柄,用于销毁。
销毁时,调用NdisDeregisterDeviceEx
函数进行释放。
至于创建和销毁的时机,应分别在MPInitialize
和MPHalt
中。
传入分发函数的是一个 IRP 对象 Irp。
熟悉 WDM 或 KMDF 下 IOCTL 编程的同学肯定知道,IOCTL 关键的参数无非是缓冲区、长度、状态和返回信息。在WDM/NDIS 下的实现还需要另加一个类型(注:原为 MajorFunction,不知怎么翻译合适)。那我们依次来获取这些参数。
第一步,直接通过 IRP 对象可以获取缓冲区、状态和返回信息。
pBuffer = (PULONG) Irp->AssociatedIrp.SystemBuffer; // 缓冲区
Irp->IoStatus.Information // 返回信息,即应用程序中的BytesReturn
Irp->IoStatus.Status // 返回状态
此处特别留意这里的缓冲区。熟悉 KMDF 的同学可能不太理解,不应该是输入缓冲区和输出缓冲区吗,这里为什么只有一个缓冲区。我阅读到一些文档,似乎可以这样理解:
在 WDM/NDIS 的 IOCTL 实现中,驱动视角的输入和输出共用了同一缓冲区
但在 OS 内部的 IO 子系统中,这一缓冲区分别对应于应用程序的输入缓冲区和输出缓冲区。发出 IOCTL 时,OS 将输入缓冲区的数据拷贝到驱动缓冲区;IOCTL 返回时,OS 再将驱动缓冲区的数据拷贝到输出缓冲区。由此达到公用缓冲区的效果。
第二步,获取一个 IRP 栈(注:原为 PIO_STACK_LOCATION):
PIO_STACK_LOCATION irpStack;
irpStack = IoGetCurrentIrpStackLocation(Irp);
通过 irpStack 我们可以获得很多属性,如:
irpStack->MajorFunction // 上文所说的IRP类型,有IRP_MJ_CREATE、IRP_MJ_CLEANUP、IRP_MJ_CLOSE、IRP_MJ_DEVICE_CONTROL等。
irpStack->Parameters.DeviceIoControl.InputBufferLength // 输入缓冲区长度
irpStack->Parameters.DeviceIoControl.OutputBufferLength // 输出缓冲区长度
irpStack->Parameters.DeviceIoControl.IoControlCode // IOCTL_Code
有了这两步获得的参数,我们便可根据驱动的需要进行 IOCTL 的逻辑实现。最后,返回 Irp。
IoCompleteRequest(Irp, IO_NO_INCREMENT);
之前在编写 PCIe 接口卡的驱动程序时,应用程序通过 CreateFile 创建与驱动程序之间的 IOCTL 通道,利用的是 GUID。NDIS 中没有利用 GUID 创建这一通道,那该如何进行呢?
上面已经提到了,我们创建了一个命名设备,设备名为\\Device\\xxxxxx
。特别留意,参考 usbnwifi 的实现,驱动在创建设备对象时,将命名设置为“设备名+数字”。所以当我们利用 CreateFile 打开设备时,实际传入的设备名应该是\\Device\\xxxxxx0
。
CreateFile(L"\\\\.\\xxxxxx0", // L是为了保证为Unicode编码
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
0,
NULL);
之后的DeviceIoControl
函数与普通的 KMDF 实现无异。
Reference:
欢迎来我的 Git Pages 博客交流,本文最早发布于 http://hoxz.me/2018/02/02/use-ioctl-in-ndis