1. 程式人生 > >Windows驅動開發WDM (11)- 多個裝置物件(同一個驅動)

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指向附在當前裝置物件上面的裝置物件。