通常在WDM驱动的AddDevice里面只会调用一次IoCreateDevice创建一个设备对象。其实我们也可以调用多次IoCreateDevice来创建多个设备对象。当驱动调用IoCreateDevice成功后,驱动对象DriverObject的DeviceObject指针会指向新创建的设备对象,这个设备对象的NextDevice=NULL。如果再调用一次IoCreateDevice,那么DriverObject::DeviceObject指向新创建的设备对象,然后新创建的设备对象的NextDevice指向前面创建的设备对象。总体来讲,DriverObject::DeviceObject永远指向IoCreateDevice新创建的设备对象,而新创建的设备对象(DeviceObject::NextDevice)会指向前面一次创建的设备对象(如果没有,那么就是NULL)。
写个例子试试看,首先增加一个创建设备对象的函数:
NTSTATUS CreateFDO(IN PDRIVER_OBJECT DriverObject, IN PDEVICE_OBJECT pdo, IN const wchar_t* pszDeviceName, IN const wchar_t* pszSymbolicLink) { NTSTATUS status; PDEVICE_OBJECT fdo; UNICODE_STRING devName; RtlInitUnicodeString(&devName,pszDeviceName);//设备名称,设备名称只能被内核模式下的其他驱动所识别。 //创建FDO(Function Device Object) status = IoCreateDevice( DriverObject, sizeof(DEVICE_EXTENSION), &(UNICODE_STRING)devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &fdo); if( !NT_SUCCESS(status)) return status; KdPrint(("CreateFDO\n")); PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension; pdx->fdo = fdo; //将FDO附加在PDO上面,并且将Extension中的NextStackDevice指向FDO的下层设备。如果PDO上面有过滤驱动的话,NextStackDevice就是过滤驱动,如果没有就是PDO。 pdx->NextStackDevice = IoAttachDeviceToDeviceStack(fdo, pdo); //创建一个缓冲,用来模拟文件。 pdx->buffer = (char*)ExAllocatePool(NonPagedPool, MAX_FILE_LEN); pdx->filelen = 0; //创建一个互斥对象 KeInitializeMutex(&pdx->myMutex, 0); UNICODE_STRING symLinkName; //创建链接符号,这样用户模式的应用就可以访问此设备。内核模式下,符号链接是以\??\开头的(或者\DosDevices\)。用户模式下则是\\.\开头。 //这里就可以在用户模式下用\\.\HelloWDM来访问本设备。 RtlInitUnicodeString(&symLinkName,pszSymbolicLink); pdx->ustrDeviceName = devName; pdx->ustrSymLinkName = symLinkName; status = IoCreateSymbolicLink(&(UNICODE_STRING)symLinkName,&(UNICODE_STRING)devName); if( !NT_SUCCESS(status)) { IoDeleteSymbolicLink(&pdx->ustrSymLinkName); status = IoCreateSymbolicLink(&symLinkName,&devName); if( !NT_SUCCESS(status)) { return status; } } fdo->Flags |= DO_BUFFERED_IO | DO_POWER_PAGABLE;//DO_BUFFERED_IO,定义为“缓冲内存设备” fdo->Flags &= ~DO_DEVICE_INITIALIZING;//将Flag上的DO_DEVICE_INITIALIZING位清零,保证设备初始化完毕,必须的。 return STATUS_SUCCESS; }
这个函数会创建一个新的设备对象。看看AddDevice函数:
NTSTATUS HelloWDMAddDevice(IN PDRIVER_OBJECT DriverObject, IN PDEVICE_OBJECT PhysicalDeviceObject) //DriverObject就是指向本驱动程序的一个对象,是由PNP管理器传递进来的。 //PhysicalDeviceObject是PNP管理器传递进来的底层驱动设备对象,这个东西在NT驱动中是没有的。通常称之为PDO,确切的说是由总线驱动创建的。 { PAGED_CODE(); KdPrint(("Enter HelloWDMAddDevice\n")); NTSTATUS status = CreateFDO(DriverObject, PhysicalDeviceObject, L"\\Device\\MyWDMDevice", L"\\DosDevices\\HelloWDM"); if (status == STATUS_SUCCESS) { status = CreateFDO(DriverObject, PhysicalDeviceObject, L"\\Device\\MyWDMDevice2", L"\\DosDevices\\HelloWDM2"); if (status == STATUS_SUCCESS) { status = CreateFDO(DriverObject, PhysicalDeviceObject, L"\\Device\\MyWDMDevice3", L"\\DosDevices\\HelloWDM3"); } } KdPrint(("Leave HelloWDMAddDevice\n")); return status; }
很简单,就是创建3个不同名字的设备对象。然后在DeviceIoControl派遣函数那里打印一下,比如:
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension; KdPrint(("start to call IoStartPacket, IRP: 0x%x, fdo: %x, NextStackDevice: %x, NextDevice: %x\n", Irp, fdo, pdx->NextStackDevice, fdo->NextDevice)); PDEVICE_OBJECT pFdo = fdo->DriverObject->DeviceObject; do { pdx = (PDEVICE_EXTENSION)pFdo->DeviceExtension; KdPrint(("Device: %x, Device name: %ws\n", pFdo, pdx->ustrDeviceName.Buffer)); pFdo = pFdo->NextDevice; } while (pFdo != NULL);
首先打印DeviceIoControl派遣函数传进来的设备对象,然后根据DriverObject::DevicObject和NextDevice打印整个设备链。看看测试代码:
// TestWDMDriver.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <windows.h> #include <process.h> #define DEVICE_NAME L"\\\\.\\HelloWDM" #define DEVICE_NAME2 L"\\\\.\\HelloWDM2" void Test(void* pParam) { int index = (int)pParam; //设置overlapped标志,表示异步打开 HANDLE hDevice; if (index == 1) { hDevice = CreateFile(DEVICE_NAME2,GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,NULL,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,NULL); wprintf(L"CreateFile, name: %s, ret: %x\n", DEVICE_NAME2, hDevice); } else { hDevice = CreateFile(DEVICE_NAME,GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,NULL,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,NULL); wprintf(L"CreateFile, name: %s, ret: %x\n", DEVICE_NAME, hDevice); } if (hDevice != INVALID_HANDLE_VALUE) { char inbuf[100] = {0}; sprintf(inbuf, "hello world %d", index); char outbuf[100] = {0}; DWORD dwBytes = 0; DWORD dwStart = GetTickCount(); printf("input buffer: %s\n", inbuf); OVERLAPPED ol = {0}; ol.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); BOOL b = DeviceIoControl(hDevice, CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_IN_DIRECT, FILE_ANY_ACCESS), inbuf, strlen(inbuf), outbuf, 100, &dwBytes, &ol); printf("DeviceIoControl thread %d, returns %d, last error: %d, used: %d ms, input: %s\n", index, b, GetLastError(), GetTickCount() - dwStart, inbuf); WaitForSingleObject(ol.hEvent, INFINITE); DWORD dwEnd = GetTickCount(); //将输出buffer的数据和'm'亦或,看看是否能够得到初始的字符串。 for (int i = 0; i < strlen(inbuf); i++) { outbuf[i] = outbuf[i] ^ 'm'; } printf("Verify thread %d, outbuf: %s, used: %d ms\n", index, outbuf, dwEnd - dwStart); CloseHandle(hDevice); } else printf("CreateFile failed, err: %x\n", GetLastError()); } int _tmain(int argc, _TCHAR* argv[]) { HANDLE handles[2]; for (int i = 0; i < 2; i++) { handles[i] = (HANDLE)_beginthread(Test, 0, (void*)i); Sleep(100); } WaitForMultipleObjects(2, handles, TRUE, INFINITE); printf("Test ends\n"); return 0; }
就是创建2个线程,然后打开2个设备。看看结果:
测试代码打开了设备1和设备2,然后从log里面可以看到DeviceIoControl派遣函数传进来的设备对象是设备3,挺奇怪的,不知道为什么,有待研究。
然后从DriverObject::DeviceObject可以打印整个设备链,从log里面可以看到3个设备。画图描述一下设备链:
小结:一个驱动可以创建多个设备对象(有没有必要那是另外一码事),然后通过DriverObject::DeviceObject和DeviceObject::NextDevice可以遍历整个设备链。
代码:http://download.csdn.net/detail/zj510/4887669
DDK编译驱动,VS2008编译测试代码。
提高:
又做了个测试,打印IoAttachDeviceToDeviceStack的返回值。
pdx->NextStackDevice = IoAttachDeviceToDeviceStack(fdo, pdo);
每次attach设备的时候,都是attach到pdo上,那么三次attach返回值一样吗?打印代码:
PDEVICE_OBJECT pFdo = fdo->DriverObject->DeviceObject; do { pdx = (PDEVICE_EXTENSION)pFdo->DeviceExtension; KdPrint(("Device: %x, Device name: %ws, PDX::NextStackDevice: %x\n", pFdo, pdx->ustrDeviceName.Buffer, pdx->NextStackDevice)); pFdo = pFdo->NextDevice; } while (pFdo != NULL);
就是从设备扩展里面把NextStackDevice打印出来,看看结果:
从不同的颜色下划线可以看到,设备3的NextStackDevice是设备2,设备2的NextStackDevice是设备1, 那么设备1的NextStackDevice是什么呢?应该是PDO吧。
这个例子里面设备对象的创建顺序是设备1,设备2,设备3.当创建设备1的时候,PDO上面没有任何东西,那么设备1的NextStackDevice就指向PDO,当创建设备2的时候,pdo上面已经附加了设备1,那么IoAttachDeviceToDeviceStack就将设备2附加到了设备1上,设备3就附加到了设备2上面。我觉得这个地方应该是个垂直结构,这个设备栈的结构是:设备3-》设备2-》设备1-》PDO,其中设备3是栈顶。那么现在也可以解释上面的一个问题了(黑体字),为什么测试代码打开了设备1和设备2,结果派遣函数里面的设备对象居然是设备3.原因就是I/O管理器会把IRP请求送给栈顶的设备,也就是设备3,然后各个设备对象可以依次往下传递。
这个例子只是为了测试IoCreateDevice和IoAttachDeviceToDeviceStack,真正的工作中,应该很少会在AddDevice里面创建多个设备对象的吧。
附:
用DriverTree来验证一下,截了几个图以供参考:
从上面的图,可以清楚地看到PnpManager创建了一个虚拟的PDO(\Device\0000003c),因为这个驱动是个虚拟的设备,那么虚拟总线驱动就会创建一个虚拟的PDO。然后PDO的Attached Device会指向第一个附在它上面的设备对象,这里是MyWDEDevice,因为这个设备是第一个被驱动程序创建的。
看看MyWDMDevice的截图:
NextDevice指向0,因为这是驱动创建的第一个设备。然后AttachedDevice是0x82256030,其实他指向MyWDMDevice2,这是驱动创建的第二个设备。看MyWDMDevice2的截图:
看MyWDMDevice2的NextDevice指向MyWDEDevice,AttachedDevice指向MyWDMDevice3.看看MyWDMDevice3,正确的情况应该是NextDevice指向MyWDMDevice2(0x82256030),然后AttachedDevice指向0.看结果:
完全正确。再复习一下:
1. DeviceObject::NextDevie指向同一个驱动的前一次IoCreateDevice创建的设备对象。
2. DriverObject::DeviveObject指向最后一次IoCreateDevice创建的设备对象。通过DriverObject::DeviceObject和DeviceObject::NextDevice可以找到同一个驱动创建的对象(一个对象链)
3. DeviceObject::AttachedObject指向附在当前设备对象上面的设备对象。