算起來,決定學習WDM也有一年時間了。不過,由於研一緊張的課程,以及研一下期教研室橫向的壓力,我也很少有時間能靜下心來看看書看看代碼。可以說是連三天打魚兩天晒網都不如。過了大半年,Walter Oney的《Windows Driver Model》好不容易看了5章,後面的章節零零星星看了一點,就開始迫不及待的投入實踐了。實際上再不投入實踐我都不知道要學到什麼時候去了,因為以後只會越來越忙。於是,我寫了我的第一個驅動程式,非常簡單,就相當於我們剛學C語言時學的那個“Hello World”程式。程式雖然簡單,但是通過這個過程,我對前面看書所學到的東西有了一個更深入更直觀的理解,很多以前模糊的概念通過這次實踐現在變得清晰了。此外,通過寫這個程式,我總算是熟悉了WDM程式的總體構架以及編程過程,因此,這個程式也算是一個敲門磚吧。好了,廢話就這麼多,下面進入正題。 WDM,即Windows Driver Model,是Windows環境下開發驅動程式的有力工具。初學WMD的時候覺得這玩意很有些難,現在雖然不能說完全學會了,但總算是入了那個門檻了吧,回想起來,覺得難的原因主要是對windows系統瞭解還是太少。所以,學習WDM首先就要對系統結構有一個比較深入的瞭解,起碼要瞭解系統是怎樣從使用者應用程式一步步通過驅動程式調用到硬體裝置的。下面,給出一個WDM一書中描述windows 2000系統結構的一個:
在這個圖中,“裝置驅動程式”那一塊,就是我們的驅動程式所處的位置。只瞭解了這一個結構還不夠,我們還必須瞭解驅動程式之間的結構,瞭解它們之間是怎麼互動的。驅動程式是一個分層的結構,一個硬體裝置並不是只由一個驅動程式來管理,在它相關聯的物理裝置驅動程式之上,還有很多過濾驅動程式。與這些過濾驅動程式相關聯的,就是這個物理裝置對象的過濾器裝置對象。那麼,一個使用者模式的請求,必須通過上層的過濾器裝置對象,一層一層的往下傳,最終才能到達物理裝置對象。這有點像TCP/IP分層結構模型,一個應用程式層的資料包必須通過傳輸層、網路層這樣一層一層的往下傳,最終才能達到物理層並傳遞到網路中。而設計這樣的分層模型的目的,我想應該是為了方便擴充,比如如果想對某個裝置加入新的管理操作,那麼不需要修改其已有的物理裝置驅動程式和過濾器驅動程式,而只需要加入新的過濾器裝置對象以及相應的驅動程式,在這裡加入新的操作就行了。下面,還是用一個圖來表示這種分層的結構模型:這個圖左邊的PDO、FDO、FiDO等,就是指裝置對象。而IRP——IO請求包,就是上一層裝置對象向下一層裝置對象發送的請求,也就是它們之間互動的資訊。另外,需要指出的一點是,在很多核心模式編程中,驅動程式並不一定要與某一個實際存在的物理裝置相關聯,它可以僅建立一個虛擬裝置對象,而這個裝置對象不與任何實際的物理裝置相關聯。因為在很多情況下,使用者編寫驅動的目的僅僅是要讓自己的代碼執行在系統的核心態中。 有了前面的這些必備知識,下面就要看看到底應該怎樣編寫WDM驅動程式了。同我們學習c語言的時候一樣,首先要從程式進入點開始。在驅動程式中,這個進入點就是DriverEntry函數(相當於c語言的main函數),它在驅動程式被載入進記憶體的時候調用。DriverEntry函數有兩個參數,其中第一個參數PDRIVER_OBJECT pDriverObj是指向該驅動程式對應的驅動程式對象的指標。在DriverEntry函數中,一個重要的任務就是要設定驅動程式對象的幾個函數指標,這樣,該驅動程式對象關聯的裝置對象在接收到上層的IRP的時候,就會通過驅動程式對象中設定的函數指標,找到相應的函數來做處理: pDriverObj->DriverUnload = DriverUnload; pDriverObj->MajorFunction[IRP_MJ_CREATE] = pDriverObj->MajorFunction[IRP_MJ_CLOSE] = pDriverObj->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverDispatch; 除此之外,DriverEntry函數還有一個重要的任務,就是要建立裝置對象並為其建立符號串連。(這裡說明一下,在規範的WDM程式中,建立裝置對象的任務本來該由AddDevice函數來做,而這個函數也是通過驅動程式對象的一個函數指標來定位的。在這種規範的WDM程式中,一旦有新硬體加入,系統就會自動通過驅動程式對象的函數指標找到AddDevice函數,並調用它來建立裝置對象。但是在這裡,我並不是在為實際存在的硬體寫驅動,而只是寫一個核心模式下的程式,因此就只需要在DriverEntry函數中建立一個裝置對象就行了。)IoCreateDevice( pDriverObj,0,&deviceName,FILE_DEVICE_UNKNOWN,FILE_DEVICE_SECURE_OPEN,true,&pDeviceObj ); //建立裝置對象IoCreateSymbolicLink( &linkName, &deviceName ); //建立符號串連 從上面調用IoCreateDevice函數的參數中還可以看出,裝置對象和驅動程式對象是相關聯的,這也就可以解釋為什麼是裝置接收到IRP,而相應的處理函數卻是由驅動程式對象中的函數指標定位的。至於建立符號串連,那是為了方便在使用者模式中調用裝置對象。從前面設定的驅動程式對象中的函數指標可以看到,主要有兩個函數:卸載函數DriverUnload和派遣函數DriverDispatch。DriverUnload函數應該很容易理解,它是在驅動程式被卸載出記憶體的時候調用,主要做一些釋放記憶體之類的工作。而在我的這個程式中,所有的IRP都是在同一個函數裡面進行處理的,這就是派遣函數DriverDispatch(實際上很多WDM程式都是這樣做的)。下面就分別介紹一下這兩個函數。DriverUnload函數的主要任務是將建立的裝置對象和符號串連刪除掉,當然如果在程式中還分配了其他記憶體需要釋放,也是在這裡完成。IoDeleteSymbolicLink( &linkName );IoDeleteDevice( pDriverObj->DeviceObject ); 派遣函數DriverDispatch主要負責處理上層的IRP。這裡先要提一下,每個IRP都與兩個資料結構相關聯,就是IRP本身和IRP Stack——IO_STACK_LOCATION結構。在這兩個結構裡面,包含了所有上層傳遞給本層裝置對象的資訊。最重要的一個資訊就是:在IO_STACK_LOCATION結構中,包含了IRP的功能碼MajorFunction和MinorFunction(IRP的功能碼標識了該IRP具體是什麼請求,比如讀請求的MajorFunction值為IRP_MJ_READ)。DriverDispatch函數的處理流程一般是這樣的:首先通過IRP獲得IPR Stack;然後從IRP Stack中得到該IRP的主功能碼MajorFunction,判斷主功能碼並做相應處理;處理完該請求後,根據具體情況選擇完成該請求或者向下一層裝置對象傳遞該IRP。獲得IRP Stack很簡單,只需要調用函數IoGetCurrentIrpStackLocation即可:PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation( pIrp ); 判斷主功能碼並做相應的處理這一步一般是由一個switch-case語句實現的: switch( pIrpStack->MajorFunction ) { case IRP_MJ_CREATE: DbgPrint( "Info: Create!/n" ); break; case IRP_MJ_CLOSE: DbgPrint( "Info: Close!/n" ); break; case IRP_MJ_DEVICE_CONTROL: { switch( pIrpStack->Parameters.DeviceIoControl.IoControlCode ) { case IOCTL_GET_INFO: { RtlCopyMemory( pIrp->UserBuffer, "This is a test driver!", 23 ); information = 23; break; } default: break; } } default: break; } 最後一步,如果需要完成該請求,那麼應該先設定IRP結構中的IoStatus域,然後調用函數IoCompleteRequest: pIrp->IoStatus.Status = STATUS_SUCCESS; pIrp->IoStatus.Information = information; IoCompleteRequest(pIrp, IO_NO_INCREMENT); 如果需要向下一層裝置對象傳遞該IRP,則要先初始化往下傳遞的IRP對應IRP Stack(可以直接將當前的IRP Stack複製給下層IRP Stack),然後調用IoCallDriver函數往下層傳遞該IRP:IoCopyCurrentIrpStackLocationToNext(pIrp);status = IoCallDriver(pLowerDeviceObj, pIrp); 以上就是對我寫的這個驅動實驗程式的簡單分析,也是我對WDM驅動程式架構結構的一個初步理解。由於我只是初學WDM,因此一定有很多理解錯誤或者遺漏的地方。下面,我再簡單介紹一下怎樣從使用者模式的程式中調用驅動。使用者模式的程式要調用驅動,首先就要開啟裝置,也就是驅動程式中建立的裝置對象。這可以通過調用CreateFile函數來實現。CreateFile函數本來是用於開啟檔案,它的第一個參數就是檔案名稱。而這裡,我們以裝置名稱作為它的第一個參數傳入,那麼該函數開啟的就是裝置了。這裡所說的裝置名稱,實際上是驅動程式裡面為裝置對象建立的符號串連名。比如使用者模式中給出的裝置名稱為” //./MyDevice”,I/O管理器在執行名稱搜尋前先自動把”//./”轉換成”/??/”,這樣就成了” /??/MyDevice”,這就是驅動程式裡面建立的符號串連名了。開啟裝置後,使用者模式的程式就可以調用ReadFile、WriteFile和DeviceIoControl等函數向驅動程式發出請求了。最後,給出我的實驗程式的源碼。 //Driver部分: #ifdef __cplusplusextern "C" {#endif #include "ntddk.h" #define DEVICE_NAME L"//Device//MyDevice"#define LINK_NAME L"//??//MyDevice"#define IOCTL_GET_INFO / CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS) void DriverUnload( PDRIVER_OBJECT pDriverObj );NTSTATUS DriverDispatch( PDEVICE_OBJECT pDeviceObj, PIRP pIrp ); // 驅動程式載入時調用DriverEntry常式:NTSTATUS DriverEntry( PDRIVER_OBJECT pDriverObj, PUNICODE_STRING pRegistryString ){ DbgPrint( "DriverEntry!/n" ); NTSTATUS status; PDEVICE_OBJECT pDeviceObj; UNICODE_STRING deviceName; RtlInitUnicodeString( &deviceName, DEVICE_NAME ); status = IoCreateDevice( pDriverObj, 0, &deviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, true, &pDeviceObj ); if( !NT_SUCCESS( status ) ) { DbgPrint( "Error: Create device failed!/n" ); return status; } UNICODE_STRING linkName; RtlInitUnicodeString( &linkName, LINK_NAME ); status = IoCreateSymbolicLink( &linkName, &deviceName ); if( !NT_SUCCESS( status ) ) { DbgPrint( "Error: Create symbolic link failed!/n" ); IoDeleteDevice( pDeviceObj ); return status; } pDriverObj->DriverUnload = DriverUnload; pDriverObj->MajorFunction[IRP_MJ_CREATE] = pDriverObj->MajorFunction[IRP_MJ_CLOSE] = pDriverObj->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverDispatch; return STATUS_SUCCESS;} void DriverUnload( PDRIVER_OBJECT pDriverObj ){ DbgPrint( "DriverUnload!/n" ); if( pDriverObj->DeviceObject != NULL ) { UNICODE_STRING linkName; RtlInitUnicodeString( &linkName, LINK_NAME ); IoDeleteSymbolicLink( &linkName ); IoDeleteDevice( pDriverObj->DeviceObject ); } return;} NTSTATUS DriverDispatch( PDEVICE_OBJECT pDeviceObj, PIRP pIrp ){ ULONG information = 0; PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation( pIrp ); switch( pIrpStack->MajorFunction ) { case IRP_MJ_CREATE: DbgPrint( "Info: Create!/n" ); break; case IRP_MJ_CLOSE: DbgPrint( "Info: Close!/n" ); break; case IRP_MJ_DEVICE_CONTROL: { switch( pIrpStack->Parameters.DeviceIoControl.IoControlCode ) { case IOCTL_GET_INFO: { RtlCopyMemory( pIrp->UserBuffer, "This is a test driver!", 23 ); information = 23; break; } default: break; } } default: break; } pIrp->IoStatus.Status = STATUS_SUCCESS; pIrp->IoStatus.Information = information; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return STATUS_SUCCESS;} #ifdef __cplusplus}#endif //使用者模式程式部分: #include "stdafx.h"#include "stdio.h"#include "windows.h" #define DEVICE_NAME "////.//MyDevice"#define CTL_CODE( DeviceType, Function, Method, Access ) ( / ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) /)#define FILE_DEVICE_UNKNOWN 0x00000022#define METHOD_NEITHER 3#define FILE_ANY_ACCESS 0#define IOCTL_GET_INFO / CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS) int main(int argc, char* argv[]){ HANDLE hDevice = CreateFile( DEVICE_NAME, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); if( hDevice == INVALID_HANDLE_VALUE ) { printf( "Error: Can't open the device!/n" ); return -1; } unsigned long numOfBytesReturned; char info[32] = {0}; if( DeviceIoControl( hDevice, IOCTL_GET_INFO, NULL, 0, info, 32, &numOfBytesReturned, NULL ) == true ) printf( "Information: %s /n", info ); CloseHandle( hDevice ); Sleep( 3000 ); return 0;}