程式扎記: [ Windows DDP ] 我的第一個 Windows 驅動程式 - HelloDDK (含開發環境設置)
[ Windows DDP ] 我的第一個 Windows 驅動程式 - HelloDDK (含開發環境設置)
前言 : Windows 驅動程式的編寫, 往往需要開發人員對 Windows 內核有深入了解與大量的內核除錯技巧. 稍有不慎, 就會造成系統的崩潰. 因此初次涉及 Windows 驅動程式開發的程式師即使有大量的 Win32 程式開發技巧, 往往入門也需要一段時間. 這裡透過兩個簡單的 Windows 驅動程式, 一個是 NT 式的驅動程式, 另一個則是 WDM 式的驅動程式來作基礎的介紹與開發環境設置. 這兩個驅動程式沒有操作具體的硬體裝置, 只是在系統裡新建的虛擬裝置. 在隨後章節, 它們會作為基本驅動程式框架而重複使用. DDK 的安裝 : 在編寫第一個驅動之前, 需要先安裝微軟公司提供的 Windows 驅動程式開發套件 DDK (Driver Development Kit). 在安裝過程請選擇完整安裝, 即安裝 DDK 的所有元件. 安裝完畢後, 會在開始功能表中出現對應的專案. 其中主要用到的是 Build Environment. 如下圖所示 : 第一個驅動程式 HelloDDK 的程式碼分析 : Windows 驅動程式分為兩類, 一類是不支援隨插即用功能的 NT 式驅動程式. 另一類是支援隨插即用功能的 WDM 驅動程式. 接下來要介紹的 HelloDDK 是一個最簡單的 NT 式驅動程式. 首先來看看 HelloDDK 標頭檔. - HelloDDK 的標頭檔 - Driver.h :
view plaincopy to clipboardprint?/************************************************************************ * 檔案名稱:Driver.h * 作 者: * 完成日期:2007-11-1 *************************************************************************/ #pragma once #ifdef __cplusplus extern "C" { #endif #include #ifdef __cplusplus } #endif #define PAGEDCODE code_seg("PAGE") #define LOCKEDCODE code_seg() #define INITCODE code_seg("INIT") #define PAGEDDATA data_seg("PAGE") #define LOCKEDDATA data_seg() #define INITDATA data_seg("INIT") #define arraysize(p) (sizeof(p)/sizeof((p)[0])) typedef struct _DEVICE_EXTENSION { PDEVICE_OBJECT pDevice; UNICODE_STRING ustrDeviceName; //裝置名稱 UNICODE_STRING ustrSymLinkName; //符號連結名 } DEVICE_EXTENSION, *PDEVICE_EXTENSION; // 函式宣告 NTSTATUS CreateDevice (IN PDRIVER_OBJECT pDriverObject); VOID HelloDDKUnload (IN PDRIVER_OBJECT pDriverObject); NTSTATUS HelloDDKDispatchRoutine(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp); HelloDDK 的標頭檔主要是為了匯入驅動程式開發所必需的 NTDDK.h 標頭檔. 此標頭檔包含了對 DDK 的所有會出函式的宣告. NT 式的驅動程式要匯入的標頭檔是 NTDDK.h. 而 WDM 式要匯入的標頭檔為 WDM.h. 另外此標頭檔定義了幾個標籤, 分別在程式中指明函式和變數分配在分頁記憶體中或非分頁式記憶體中. (分頁/非分頁式記憶體概念之後會介紹) 最後, 該標頭檔列出了此驅動的函式宣告. 程式碼 6-15 行, 包含了 ntddk.h 標頭檔. 所有 NT 式驅動程式都要包含此標頭檔. 因為這裡採用的是 C++ 語言編寫, 如果直接包含 ntddk.h , 函式的符號表會匯入錯誤. 所以需要加入 extern "C", 這樣可以保證符號表可以正確匯入. 程式碼 17-23行 定義分頁標記, 非分頁標記和初始化區塊. 在 Windows 驅動程式的開發中. 所有程式的函式和變數要被指名被載入到分頁記憶體中還是在非分頁記憶體中. 程式碼中加入這裡的巨集, 就會被指名函式和變數是位於分頁或是非分頁記憶體中. 另外有一個特殊函式 DriverEntry 需要放在 INIT 旗標的記憶體中. INIT 旗標指明該函式只是在載入的時候需要載入記憶體, 而當驅動程式成功載入後, 該函式就可以從記憶體中卸載掉. 程式碼 27-31行 指定一個裝置擴充結構體, 這種結構體廣泛應用於驅動程式中. 根據不同的驅動程式需求, 它負責補充定義裝置的相關資訊. - HelloDDK 的入口函式 和普通應用程式不同的是 Windows 驅動程式的入口函式不是 main 函式, 而是一個叫做 DriverEntry 的函式. 程式碼在下面列出. DriverEntry 函式由內核中的 I/O 管理器負責呼叫, 其函式有兩個參數 : pDriverObject 和 pRegistryPath. 其中 pDriverObject 是 I/O 管理器傳遞進來的驅動物件, pRegistryPath 是一個 Unicode 字串, 指向驅動程式負責的登錄表. - Driver.cpp (EntryDriver 函式) :
view plaincopy to clipboardprint?/************************************************************************ * 檔案名稱:Driver.cpp * 作 者: * 完成日期:2007-11-1 *************************************************************************/ #include "Driver.h" /************************************************************************ * 函式名稱:DriverEntry * 功能描述:初始化驅動程式，定位和申請硬體資源，創建內核物件 * 參數列表: pDriverObject:從I/O管理器中傳進來的驅動物件 pRegistryPath:驅動程式在登錄表的中的路徑 * 返回 值:返回初始化驅動狀態 *************************************************************************/ #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; } 程式碼 17 行, 用 #progma 指明此函式是載入到 INIT 記憶體區域中, 即成功卸載後可以退出記憶體. 程式碼 18 行, 旗標 DriverEntry 函式的開始. 注意此處在函式體的前面用 extern "C" 修飾, 這樣在編譯的時候會編譯成 _DriverEntry@8 的符號. 如果不加入此修飾符, 編譯器會按照 C++ 的符號名編譯, 導致錯誤連結. 程式碼 23 行, 列印一行除錯資訊. KdPrint 其實是一個巨集, 在除錯版本 (Checked 版) 中, 會用 DbgPrint 代替. 而在發行版本 (Free 版) 中, 則不執行任何操作, 其功能類似於 MFC 中的 TRACE 巨集. 由於驅動程式是執行在 Windows 的核心態, 沒有使用者介面, 所以查看除錯資訊有別於 Win32 程式. 程式碼 26-30 行, 驅動程式向 Windows 的 I/O 管理器登錄一些 Callback 函式. Callback 函式是由程式員定義的函式, 這些函式不是由驅動程式本身負責呼叫, 而是作業系統負責呼叫. 程式員將這些函式的入口位址告訴作業系統, 作業系統會在適當時後呼叫這些函式. 在這個例子中, 這幾個Callback 函式基本上是自解譯型的. 我們可以依據函式名分析其作用. 當驅動程式被卸載時, 呼叫 HelloDDKUnload. 當驅動程式處理新建, 關閉與獨寫時相關 IRP 時, 呼叫 HelloDDKDispatchRoutine. 程式碼 33 行, 呼叫 CreateDevice 函式, 此函式稍後解釋. 程式碼 36 行, 返回 CreateDevice 函式的執行結果, 如果執行正確, 驅動將被成功載入. - 新建裝置常式 CreatedDevice 函式是一個輔助函式 (Helper Function), 輔助 DriverEntry 新建一個裝置物件, 其完全可以展開放在 DriverEntry 中, 但是為了程式碼的條理, 故額外定義一個函式處理, 代碼如下. - Driver.cpp (CreateDevice 函式) :
view plaincopy to clipboardprint?/************************************************************************ * 函式名稱:CreateDevice * 功能描述:初始化裝置物件 * 參數列表: pDriverObject:從I/O管理器中傳進來的驅動物件 * 返回 值:返回初始化狀態 *************************************************************************/ #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, 0, 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; } 程式碼 16-18 行, 建構一個 Unicode 字串, 此字串用來儲存此裝置物件的名稱. Unicode 字串大量運用在驅動程式開發中, 有關 Unicode 的講解留至後續章節介紹. 程式碼 21-28 行, 用 IoCreateDevice 函式新建一個裝置物件. 其物件名稱來自於上一步建構的Unicode 字串, 裝置類型為 FILE_DEVICE_UNKNOWN. 且此種裝置為獨佔裝置, 及裝置只能被被一個應用程式所此用. 程式碼 30 行, 表明此種裝置為 BUFFERED_IO 裝置. 裝置對記憶體的操作分為兩種, BUFFERED_IO 和 DO_DIRECT_IO, 此部分講解留至後續章節說明. 程式碼 31-33 行, 填寫裝置的擴充結構體, 在其他驅動程式的函式中, 可以很方便的的到這個結構體, 進而得到該裝置的自訂資訊. 此結構體定義在 Driver.h 中. 程式碼 34-38 行, 新建符號連結. 驅動程式雖然有了裝置名稱, 但是這種裝置名稱只能在內核態可見, 而對於應用程式不可見. 因此驅動程式需要 export 一個符號鏈結, 該鏈結指向真正的裝置名稱. 程式碼 39-44 行, 當裝置新建成功後返回. 如果不成功則刪除該裝置. - 卸載驅動程式 卸載驅動常式用來卸載裝置, 由 I/O 管理器負責呼叫此 Callback 函式. 此常式遍歷系統中所有此類裝置物件. 第一個裝置物件的位址存在驅動物件的 DeviceObject 欄位中, 每個裝置物件的 NextDevice 欄位記錄著下一個裝置物件的位址, 這樣就形成了一個鏈表. 卸載驅動常式的主要目的就是遍歷系統中所有此類裝置物件, 然後刪除裝置物件以及符號鏈結. - Driver.java (HelloDDKUnload 函式) :
view plaincopy to clipboardprint?/************************************************************************ * 函式名稱:HelloDDKUnload * 功能描述:負責驅動程式的卸載操作 * 參數列表: pDriverObject:驅動物件 * 返回值:返回狀態 *************************************************************************/ #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 ); } } - 預設派遣常式 : 對裝置的新建, 關閉和讀寫操作, 都被指定到這個預設的派遣常式中. 由於這是一個最簡單的演示程式, 故只是簡單的將其成功返回. 後面章節會陸續擴充該常式. - Driver.cpp (HelloDDKDispatchRoutine 函式) :
view plaincopy to clipboardprint?/************************************************************************ * 函式名稱:HelloDDKDispatchRoutine * 功能描述:對讀IRP進行處理 * 參數列表: pDevObj:功能裝置物件 pIrp:從IO請求包 * 返回 值:返回狀態 *************************************************************************/ #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 = 0; // bytes xfered IoCompleteRequest( pIrp, IO_NO_INCREMENT ); KdPrint(("Leave HelloDDKDispatchRoutine\n")); return status; } HelloDDK 的編譯和安裝 : - HelloDDK 編譯 本節會帶領我們一步步地對 HelloDDK 進行編譯與安裝. 編譯和安裝往往是初學者最先需要面對的問題. 這裡將會介紹傳統方法使用 DDK 編譯環境進行編譯. 使用 DDK 環境編譯 HelloDDK 方法需要編寫一個編譯指令檔, 在這個腳本中描述了 DDK 驅動程式的原始檔案, 用到的 lib 檔和 include 路徑名, 編譯輸出的目錄和檔案名等資訊. (更多可以補充說明). 接著請在原始程式的相同目錄下新建兩個檔案 makefile 和 Sources, 這兩個檔案都是文字檔, 內容如下 : - Sources :
view plaincopy to clipboardprint?TARGETNAME=HelloDDK TARGETTYPE=DRIVER TARGETPATH=OBJ INCLUDES=$(BASEDIR)\inc;\ $(BASEDIR)\inc\ddk;\ SOURCES=Driver.cpp\ - makefile :
view plaincopy to clipboardprint?# # DO NOT EDIT THIS FILE!!! Edit .\sources. If you want to add a new source # file to this component. This file merely indirects to the real make file # that is shared by all the driver components of the Windows NT DDK # !INCLUDE $(NTMAKEENV)\makefile.def 第 1 行 說明此驅動的名稱. 第 2 行 指明此驅動的類型為 NT 型驅動程式. 第 3 行 設置編譯的輸出目錄. 第 5-6 行 設置 include 目錄. 第 8 行 指定原始檔案. 編寫完這兩個腳本後, 在 Windows 的開始功能表中選擇 "Windows XP Checked Build Environment" 編譯環境. 這裡選擇的是 Checked 版本而不是 Free 版本. 這裡選擇 Checked 版本而不是 Free 版本. 兩者差別類似 Win32 程式開發的 Debug 版本與 Release 版本. 選擇版本後, 進入的是一個命令列的視窗, 用 cd 進入到需要編譯的目錄, 然後輸入 "build". DDK 的編譯環境會自動呼叫編譯器進行編譯 : 編譯好的結果位於原始程式目錄下的子目錄 objchk_wxp_x86\i386 中. 編譯出來的二進位檔案為 HelloDDK.sys. 它不像 exe 檔那樣執行, 而是必須透過特殊的載入方式. - HelloDDK 安裝 NT 式驅動程式類類似於 Windows 服務程式, 以服務的方式載入系統中. 為了簡化步驟, 這裡利用了一個叫做 DriverMonitor 的工具載入 HelloDDK. DriverMonitor 是 Compuware 公司開發的 DriverStudio 中的一個工具. 如果沒有裝 DriverMonitor, 後續章節會介紹如何編寫一個 NT 式驅動程式的載入器. 執行 DriverMonitor, 選擇 "File" | "Open Driver" , 將會彈出檔案選擇對話框, 選擇編譯好的 HelloDDK.sys 檔案, 再次選擇 "File" | "Start Driver". 至此 NT 驅動載入成功, DriverMonitor 會報告載入狀況, 如下圖所示 : 用 Driver Monitor 載入驅動後, 預設是載入一次, 重啟電腦後該驅動不會被載入. 如果想在每次開機啟動時自動載入, 需要修改設置. 選擇 "Edit" | "Properties" 彈出對話框在 "Start Type" 選擇組中, 選擇 "Automatic" 選項按鈕. 保存後就可以在每次開機啟動時自動載入該驅動. 成功載入的驅動, 會出現在 Windows 的裝置管理員中, 預設情況下, NT 式的驅動是隱藏的, 在裝置管理員中選擇 "檢視" | "顯示隱藏裝置" , 如下圖所示 : 張貼者：