Windows驅動開發WDM (11)- 多個裝置物件(同一個驅動)
通常在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可以遍歷整個裝置鏈。
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指向附在當前裝置物件上面的裝置物件。