Windows驅動開發(1) - 驅動程式結構
1、NT式
1.1 頭檔案
Driver.h頭檔案中包含了開發NT式驅動所需要的NTDDK.h,此外還定義了幾個标志來指明函數和變量配置設定在分頁記憶體還是非分頁記憶體中。Windows驅動程式的入口函數是DriverEntry函數。WDM式的驅動程式要導入的頭檔案是WDM.h。
說明:
1)采用C++程式設計,是以需要用extern “C”,因為我們導入的是C的函數的符号表。
2)在驅動中用到的變量或函數都需要指定配置設定在分頁或非分頁記憶體中,分頁記憶體在實體記憶體不夠的情況下可能會被交換出去,對于一些需要高IRQL的例程絕對不能被交換出頁面,是以它們必須被定義為非分頁記憶體。
3)DriverEntry需要放在INIT标志的記憶體中。
1.2 驅動程式的入口函數
#pragma INITCODE
extern "C" NTSTATUS DriverEntry (
IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegistryPath )
{
NTSTATUS status;
KdPrint(("Enter DriverEntry\n"));
//注冊其他驅動調用函數入口
pDriverObject->DriverUnload = HelloDDKUnload;
pDriverObject->MajorFunction[IRP_MJ_CREATE] = HelloDDKDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloDDKDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = HelloDDKDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_READ] = HelloDDKDispatchRoutine;
//建立驅動裝置對象
status = CreateDevice(pDriverObject);
KdPrint(("DriverEntry end\n"));
return status;
}
1) 猶如控制台程式需要main、Win32 程式需要WinMain、DLL 程式需要DllMain 一樣,驅動程式也有自己的入口點,即DriverEntry。DriverEntry 需要被加載到INIT 記憶體區域中,這樣當驅動被解除安裝後它可以退出記憶體。
2) DriverEntry 是由核心中的I/O 管理器負責調用的,它有兩個參數DriverObject 和RegistryPath(當然形參的名字可以自己改變)。其中DriverObject 是由I/O管理器傳遞進來的驅動對象,RegistryPath 則指向此驅動負責的系統資料庫。
3)我們可以看到DriverEntry 首先是定義了一些變量,然後調用IoCreateDevice 建立裝置對象,緊接着調用IoCreateSymbolicLink 建立符号連結。HelloDDKDispatchRoutine等是驅動程式在向Windows 的I/O 管理器注冊一些回調函數。上面代碼的含義是:當驅動程式将被解除安裝時自動調用HelloDDKUnload例程;當驅動程式接收到 IRP_MJ_CREATE 時自動調用HelloDDKDispatchRoutine。
4)KdPrint是宏,用來輸出。類似于MFC中的TRACE。
5)#pragma INITCODE來指明此函數加載到INIT記憶體函數中。
6)在驅動對象DriverObject 中,有個函數指針數組MajorFunction,它裡面的每一個元素都記錄着一個函數的位址對應着相應的IRP,我們可以通過簡單地設定這個數組将IRP 與相應的派遣函數關聯起來
1.3 建立裝置例程(函數)
#pragma INITCODE
NTSTATUS CreateDevice (IN PDRIVER_OBJECT pDriverObject)
{
NTSTATUS status;
PDEVICE_OBJECT pDevObj;
PDEVICE_EXTENSION pDevExt;
//建立裝置名稱
UNICODE_STRING devName;
RtlInitUnicodeString(&devName,L"\\Device\\MyDDKDevice");
//建立裝置
status = IoCreateDevice( pDriverObject,
sizeof(DEVICE_EXTENSION),
&(UNICODE_STRING)devName,
FILE_DEVICE_UNKNOWN,
, TRUE,
&pDevObj );
if (!NT_SUCCESS(status))
return status;
pDevObj->Flags |= DO_BUFFERED_IO;
pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
pDevExt->pDevice = pDevObj;
pDevExt->ustrDeviceName = devName;
//建立符号連結
UNICODE_STRING symLinkName;
RtlInitUnicodeString(&symLinkName,L"\\??\\HelloDDK");
pDevExt->ustrSymLinkName = symLinkName;
status = IoCreateSymbolicLink( &symLinkName,&devName );
if (!NT_SUCCESS(status))
{
IoDeleteDevice( pDevObj );
return status;
}
return STATUS_SUCCESS;
}
- RtlInitUnicodeString
- IoCreateDevice
NTSTATUS IoCreateDevice
(
IN PDRIVER_OBJECT DriverObject,
IN ULONG DeviceExtensionSize,
IN PUNICODE_STRING DeviceNameOPTIONAL,
IN DEVICE_TYPE DeviceType,
IN ULONG DeviceCharacteristics,
IN BOOLEAN Exclusive,
OUT PDEVICE_OBJECT *DeviceObject
);
- DriverObject:一個指向調用該函數的驅動程式對象.每一個驅動程式在它的DriverEntry過程裡接收一個指向它的驅動程式對象.
- DeviceExtensionSize:指定驅動程式為裝置擴充對象而定義的結構體的大小.
- DeviceName:(可選的參數)指向一個以零結尾的包含Unicode字元串的緩沖區,那是這個裝置的名稱,該字元串必須是一個完整的裝置路徑名.
- DeviceType:指定一個由一個系統定義的FILE_DEVICE_XXX常量,表明了這個裝置的類型
- DeviceCharacteristics:指定一個或多個系統定義的常量,連接配接在一起,提供有關驅動程式的裝置其他資訊.對于可能的裝置特征資訊,見DEVICE_OBJECT結構體.
- Exclusive:如果指定裝置是獨占的,大部分驅動程式設定這個值為FALSE,如果是獨占的話設定為TRUE,非獨占設定為FALSE.
-
DeviceObject:一個指向DEVICE_OBJECT結構體指針的指針,這是一個指針的指針,指向的指針用來接收DEVICE_OBJECT結構體的指針
1) 前面我們建立的裝置對象雖然有個參數指定了裝置名稱,但是這個裝置名稱隻能在核心态可見,也就說ring3 的應用層程式是看不見它的,是以驅動程式需要向ring3 公布一個符号連結,這個連結指向真正的裝置名稱,而ring3 的應用程式可以通過該符号連結找到驅動程式進行通信。實際上我們經常所說的C 盤、D 盤就是一個符号連結,它們在核心中的真正裝置對象是“\Device\HarddiskVolume1”和“\Device \HarddiskVolume2”。在核心模式下,符号連結是以“\??\”( 或“\DosDevices\”)開頭的,如C 盤就是“\??\C:”,而在使用者模式下,則是以“\.\”開頭的,如C 盤就是“\.\C:”。
1.4 解除安裝驅動例程
#pragma PAGEDCODE
VOID HelloDDKUnload (IN PDRIVER_OBJECT pDriverObject)
{
PDEVICE_OBJECT pNextObj;
KdPrint(("Enter DriverUnload\n"));
pNextObj = pDriverObject->DeviceObject;//由驅動對象得到裝置對象
while (pNextObj != NULL)
{
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
pNextObj->DeviceExtension;
//删除符号連結
UNICODE_STRING pLinkName = pDevExt->ustrSymLinkName;
IoDeleteSymbolicLink(&pLinkName);
//删除裝置對象
pNextObj = pNextObj->NextDevice;
IoDeleteDevice( pDevExt->pDevice );
}
}
解除安裝驅動例程是我們在DriverEntry 中自己定義的,當驅動被解除安裝時I/O管理器負責調用該例程,它主要做一些掃尾處理的工作。
KdPrint:由于驅動程式工作于核心态,不像控制台的程式一樣可以使用printf 輸出一些資訊,也不像Win32 程式可以通過MessageBox 來彈出一個對話框,它要想輸出一些資訊,就需要調用DbgPrint 函數,不過這個函數輸出的資訊我們無法直接看到,需要使用一些專門的工具,比如DbgView (KmdManager)等。
有些内容我們隻想在調試版輸出,在發行版忽略,是以DDK 中定義了一個宏KdPrint,它在發行版不被編譯,隻在調試版才會運作。KdPrint是這樣定義的:
1.5 派遣例程
#pragma PAGEDCODE
NTSTATUS HelloDDKDispatchRoutine(IN PDEVICE_OBJECT pDevObj,IN PIRP pIrp)
{
KdPrint(("Enter HelloDDKDispatchRoutine\n"));
NTSTATUS status = STATUS_SUCCESS;
// 完成IRP
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = ; // bytes xfered
IoCompleteRequest( pIrp, IO_NO_INCREMENT );
KdPrint(("Leave HelloDDKDispatchRoutine\n"));
return status;
}
派遣例程是處理IRP的。
1.6 編譯
- DDK方式
- VC方式:參見《VS2013 + WDK7.6搭建驅動開發環境》
1.7 驅動安裝
用DriverStudio中的工具:DriverMonitor。
2、WDM式驅動
#pragma INITCODE
extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegistryPath)
{
KdPrint(("Enter DriverEntry\n"));
pDriverObject->DriverExtension->AddDevice = HelloWDMAddDevice; //相比NT,此回調函數的作用是建立裝置對象并由PNP管理器調用。
...
}
#pragma PAGEDCODE
NTSTATUS HelloWDMAddDevice(IN PDRIVER_OBJECT DriverObject,
IN PDEVICE_OBJECT PhysicalDeviceObject)
{
PAGED_CODE();
KdPrint(("Enter HelloWDMAddDevice\n"));
...
}
- PAGED_CODE();//宏,隻有check版本中有效,當此例程所在的中斷請求超過APC_LEVEL時,會産生一個斷言。
- 對IRP_MN_REMOVE_DEVICE的處理,類似于NT式驅動中的解除安裝例程,而在WDM式驅動中,解除安裝例程幾乎不做處理。
2.1 編譯:
同NT
2.2 安裝:
用EzDriverInstall安裝或控制面版中新增硬體。
3、小結
實際上,常見的Windows 驅動程式是可以分成兩類的:一類是不支援即插即用功能的NT 式驅動程式,另一類是支援即插即用的WDM 式驅動程式。NT 式驅動的安裝是基于服務的,可以通過修改系統資料庫進行,也可以直接通過服務函數如CreateService 進行安裝;但WDM 式驅動不同,它安裝的時候需要通過編寫一個inf 檔案進行控制。除此之外,它們所使用的頭檔案也不大相同,例如NT 式驅動往往需要導入一個名為“ntddk.h”的頭檔案,而WDM 式驅動需要的卻是“wdm.h”頭檔案。我們在學習的過程中所編寫的大多屬于NT 式驅動,除非我們需要自己的裝置支援即插即用,才需要考慮編寫WDM 式驅動程式。