程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 文件系統驅動編程基礎篇之2——標准模型

文件系統驅動編程基礎篇之2——標准模型

編輯:關於C++

二、標准模型

WDM采用了結構化的編程方式,執行效率很高,但編寫效率較低,這也是DriverStudio得以發展的重要原因。正如掌握了COM原理,使用ATL才能掌握精髓的道理一樣,讀者需要忍受記憶大量基礎知識的“痛苦”,暫時放棄編寫驅動程序的捷徑。

下面的某些圖示稍顯陳舊,不過已經足以說明問題了。

盡管驅動程序分為多個種類(圖1-4),但它們包含的基本內容(圖1-5)是一致的。每個驅動程序都從初始化程序DriverEntry進入,通過某個派遣例程DispatchXXX派發特定命令(我們不妨稱之為IRP),這些IRP有可能在派遣例程裡就得到了解決,也有可能交給驅動程序的其他部分解決。如果驅動程序A本身不能處理這個IRP命令,它就需要將IRP傳遞到更下層的驅動B,由它們來處理,此時驅動程序A可能因為等待IRP完成而處於睡眠狀態,或繼續處理新的IRP,直到下層驅動B通知(或通過某種機制喚醒並通知)A該IRP已經處理完畢了,此時A就將處理的結果(我們稱之為NTSTATUS)返回原來派發這個IRP的發起人。這就是驅動程序處理IRP的一個簡化過程。

我們提到了驅動程序是分層的這個概念,那麼如何理解分層的概念呢?請看圖示:

引用資料1的原話:WDM模型使用了如圖2-1的層次結構。圖中左邊是一個設備對象堆棧。設備對象是系統為幫助軟件管理硬件而創建的數據結構。一個物理硬件可以有多個這樣的數據結構。處於堆棧最底層的設備對象稱為物理設備對象(physical device object),或簡稱為PDO。在設備對象堆棧的中間某處有一個對象稱為功能設備對象(functional device object),或簡稱FDO。在FDO的上面和下面還會有一些過濾器設備對象(filter device object)。位於FDO上面的過濾器設備對象稱為上層過濾器,位於FDO下面(但仍在PDO之上)的過濾器設備對象稱為下層過濾器。

由某個家伙(可能是用戶模式下的應用程序,也可能是系統內核組件)發起的IRP從上層過濾器驅動程序一直順流而下,傳遞到總線驅動程序處理後,再逐級返回上層,最終發起人得到處理的結果。

一般情況下,IRP也許不需要傳遞到總線驅動程序就被處理掉了,但如果大家都不認識這個IRP,他們就只好逐級下傳了,如果此時有個搞破壞的驅動程序混了進來,攔截了這個IRP,輕則丟失用戶信息、重啟、死機,重則造成系統區的數據混亂,你除了重新安裝操作系統再無任何事情可做。由此可見,我們不要求驅動程序“有理想”,但必須“有紀律”,每個驅動程序都必須嚴格按照規范書寫代碼,這要求編程人員具備較高的素質。

下面我們來了解驅動編程裡最基本的標准模型,這個模型不能解決所有的編程需求,根據需要,它將存在各種變化。我們來看看這個驅動編程裡的“基本定式”:

IO管理器,大家應該理解為該IRP的發起人,可能是張三,也可能是李四,而不是某個固定的組件。這個模型表明了單個驅動程序裡各部件的合作與分工,注意它是個循環不斷的過程,它的發起人與最終接受人是相同的,所謂“從哪裡來,就回哪裡去”。如果我們的編程不涉及真正的硬件,StartIo例程、中斷服務例程ISR、DPC例程均可能不存在。各部件的具體功能請參看資料1的第五章。

三、基本例程、常用數據結構與函數

為正確理解各類例程的具體功能,需要弄清涉及的眾多內核函數、數據結構,讀者應以本文和資料1為索引,認真的閱讀Msdn上的相關內容。

我們的代碼將從入口函數DriverEntry處開始執行,一般情況下,不要將它改名,否則需要修改DDK裡的Build腳本。

驅動函數定義一般采用__stdcall約定,這個約定,在vs和bcb裡的實際行為是不同的。如DriverEntry,兩種編譯器編譯後的庫中名字(即外部名字)分別是_DriverEntry@8和DriverEntry。我們還習慣以IN,OUT宏顯式說明函數的參數是輸入或輸出參數。

DriverEntry裡常見的幾個例程由紅字標出,包括添加(硬件、虛擬)設備函數AddDevice、驅動卸載函數DriverUnload、StartIo函數以及放置於MajorFunction數組裡的派遣函數。DriverEntry還申請了分頁池以保存注冊表中的服務鍵,但作為文件系統驅動的DriverEntry,一般還會聲明快速IO派遣函數,這組派遣函數沒有出現在示例中。

