Windows驱动开发要点总结一
1 概述
驅動程序大體可分為兩類三種:第一類:傳統型驅動
? ? 傳統型驅動的特點就是所有的IRP都需要自己去處理,自己實現針對不同IRP的派發函數。其可以分
為以下兩種:
? ? 1. Nt式驅動:此驅動通過注冊系統服務來加載,并且不支持即插即用功能(即沒有處理IRP_MJ_PNP
這個IRP)。
? ? 2. WDM驅動:此驅動不通過注冊系統服務來加載,需啊喲自己編寫inf文件。同時,它與NT式驅動相
比最大的特點就是支持即插即用功能。
? ? 第二類:微過濾驅動
? ? 微過濾驅動是微軟推出的一個驅動框架。它將驅動程序內創建設備對象之類的操作全部封裝了,讓
用戶無需理會此部分繁雜的工作。用戶只需要針對不同的IRP處理好他們響應的前-后操作還有用戶態與
內核態的通信即可,即可以理解為微過濾驅動對IRP的處理類似于用戶態的API HOOK。
? ? 對于剛開始編寫驅動程序的新人來說,使用微過濾驅動是最好不過的了。因為它將大量的內部邏輯
進行了封裝,我們只需要實現相應的處理邏輯即可。
? ? 驅動的編譯方式有很多種。
? ? .微軟官方推薦使用WDK提供的Build Environments來對相應系統編譯驅動,此方法需要用戶自行創
建Source文件編譯,對于不熟悉的人略顯困難
? ? .VS+ddkwizard。ddkwizard插件會在VS中添加一個DDK工程,它可以生成基本的WDM驅動模板,相對
來講會稍微方便些,但是還是需要一些配置。
? ? .直接使用VS編譯。對于做慣用戶態開發的人來講,這個是最方便的,目前我用的也是這種方式。它
的配置項會相對多一點,但是相信這不是障礙。下面就介紹一下如何配置VS(以VS2005為例)
VS2005配置驅動編譯環境方法:
1. 安裝WDK,并配置系統環境變量:WDKROOT-D:\WinDDK\7600.16385.1
2. 啟動VS2005,在菜單欄“工具”-“選項”內選擇“項目和解決方案”-“VC++目錄”依次添加所需的
目錄,具體配置如下圖:
注意:$(WDKROOT)\inc\api一定要放在第一個,否則會導致編譯失敗
3. 具體的項目屬性按照如下設置:
注:創建項目時選擇“Win32項目”
按照以上圖片配置好后就可以直接編譯驅動程序了,此配置是XP系統的,其他系統的鏈接不同的庫就可
以了。
注:如果在編譯過程中遇到如下錯誤:
error LNK2019: 無法解析的外部符號 @__security_check_cookie@4
請您將“項目屬性”-“C/C++”-“代碼生成”的“緩沖區安全檢查”設為“否”
2 構建和加載
將代碼植入內核中的直接方式是使用可加載模塊。正如其名稱所示,設備驅動程序通常是用于設備的。然而,通過驅動程序可以引入任何代碼。
DDK提供了兩種不同的構建環境:檢查(checked)構建和自由(free)構建環境。
在開發設備驅動程序時使用檢查構建環境,對于發行代碼則使用自由構建環境。
檢查構建環境可以是“開始”菜單的“程序”中 Windows DDK圖標組之下的一個鏈接。打開了構建環境
的命令shell后,將當前目錄改為驅動程序目錄,并輸入命令“build”。理想情況下.不會出現任何錯
誤,此時就得到了我們的第一個驅動程序。一個提示:要確保驅動程序目錄所在位置的完整路徑中不包
含任任何空格。
已建立的驅動程序正確加載方法是使用服務控制管理器(Service Control Manager,SCM)。
使用SCM需要創建注冊表鍵,當驅動程序通過SCM加載時,它是不可分頁的。
系統上的每個進程都維護唯一的頁目錄,都擁有自己私有的CR3寄存器的值。
因此,兩個不同的進程可以同時訪問內存地址0x00400000,但將其轉換成兩個獨立的物理內存地址;這
也是為何一個進程無法查看另一個進程內存的原因。
CPU負責跟蹤為軟件代碼和內存分配環的情況,并在各環之間實施訪問限制。
3 基礎數據結構
1) DRIVER_OBJECT驅動對象
WDK中對驅動對象的定義 ?每個驅動程序都會有一個唯一的驅動對象與之對應 ?
它是在驅動加載時被內核對象管理程序創建的 ?
DRIVER_OBJECT 成員說明:
DeviceObject : 每個驅動程序都會有至少一個設備對象。每個設備對象都有一個指向下一個設備對象的
指針,最后一個設備對象指向空。此參數指的是驅動對象的第一個設備對象。設備對象的創建與刪除都
是由程序員自行處理的。
DriverName : 驅動名稱,由UNICODE_STRING記錄。一般格式為\Driver\[DriverName]。
HardwareDatabase : ?設備的硬件數據庫名稱。一般格式為\REGISTRY\MACHINE\HARDWARE\DESCRIPTION
\SYSTEM。
DriverStartIo : 記錄StartIO派發函數地址,用于序列化操作。
DriverUnload : 指定驅動卸載時的回調函數地址。
MajorFunction : 記錄處理IRP的派發函數的函數地址。
FastIoDispatch : 文件驅動中會用到此成員,用于處理快速IO請求。
2) DEVICE_OBJECT設備對象
WDK定義的設備對象 ?DEVICE_OBJECT成員說明:
DriverObject : 指向驅動程序中的驅動對象。如果多個設備對象屬于同一個驅動程序,則它們所指的驅
動對象是相同的。
NextDevice : 指向下一個設備對象。
AttachedDevice : 指向下一個設備對象。如果有更高一層的驅動附加到這個驅動的時候,其指向的就是
更高一層的那個驅動。
CurrentIrp : 使用StartIO派發函數的時候,它指向的是當前的IRP結構
Flags : 標志域,32位無符號整形,其值有以下幾種:
DO_BUFFERED_IO : 讀寫操作使用緩沖方式(系統復制緩沖區)訪問用戶模式數據。
DO_EXCLUSIVE : 一次只允許一個線程打開設備句柄。
DO_DIRECT_IO : 讀寫操作使用直接方式(內存描述表)訪問用戶模式數據。
DO_DEVICE_INITIALIZING : 設備對象正在初始化。
DO_POWER_PAGABLE : 必須在PASSIVE_LEVEL級上處理IRP_MJ_PNP請求。
DO_POWER_INRUSH : 設備上電期間需要大電流。
DeviceExtension : 指向設備擴展對象。設備擴展對象是一個程序員自己定義的結構體。在驅動程序中
,應該盡量避免全局變量的使用,因為全局變量不容易同步,所以將全局變量存在設備擴展中是一個非
常好的解決方案。
DeviceType : 設備類型,常用的設備類型有:
FILE_DEVICE_BEEP:蜂鳴器設備對象。
FILE_DEVICE_CD_ROM:CD光驅設備對象。
FILE_DEVICE_CD_ROM_FILE_SYSTEM:CD光驅文件系統設備對象。
FILE_DEVICE_CONTROLLER:控制器設備對象。
FILE_DEVICE_DATALINK:數據鏈設備對象。
FILE_DEVICE_DFS:DFS設備對象。
FILE_DEVICE_DISK:磁盤設備對象。
FILE_DEVICE_DISK_FILE_SYSTEM:磁盤文件系統設備對象。
FILE_DEVICE_FILE_SYSTEM:文件系統設備對象。
FILE_DEVICE_INPORT_PORT:輸入端口設備對象。
FILE_DEVICE_KEYBOARD:鍵盤設備對象。
FILE_DEVICE_MAILSLOT:郵槽設備對象。
FILE_DEVICE_MIDI_IN:MIDI輸入設備對象。
FILE_DEVICE_MIDI_OUT:MIDI輸出設備對象。
FILE_DEVICE_MOUSE:鼠標設備對象。
FILE_DEVICE_MULTI_UNC_PROVIDER:多UNC設備對象。
FILE_DEVICE_NAMED_PIPE:命名管道設備對象。
FILE_DEVICE_NETWORK:網絡設備對象。
FILE_DEVICE_NETWORK_BROWSER:網絡瀏覽器設備對象。
FILE_DEVICE_NETWORK_FILE_SYSTEM:網絡文件系統設備對象。
FILE_DEVICE_NULL:空設備對象。
FILE_DEVICE_PARALLEL_PORT:并口設備對象。
FILE_DEVICE_PHYSICAL_NETCARD:物理網卡設備對象。
FILE_DEVICE_PRINTER:打印機設備對象。
FILEDEVICE_SCANNER:掃描儀設備對象。
FILE_DEVICE_SERIAL_MOUSE_PORT:串口鼠標設備對象。
FILE_DEVICE_SERIAL_PORT:串口設備對象。
FILE_DEVICE_SCREEN:屏幕設備對象。
FILE_DEVICE_SOUND:聲音設備對象。
FILE_DEVICE_STREAMS:流設備對象。
FILE_DEVICE_TAPE:磁帶設備對象。
FILE_DEVICE_TAPE_FILE_SYSTEM:磁帶文件系統設備對象。
FILE_DEVICE_TRANSPORT:傳輸設備對象。
FILE_DEVICE_UNKNOW:未知設備對象。
FILE_DEVICE_VIDEO:視頻設備對象。
FILE_DEVICE_VIRTUAL_DISK:虛擬磁盤設備對象。
FILE_DEVICE_WAVE_IN:聲音輸入設備對象。
FILE_DEVICE_WAVE_OUT:聲音輸出設備對象。
FILE_DEVICE_8042_PORT:8042端口設備。
FILE_DEVICE_NETWORK_REDIRECTOR:網卡設備對象。
FILE_DEVICE_BATTERY:電池設備對象。
FILE_DEVICE_BUS_EXTENDER:總線擴展設備對象。
FILE_DEVICE_MODEM:調制解調器設備對象。
FILE_DEVICE_VDM:VDM設備對象。
FILE_DEVICE_MASS_STORAGE:大容量存儲設備對象。
FILE_DEVICE_SMB:SMB設備對象。
FILE_DEVICE_KS:內核流設備對象。
FILE_DEVICE_CHANGER:充電設備對象。
FILE_DEVICE_SMARTCARD:智能卡設備對象。
FILE_DEVICE_ACPI:ACPI設備對象。
FILE_DEVICE_DVD:DVD設備對象。
根據設備的需要,需要填寫響應的設備類型。當制作虛擬設備時,應當選擇FILE_DEVICE_UNKONWN類型的
設備。
StackSize : 在多層驅動情況下,驅動與驅動之間會形成類似堆棧的結構。IRP會依次從最高層傳遞到最
底層。StackSize就是驅動的層數。
AlignmentRequirement : 設備在大容量傳輸的時候,需要內存對齊,以保證傳輸速度。
3) 設備擴展
設備對象中只包含了設備的基本信息,如果需要保存其他的信息可以使用設備擴展。設備擴展是由程序員自定義的,可以按照自己的需要添加相關的信息。設備擴展保存在非分頁內存中。
在驅動程序中應該盡量避免使用全局函數,因為全局函數往往導致函數的不可重入性。將全局變量以設
備擴展方式儲存,加以適當的同步保護措施是一個很好的解決方案。除此之外設備擴展往往還會記錄一
下信息:
設備對象的反向指針。
設備狀態或驅動環境信息。
中斷對象指針。
控制器對象指針。
由于設備擴展是驅動程序專用的,它的結構必須在驅動程序的頭文件中定義。
4 字符串
4.1 使用字符串結構?
傳統 C語言的字符串相當的不安全。很容易導致緩沖溢出漏洞。這是因為沒有任何地方確切的表明一個字符串的長度。僅僅用一個’\0’字符來標明這個字符串的結束。一旦碰
到根本就沒有空結束的字符串(可能是攻擊者惡意的輸入、或者是編程錯誤導致的意外),
程序就可能陷入崩潰。?
? 使用高級C++特性的編碼者則容易忽略這個問題。因為常常使用std::string和CString
這樣高級的類。不用去擔憂字符串的安全性了。?
? 在驅動開發中,一般不再用空來表示一個字符串的結束。而是定義了如下的一個結構:?
typedef struct _UNICODE_STRING {?
? ? USHORT Length; ? ? ? ?// 字符串的長度(字節數)?
? ? USHORT MaximumLength; ? ?// 字符串緩沖區的長度(字節數)?
? ? PWSTR ?Buffer; ? ? ? // 字符串緩沖區?
? } UNICODE_STRING, *PUNICODE_STRING;?
?
? 以上是 Unicode 字符串,一個字符為雙字節。與之對應的還有一個 Ansi 字符串。Ansi
字符串就是 C 語言中常用的單字節表示一個字符的窄字符串。?
?
? typedef struct _STRING {?
? ? USHORT Length;?
? ? USHORT MaximumLength;?
? ? PSTR Buffer;?
? } ANSI_STRING, *PANSI_STRING;?
在驅動開發中四處可見的是 Unicode 字符串。因此可以說:Windows 的內核是使用
Uincode 編碼的。ANSI_STRING 僅僅在某些碰到窄字符的場合使用。
? UNICODE_STRING 并不保證Buffer 中的字符串是以空結束的。因此,類似下面的做法都
是錯誤的,可能會會導致內核崩潰:
?UNICODE_STRING str;?
? … ?
? len = wcslen(str.Buffer); ? ? ?// 試圖求長度。?
? DbgPrint(“%ws”,str.Buffer); ? ?// 試圖打印 str.Buffer。?
? ?
? 如果要用以上的方法,必須在編碼中保證 Buffer 始終是以空結束。但這又是一個麻煩
的問題。所以,使用微軟提供的 Rtl系列函數來操作字符串,才是正確的方法。
4.2 字符串的初始化?
? UNICODE_STRING 結構中并不含有字符串緩沖的空間。以下的代碼是完全錯誤的,內核會立刻崩潰:?
? ?
? UNICODE_STRING str;?
? wcscpy(str.Buffer,L”my first string!”);?
? str.Length = str.MaximumLength = wcslen(L”my first string!”) * sizeof(WCHAR);?
?
? 以上的代碼定義了一個字符串并試圖初始化它的值。這樣做是不對的。因為 str.Buffer 只是一個未初始化的指針。它并沒有指向有意義的空間。相反以下的方法是
正確的:?
?// 先定義后,再定義空間?
UNICODE_STRING str;?
? str.Buffer = L”my first string!”;?
? str.Length = str.MaximumLength = wcslen(L”my first string!”) * sizeof(WCHAR);?
? ? … …?
? ?
? 上面代碼的第二行手寫的常數字符串在代碼中形成了“常數”內存空間。這個空間位于
代碼段。將被分配于可執行頁面上。一般的情況下不可寫。為此,要注意的是這個字符串空
間一旦初始化就不要再更改。否則可能引發系統的保護異常。實際上更好的寫法如下:?
//請分析一下為何這樣寫是對的:?
UNICODE_STRING str = { ?
? ? sizeof(L”my first string!”) – sizeof((L”my first string!”)[0]),?
? ? sizeof(L”my first string!”),?
? ? L”my first_string!” };?
?
? 但是這樣定義一個字符串實在太繁瑣了。但是在頭文件 ntdef.h中有一個宏方便這種定
義。使用這個宏之后,我們就可以簡單的定義一個常數字符串如下:?
UNICODE_STRING str = RTL_CONSTANT_STRING(L“my first string!”);?
這只能在定義這個字符串的時候使用。為了隨時初始化一個字符串,可以使用
RtlInitUnicodeString。示例如下:?
? ?
? UNICODE_STRING str;?
? RtlInitUnicodeString(&str,L”my first string!”);?
?
? 用本小節的方法初始化的字符串,不用擔心內存釋放方面的問題。因為我們并沒有分配
任何內存。
4.3 字符串的拷貝
?因為字符串不再是空結束的,所以使用 wcscpy來拷貝字符串是不行的。UNICODE_STRING
可以用 RtlCopyUnicodeString來進行拷貝。在進行這種拷貝的時候,需要注意:拷貝目的字符串的 Buffer必須有足夠的空間。如果 Buffer的空間不足,字符串會拷貝不完
全。這是一個比較隱蔽的錯誤。?
? 下面舉一個例子。?
?
? UNICODE_STRING dst; ? ? // 目標字符串?
? WCHAR dst_buf[256]; ? ? ? // 我們現在還不會分配內存,所以先定義緩沖區?
? UNICODE_STRING src = RTL_CONST_STRING(L”My source string!”);?
? ?
? // 把目標字符串初始化為擁有緩沖區長度為 256的UNICODE_STRING 空串。?
? RtlInitEmptyString(dst,dst_buf,256*sizeof(WCHAR));?
? RtlCopyUnicodeString(&dst,&src); ?// 字符串拷貝!?
以上這個拷貝之所以可以成功,是因為 256比 L” My source string!”的長度要大。
如果小,則拷貝也不會出現任何明示的錯誤。但是拷貝結束之后,字符串實際上被截短了。?
? 我曾經犯過的一個錯誤是沒有調用 RtlInitEmptyString。結果 dst 字符串被初始化認
為緩沖區長度為 0。雖然程序沒有崩潰,卻實際上沒有拷貝任何內容。?
? 在拷貝之前,最謹慎的方法是根據源字符串的長度動態分配空間。
4.4 字符串的連接?
? 會常常碰到這樣的需求:要把兩個字符串連接到一起。簡單的追加一個字符串并不困難。重要的依然是保證目標字符串的空間大小。下面是范例:?
NTSTATUS status;?
? UNICODE_STRING dst; ? ? // 目標字符串?
? WCHAR dst_buf[256]; ? ? ? // 我們現在還不會分配內存,所以先定義緩沖區?
? UNICODE_STRING src = RTL_CONST_STRING(L”My source string!”);?
? ?
? // 把目標字符串初始化為擁有緩沖區長度為 256的UNICODE_STRING 空串?
? RtlInitEmptyString(dst,dst_buf,256*sizeof(WCHAR));?
? RtlCopyUnicodeString(&dst,&src); ?// 字符串拷貝!?
? ?
? status = RtlAppendUnicodeToString(?
? ? ? &dst,L”my second string!”);?
? if(status != STATUS_SUCCESS)?
? {?
? ? ……?
? }?
NTSTATUS 是常見的返回值類型。如果函數成功,返回 STATUS_SUCCESS。否則的話,是
一個錯誤碼。RtlAppendUnicodeToString 在目標字符串空間不足的時候依然可以連接字符
串,但是會返回一個警告性的錯誤 STATUS_BUFFER_TOO_SMALL。?
? 另 外 一 種 情 況 是 希 望 連 接 兩 個 UNICODE_STRING , 這 種 情 況 請 調 用
RtlAppendUnicodeStringToString。這個函數的第二個參數也是一個 UNICODE_STRING 的指
針。
4.5 字符串的打印?
? 字符串的連接另一種常見的情況是字符串和數字的組合。有時數字需要被轉換為字符串。有時需要把若干個數字和字符串混合組合起來。這往往用于打印日志的時候。日志中可
能含有文件名、時間、和行號,以及其他的信息。?
? 熟悉 C語言的讀者會使用sprintf。這個函數的寬字符版本為 swprintf。該函數在驅動
開發中可以使用,但不安全。微軟建議使用 RtlStringCbPrintfW 來代替它。
RtlStringCbPrintfW 需要包含頭文件 ntstrsafe.h。在連接的時候,還需要連接庫
ntsafestr.lib。?
? 下面的代碼生成一個字符串,字符串中包含文件的路徑,和這個文件的大小。?
? ?
?#include <ntstrsafe.h>?
? // 任何時候,假設文件路徑的長度為有限的都是不對的。應該動態的分配?
? // 內存。但是動態分配內存的方法還沒有講述,所以這里再次把內存空間?
? // 定義在局部變量中,也就是所謂的“在棧中”?
? WCHAR buf[512] = { 0 };?
? UNICODE_STRING dst;?
? NTSTATUS status;?
? ……?
?// 字符串初始化為空串。緩沖區長度為 512*sizeof(WCHAR)?
?RtlInitEmptyString(dst,dst_buf,512*sizeof(WCHAR));?
??
?// 調用 RtlStringCbPrintfW 來進行打印?
?status = RtlStringCbPrintfW(?
? ?dst->Buffer,L”file path = %wZ file size = %d \r\n”,?
? ?&file_path,file_size);?
?// 這里調用 wcslen沒問題,這是因為 RtlStringCbPrintfW打印的?
?// 字符串是以空結束的。?
?dst->Length = wcslen(dst->Buffer) * sizeof(WCHAR);?
?RtlStringCbPrintfW 在目標緩沖區內存不足的時候依然可以打印,但是多余的部分被
截去了。返回的 status 值為 STATUS_BUFFER_OVERFLOW。調用這個函數之前很難知道究竟需
要多長的緩沖區。一般都采取倍增嘗試。每次都傳入一個為前次嘗試長度為2 倍長度的新緩
沖區,直到這個函數返回STATUS_SUCCESS 為止。?
? 值得注意的是 UNICODE_STRING 類型的指針,用%wZ 打印可以打印出字符串。在不能保
證字符串為空結束的時候,必須避免使用%ws 或者%s。其他的打印格式字符串與傳統 C 語言
中的 printf 函數完全相同。可以盡情使用。?
? 另外就是常見的輸出打印。printf 函數只有在有控制臺輸出的情況下才有意義。在驅
動中沒有控制臺。但是 Windows內核中擁有調試信息輸出機制。可以使用特殊的工具查看打
印的調試信息(請參閱附錄 1“WDK的安裝與驅動開發的環境配置”)。?
? 驅動中可以調用DbgPrint()函數來打印調試信息。這個函數的使用和printf基本相同。
但是格式字符串要使用寬字符。DbgPrint()的一個缺點在于,發行版本的驅動程序往往不希
望附帶任何輸出信息,只有調試版本才需要調試信息。但是 DbgPrint()無論是發行版本還
是調試版本編譯都會有效。為此可以自己定義一個宏:
#if DBG?
? ? KdPrint(a) ?DbgPrint##a?
? #else?
? ? KdPrint (a)?
? #endif?
? ? ?
? 不過這樣的后果是,由于 KdPrint (a)只支持 1 個參數,因此必須把 DbgPrint 的所有
參數都括起來當作一個參數傳入。導致 KdPrint 看起來很奇特的用了雙重括弧:?
?
? // 調用 KdPrint 來進行輸出調試信息?
? status = KdPrint ((?
? ? L”file path = %wZ file size = %d \r\n”,?
? ? &file_path,file_size));?
?
? 這個宏沒有必要自己定義,WDK 包中已有。所以可以直接使用 KdPrint 來代替DbgPrint
取得更方便的效果。
5 內存與鏈表
?5.1 內存的分配與釋放?
? 內存泄漏是 C 語言中一個臭名昭著的問題。作為內核開發者,將有必要自己來面對它。在傳統的 C 語言中,分配內存常常使用的函數是 malloc。這個函數的使用非常簡單,傳入長度參數就得到內存空間。在驅動中使用內存分配,這個函數不再有效。驅動中分
配內存,最常用的是調用ExAllocatePoolWithTag。
一個字符串被復制到另一個字符串的時候,最好根據源字符串的空間長度來分配目標字符串的長度。下面的舉例,是把一個字符串 src拷貝到字
符串 dst。?
?// 定義一個內存分配標記?
? #define MEM_TAG ‘MyTt’?
? // 目標字符串,接下來它需要分配空間。?
? UNICODE_STRING dst = { 0 };?
? // 分配空間給目標字符串。根據源字符串的長度。?
? dst.Buffer = ?
? ? (PWCHAR)ExAllocatePoolWithTag(NonpagedPool,src->Length,MEM_TAG);?
? if(dst.Buffer == NULL)?
? {?
? ? // 錯誤處理?
? ? status = STATUS_INSUFFICIENT_RESOUCRES;?
? ? ……?
? }?
? dst.Length = dst.MaximumLength = src->Length;?
? status = RtlCopyUnicodeString(&dst,&src);?
? ASSERT(status == STATUS_SUCCESS);?
ExAllocatePoolWithTag 的第一個參數 NonpagedPool 表明分配的內存是鎖定內存。這
些內存永遠真實存在于物理內存上。不會被分頁交換到硬盤上去。第二個參數是長度。第三
個參數是一個所謂的“內存分配標記”。?
? 內存分配標記用于檢測內存泄漏。想象一下,我們根據占用越來越多的內存的分配標記,
就能大概知道泄漏的來源。一般每個驅動程序定義一個自己的內存標記。也可以在每個模塊
中定義單獨的內存標記。內存標記是隨意的 32 位數字。即使沖突也不會有什么問題。?
? 此外也可以分配可分頁內存,使用 PagedPool即可。?
? ExAllocatePoolWithTag 分配的內存可以使用 ExFreePool 來釋放。如果不釋放,則永
遠泄漏。并不像用戶進程關閉后自動釋放所有分配的空間。即使驅動程序動態卸載,也不能
釋放空間。唯一的辦法是重啟計算機。?
? ExFreePool 只需要提供需要釋放的指針即可。舉例如下:?
?ExFreePool(dst.Buffer);?
? dst.Buffer = NULL;?
? dst.Length = dst.MaximumLength = 0;?
? ?
? ExFreePool 不能用來釋放一個棧空間的指針。否則系統立刻崩潰。像以下的代碼:?
?
? UNICODE_STRING src = RTL_CONST_STRING(L”My source string!”);?
? ExFreePool(src.Buffer);?
?
? 會招來立刻藍屏。所以請務必保持 ExAllocatePoolWithTag和 ExFreePool 的成對關系。
5.2 使用 LIST_ENTRY?
? Windows 的內核開發者們自己開發了部分數據結構,比如說 LIST_ENTRY。?? LIST_ENTRY 是一個雙向鏈表結構。它總是在使用的時候,被插入到已有的數據結構中。
一個例子。我構筑一個鏈表,這個鏈表的每個節點,是一個文件名和一個文件大小兩
個數據成員組成的結構。此外有一個 FILE_OBJECT的指針對象。在驅動中,這代表一個文件
對象。本書后面的章節會詳細解釋。這個鏈表的作用是:保存了文件的文件名和長度。只要
傳入 FILE_OBJECT 的指針,使用者就可以遍歷鏈表找到文件名和文件長度。?
?
? typedef struct {?
? ? PFILE_OBJECT file_object;?
? ? UNICODE_STRING file_name;?
? ? LARGE_INTEGER file_length;?
? } MY_FILE_INFOR, *PMY_FILE_INFOR;?
?一些讀者會馬上注意到文件的長度用 LARGE_INTEGER 表示。這是一個代表長長整型的數
據結構。這個結構我們在下一小小節“使用長長整型數據”中介紹。?
? 為了讓上面的結構成為鏈表節點,我必須在里面插入一個 LIST_ENTRY 結構。至于插入
的位置并無所謂。可以放在最前,也可以放中間,或者最后面。但是實際上讀者很快會發現
把 LIST_ENTRY放在開頭是最簡單的做法:?
?
? typedef struct {?
? ? LIST_ENTRY list_entry;?
? ? PFILE_OBJECT file_object;?
? ? UNICODE_STRING file_name;?
? ? LARGE_INTEGER file_length;?
? } MY_FILE_INFOR, *PMY_FILE_INFOR; ??
?
? list_entry 如果是作為鏈表的頭,在使用之前,必須調用 InitializeListHead 來初始
化。下面是示例的代碼:?
// 我們的鏈表頭
? LIST_ENTRY ? ?my_list_head;?
?
? // 鏈表頭初始化。一般的說在應該在程序入口處調用一下?
? void MyFileInforInilt()?
? {?
? ? InitializeListHead(&my_list_head);?
? }?
? // 我們的鏈表節點。里面保存一個文件名和一個文件長度信息。?
? typedef struct {?
? ? LIST_ENTRY list_entry;?
? ? PFILE_OBJECT file_object;?
? ? PUNICODE_STRING file_name;?
? ? LARGE_INTEGER file_length;?
? } MY_FILE_INFOR, *PMY_FILE_INFOR;?
?
? // 追加一條信息。也就是增加一個鏈表節點。請注意 file_name 是外面分配的。?
? // 內存由使用者管理。本鏈表并不管理它。?
? NTSTATUS MyFileInforAppendNode(?
? ? PFILE_OBJECT file_object, ?
? ? PUNICODE_STRING file_name,?
? ? PLARGE_INTEGER file_length)?
?{?
? ? PMY_FILE_INFOR my_file_infor = ?
? ? ? (PMY_FILE_INFOR)ExAllocatePoolWithTag(?
? ? ? ? PagedPool,sizeof(MY_FILE_INFOR),MEM_TAG);?
? ? if(my_file_infor == NULL)?
? ? ? return STATUS_INSUFFICIENT_RESOURES;?
?
? ? // 填寫數據成員。?
? ? my_file_infor->file_object = file_object;?
? ? my_file_infor->file_name = file_name;?
? ? my_file_infor->file_length = file_length;?
?
? ? // 插入到鏈表末尾。請注意這里沒有使用任何鎖。所以,這個函數不是多?
? ? // 多線程安全的。在下面自旋鎖的使用中講解如何保證多線程安全性。?
? ? InsertHeadList(&my_list_head, (PLIST_ENTRY)& my_file_infor);?
? ? return STATUS_SUCCESS; ??
? }
以上的代碼實現了插入。可以看到 LIST_ENTRY 插入到 MY_FILE_INFOR 結構的頭部的好
處。這樣一來一個 MY_FILE_INFOR 看起來就像一個 LIST_ENTRY。不過糟糕的是并非所有的
情況都可以這樣。比如 MS 的許多結構喜歡一開頭是結構的長度。因此在通過 LIST_ENTRY
結構的地址獲取所在的節點的地址的時候,有個地址偏移計算的過程。可以通過下面的一個
??
典型的遍歷鏈表的示例中看到:
for(p = my_list_head.Flink; p != &my_list_head.Flink; p = p->Flink)?
{?
? ? PMY_FILE_INFOR elem = ?
? ? ? CONTAINING_RECORD(p,MY_FILE_INFOR, list_entry);?
? ? // 在這里做需要做的事…?
? ? }?
? }?
?
? 其中的 CONTAINING_RECORD 是一個 WDK 中已經定義的宏,作用是通過一個 LIST_ENTRY
結構的指針,找到這個結構所在的節點的指針。定義如下:
#define CONTAINING_RECORD(address, type, field) ((type *)( \?
? ?(PCHAR)(address) - \?
? ?(ULONG_PTR)(&((type *)0)->field)))?
從上面的代碼中可以總結如下的信息:?
?LIST_ENTRY 中的數據成員Flink指向下一個 LIST_ENTRY。?
?整個鏈表中的最后一個 LIST_ENTRY的 Flink不是空。而是指向頭節點。?
?得到 LIST_ENTRY 之后,要用 CONTAINING_RECORD 來得到鏈表節點中的數據。?
5.3 使用長長整型數據?
? 這里解釋前面碰到的 LARGE_INTEGER 結構。與可能的誤解不同,64 位數據并非要在 64位操作系統下才能使用。在 VC中,64 位數據的類型為__int64。定義寫法如下:?
?
? __int64 file_offset;?
?
? 上面之所以定義的變量名為 file_offset,是因為文件中的偏移量是一種常見的要使用
64 位數據的情況。同時,文件的大小也是如此(回憶上一小節中定義的文件大小)。32位數
據無符號整型只能表示到4GB。而眾所周知,現在超過 4GB的文件絕對不罕見了。但是實際
上__int64 這個類型在驅動開發中很少被使用。基本上被使用到的是一個共用體:
LARGE_INTEGER。這個共用體定義如下:?
?
?typedef __int64 LONGLONG; ??
? typedef union _LARGE_INTEGER {?
? ? struct {?
? ? ? ULONG LowPart;?
? ? ? LONG HighPart;?
? ? };?
? ? struct {?
? ? ? ULONG LowPart;
? ? LONG HighPart;?
? ? } u;?
? ? ? LONGLONG QuadPart;?
? } LARGE_INTEGER;?
?
? 這個共用體的方便之處在于,既可以很方便的得到高 32位,低 32 位,也可以方便的得
到整個 64位。進行運算和比較的時候,使用 QuadPart 即可。
?LARGE_INTEGER a,b;?
? a.QuadPart = 100;?
? a.QuadPart *= 100;?
? b.QuadPart = a.QuadPart;?
? if(b.QuadPart > 1000)?
? {?
? ? KdPrint(“b.QuadPart ?< ?1000, ?LowPart ?= ?%x ?HighPart ?= ?%x”,?
b.LowPart,b.HighPart);?
? }?
? ?
? 上面這段代碼演示了這種結構的一般用法。在實際編程中,會碰到大量的參數是
LARGE_INTEGER類型的。?
5.4 使用自旋鎖
?鏈表之類的結構總是涉及到惱人的多線程同步問題,這時候就必須使用鎖。
?鎖存在的意義? 這和多線程操作有關。在驅動開發的代碼中,大多是
在于多線程執行環境的。就是說可能有幾個線程在同時調用當前函數。?
?這樣一來,前文中提及的追加鏈表節點函數就根本無法使用了。因為
FileInforAppendNode這個函數只是簡單的操作鏈表。如果兩個線程同時調用這個函數來
作鏈表的話:注意這個函數操作的是一個全局變量鏈表。換句話說,無論有多少個線程同
執行,他們操作的都是同一個鏈表。這就可能發生,一個線程插入一個節點的同時,另一
線程也同時插入。他們都插入同一個鏈表節點的后邊。這時鏈表就會發生問題。到底最后
入的是哪一個呢?要么一個丟失了。要么鏈表被損壞了。?
?如下的代碼初始化獲取一個自選鎖:?
?KSPIN_LOCK my_spin_lock;?
?KeInitializeSpinLock(&my_spin_lock);?
? KeInitializeSpinLock 這個函數沒有返回值。下面的代碼展示了如何使用這個
SpinLock。在 KeAcquireSpinLock 和KeReleaseSpinLock之間的代碼是只有單線程執行的。
其他的線程會停留在 KeAcquireSpinLock等候。直到 KeReleaseSpinLock 被調用。KIRQL是
一個中斷級。KeAcquireSpinLock 會提高當前的中斷級。但是目前忽略這個問題。中斷級在
后面講述。?
? ?
KIRQL irql;?
? KeAcquireSpinLock(&my_spin_lock,&irql);?
? // To do something …?
? KeReleaseSpinLock(&my_spin_lock,irql);?
? ?
? 初學者要注意的是,像下面寫的這樣的“加鎖”代碼是沒有意義的,等于沒加鎖:?
? ?
? void MySafeFunction()?
? {?
? ? KSPIN_LOCK my_spin_lock;?
? ? KIRQL irql;?
? ? KeInitializeSpinLock(&my_spin_lock);?
? ? KeAcquireSpinLock(&my_spin_lock,&irql);?
? ? // 在這里做要做的事情…?
? ? KeReleaseSpinLock(&my_spin_lock,irql);?
? }
原因是 my_spin_lock 在堆棧中。每個線程來執行的時候都會重新初始化一個鎖。只有
所有的線程共用一個鎖,鎖才有意義。所以,鎖一般不會定義成局部變量。可以使用靜態變
量、全局變量,或者分配在堆中(見前面的 1.2.1 內存的分配與釋放一節)。請讀者自己寫
出正確的方法。?
? LIST_ENTRY 有一系列的操作。這些操作并不需要使用者自己調用獲取與釋放鎖。只需
要為每個鏈表定義并初始化一個鎖即可:?
?? ?
? LIST_ENTRY ? ?my_list_head; ? ?// 鏈表頭?
? KSPIN_LOCK ?my_list_lock; ? ?// 鏈表的鎖?
? ?
? // 鏈表初始化函數?
? void MyFileInforInilt()?
? {?
? ? InitializeListHead(&my_list_head);?
? ? KeInitializeSpinLock(&my_list_lock);?
? }
? 鏈表一旦完成了初始化,之后的可以采用一系列加鎖的操作來代替普通的操作。比如插
入一個節點,普通的操作代碼如下:?
?
? InsertHeadList(&my_list_head, (PLIST_ENTRY)& my_file_infor);?
?
? 換成加鎖的操作方式如下:?
?
? ExInterlockedInsertHeadList(?
? ? &my_list_head,
? (PLIST_ENTRY)& my_file_infor,?
? ? &my_list_lock);?
?
? 注 意 不 同 之 處 在 于 , 增 加 了 一 個 KSPIN_LOCK 的 指 針 作 為 參 數 。 在
ExInterlockedInsertHeadList 中,會自動的使用這個 KSPIN_LOCK 進行加鎖。類似的還有
一個加鎖的 Remove函數,用來移除一個節點,調用如下:?
?
? my_file_infor = ExInterlockedRemoveHeadList (?
? ? &my_list_head, ?
? ? &my_list_lock);?
?
? 這個函數從鏈表中移除第一個節點。并返回到 my_file_infor中。?
總結
以上是生活随笔為你收集整理的Windows驱动开发要点总结一的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: js dom 操作实例图解
- 下一篇: struts 2 漏洞学习总结