extern "C"
NTSTATUS
DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload = DriverUnload;         <--1
DriverObject->DriverExtension->AddDevice = AddDevice;
DriverObject->DriverStartIo = StartIo;
DriverObject->MajorFunction[IRP_MJ_PNP] = DispatchPnp;         <--2
DriverObject->MajorFunction[IRP_MJ_POWER] = DispatchPower;
DriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = DispatchWmi;
...         <--3
servkey.Buffer = (PWSTR) ExAllocatePool(PagedPool, RegistryPath->Length + sizeof(WCHAR)); <--4
if (!servkey.Buffer)
{
return STATUS_INSUFFICIENT_RESOURCES;
}
servkey.MaximumLength = RegistryPath->Length + sizeof(WCHAR);
RtlCopyUnicodeString(&servkey, RegistryPath);
return STATUS_SUCCESS;         <--5
}

示例同時引用了多種數據結構,它們的詳細注釋可參看資料1第二章的第一小節的後半部分。我們首先掌握這些數據結構的可見域,即可由編程人員存取的域,在理解一些重要的函數時也會涉及部分非透明域,一般情況下可使用微軟推薦的函數來間接訪問它們。

不要混淆驅動對象和設備對象。驅動對象代表了內核加載的驅動鏡像,DriverEntry或AddDevice例程調用IoCreateDevice函數來創建設備對象時,驅動對象將作為該函數的一個輸入參數。設備對象作為硬件或虛擬硬件的抽象,是一個極其重要的數據結構,用於處理設備的I/O請求。

我們將在WinDbg裡實際查看這些數據結構,下面列出它們的定義圖:

內核函數根據執行的功能,大致分為如下幾類,讀者可通過例子接觸到這些函數:

函數前綴 類別 Ex… 執行支持 Hal… 硬件抽象層(僅NT/Windows 2000/XP) Io… I/O管理器(包括即插即用函數) Ke… 內核 Ks… 內核流IRP管理函數 Mm… 內存管理器 Ob… 對象管理器 Po… 電源管理(Vista下存在新的限制) Ps… 進程結構 Rtl… 運行期庫 Se… 安全 Zw… 其他函數 Cc… Cache函數 FsRtl… 文件系統運行期庫

四、WinDbg上的實踐

我們已在上一篇介紹了如何用WinDbg查看KdPrint等內核函數輸出的調試信息。事實上象vs或迅雷之類的軟件也會產生調試信息,但它們由用戶模式下的調試輸出函數發出。

本次實踐的對象是下篇將要用到的的示例代碼,我們將演示如何在WinDbg裡的常用操作,如設置斷點,查看變量的值、數據結構等,代碼位於WINDDK\3790\src\general\ioctl。如果你還不會編譯驅動程序,請趕快完成上一篇拉下的作業。

用WinDbg連接遠程機,按g返還遠程機的控制權,將編譯好的驅動程序sioctl.sys和測試程序ioctlapp.exe復制到遠程機上的任意目錄裡,如我們新建了一個目錄c:\ioctl。

Ctrl+break返回WinDbg後,用.cls命令清屏,延時加載bu sioctl!DriverEntry,此時輸入bl查看已經設置的斷點列表,WinDbg顯示:

kd> bl

0 eu 0001 (0001) (sioctl!DriverEntry)

0表示斷點的id號,e表示斷點的狀態為允許,u表示斷點未被解析,即當前加載的模塊裡未找到符合斷點的符號。

輸入g返回控制權,在遠程機裡打開cmd命令提示符窗口,輸入iocatlapp.exe運行程序,程序立即在DriverEntry處斷下(粉紅括號):

接下來的操作其實和用戶模式下的調試無大的區別,你既可以單步跟蹤(F10或F11),在源代碼上設置斷點(F9),也可以查看變量的賦值和結構(命令dv、dt…)等,請讀者隨意發揮了。如用dt查看某個結構,如:

先用!pcr查看進程或線程內核對象地址,接著查看特定地址的eprocess結構內容:dt -r1 _eprocess 81bef448,尾隨_eprocess的這個當前進程的地址可以用!process取得。特別關注基礎篇七提及的iopm:

+0x030 IopmOffset : 0x20ac

+0x032 Iopl : 0 ''

dt -r1 _ethread 8055be40

bl顯示的內容變更為:

kd>bl

0 e f8d6a5b0 [d:\0vcprojects\ioctl\sys\sioctl.c @ 123] 0001 (0001) SIoctl!DriverEntry

請讀者據資料2與WinDbg幫助文檔認真實踐常用的命令,同時在網上閱讀一些調試高手發表的文章。只要多實踐,可以很輕松的掌握這項基本功,畢竟我們已經擁有源代碼,這和通過反編譯來破解信息的難度是不可相提並論的。

五、結語

本篇是驅動編程學習過程中必須跨過的生死關,如果時間充裕,建議將資料1的第二至第五章先通讀一遍,再精讀兩至三遍。一些不影響大局的細節(如第三章)粗通即可,無須死記;一些新的概念,如第四章的同步技術,可多花費時間盡可能努力的理解這項技術。

驅動編程與匯編語言的學習有相似之處,入門總是先難後易。如果代碼在讀者的眼中和天書一般艱難,請不要懷疑自己的能力。不能理解的概念無非是因為在它之前還存在其他未知的知識,把一個龐大的論題分解為若干小塊,逐步解決它們,總有豁然開朗的時候。

本篇不設置參考完成時間,根據個人的實際情況,盡快完成入門階段的學習。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved