版權聲明php
本書是免費電子書。 做者保留一切權利。但在保證本書完整性(包括版權聲明、前言、正文內容、後記、以及做者的信息),並不增刪、改變其中任何文字內容的前提下,歡迎任何讀者 以任何形式(包括各類格式的文檔)複製和轉載本書。同時不限制利用此書贏利的行爲(如收費註冊下載,或者出售光盤或打印版本)。不知足此前提的任何轉載、 複製、贏利行爲則是侵犯版權的行爲。
發現本書的錯漏之處,請聯繫做者。請不要修改本文中任何內容,不通過做者的贊成發佈修改後的版本。
做者信息html
做者網名楚狂人。真名譚文。在上海從事Windows驅動開發相關的工做。對本書任何內容有任何疑問的讀者,能夠用下列方式和做者取得聯繫:
QQ:16191935
MSN:walled_river@hotmail.com
前言node
本書很是適合熟悉Windows應用編程的讀者轉向驅動開發。全部的內容都從最基礎的編程方法入手。介紹相關的內核API,而後舉出示範的例子。這本書只 有不到70頁,是一本很是精簡的小冊子。因此它並不直接指導讀者開發某種特定類型的驅動程序。而是起到一個入門指導的做用。
即便都是使用C/C++語言的代碼,在不一樣的應用環境中,經常看起來仍是截然不同。好比用TurboC++編寫的DOS程序代碼和用VC++編寫的MFC應用程序的代碼,看起來就幾乎不像是同一種語言。這是因爲它們所依賴的開發包不相同的緣故。
在任何狀況下都以寫出避免依賴的代碼爲最佳。這樣能夠避免重複勞動。可是咱們在學習一種開發包的使用時,必須習慣這個環境的編碼方式,以便得到充分利用這個開發包的能力。
本書的代碼幾乎都依賴於WDK(Windows Driver Kit)。可是不限WDK的版本。WDK還在不斷的升級中。這個開發包是由微軟公司免費提供的。讀者能夠在微軟的網站上下載。
固然讀者必須把WDK安裝的計算機上並配置好開發環境。具體的安裝和配置方法本書沒有提供。由於網上已經有很是多的中文文檔介紹它們。
讀完這本書以後,讀者必定能夠更輕鬆的閱讀其餘專門的驅動程序開發的文檔和相關書籍。而不至於看到大量沒法理解的代碼而中途放棄。若是有任何關於本書的內 容的問題,讀者能夠隨時發郵件到mfc_tan_wen@163.com或者walled_river@hotmail.com。可以回答的問題我通常都 會答覆。
寫本書的時候,我和wowocock合做的一本名爲《天書夜讀》(在網上有一個大約20%內容的縮減電子版本)正在電子工業出版社編輯。預計還有不到一個 月左右就會出版。這也是我本身所見的惟一一本中文原創的從彙編和反彙編角度來學習Windows內核編程和信息安全軟件開發的書。但願讀者多多支持。有想 購買的讀者請發郵件給我。我會在本書出版的第一時間,回覆郵件告知購買的方法。
此外我正在寫另外一本關於Windows安全軟件的驅動編程的書。可是題目尚未擬好。實際上,讀者如今見到的免費版本的《Windows驅動編程基礎教程》是從這本書的第一部分中節選出來的。這本書篇幅比較大,大約有600-800頁。主要內容以下:
第一章驅動編程基礎
第二章磁盤設備驅動
第三章磁盤還原與加密
第四章傳統文件系統過濾
第五章小端口文件系統過濾
第六章文件系統保護與加密
第七章協議網絡驅動
第八章物理網絡驅動
第九章網絡防火牆與安全鏈接
第十章打印機驅動與虛擬打印
第十一章視頻驅動與過濾
附錄A WDK的安裝與驅動開發的環境配置
附錄B 用WinDbg調試Windows驅動程序
這本書尚未完成。可是確定要付出巨大的精力,因此請讀者不要來郵件索取完整的免費的電子版本。但願讀者支持本書的紙版出版。由於沒有完成,因此尚未聯繫出版商。有願意合做出版本書的讀者請發郵件與我聯繫。
凡是發送郵件給個人讀者,我將會發送郵件提供本人做品最新的出版信息,以及最新發布的驅動開發相關的免費電子書。若是不須要這些信息的,請在郵件裏註明,或者回復郵件給我來取消訂閱。
譚文
2008年6月9日
目錄程序員
第一章 字符串面試
1.1 使用字符串結構算法
經常使用傳統C語言的程序員比較喜歡用以下的方法定義和使用字符串:
char *str = { 「my first string」 }; // ansi字符串
wchar_t *wstr = { L」my first string」 }; // unicode字符串
size_tlen = strlen(str); //ansi字符串求長度
size_twlen = wcslen(wstr); //unicode字符串求長度
printf(「%s%ws %d %d」,str,wstr,len,wlen); // 打印兩種字符串
可是實際上這種字符串至關的不安全。很容易致使緩衝溢出漏洞。這是由於沒有任何地方確切的代表一個字符串的長度。僅僅用一個’\0’字符來標明這個字符串 的結束。一旦碰到根本就沒有空結束的字符串(多是攻擊者惡意的輸入、或者是編程錯誤致使的意外),程序就可能陷入崩潰。
使用高級C++特性的編碼者則容易忽略這個問題。由於經常使用std::string和CString這樣高級的類。不用去擔心字符串的安全性了。
在驅動開發中,通常再也不用空來表示一個字符串的結束。而是定義了以下的一個結構:
typedefstruct _UNICODE_STRING {
USHORTLength; // 字符串的長度(字節數)
USHORTMaximumLength; // 字符串緩衝區的長度(字節數)
PWSTR Buffer; //字符串緩衝區
}UNICODE_STRING, *PUNICODE_STRING;
以上是Unicode字符串,一個字符爲雙字節。與之對應的還有一個Ansi字符串。Ansi字符串就是C語言中經常使用的單字節表示一個字符的窄字符串。
typedefstruct _STRING {
USHORTLength;
USHORTMaximumLength;
PSTRBuffer;
}ANSI_STRING, *PANSI_STRING;
在驅動開發中四處可見的是Unicode字符串。所以能夠說:Windows的內核是使用Uincode編碼的。ANSI_STRING僅僅在某些碰到窄字符的場合使用。並且這種場合很是罕見。
UNICODE_STRING並不保證Buffer中的字符串是以空結束的。所以,相似下面的作法都是錯誤的,可能會會致使內核崩潰:
UNICODE_STRINGstr;
…
len =wcslen(str.Buffer); // 試圖求長度。
DbgPrint(「%ws」,str.Buffer); // 試圖打印str.Buffer。
若是要用以上的方法,必須在編碼中保證Buffer始終是以空結束。但這又是一個麻煩的問題。因此,使用微軟提供的Rtl系列函數來操做字符串,纔是正確的方法。下文逐步的講述這個系列的函數的使用。
1.2 字符串的初始化編程
請回顧以前的UNICODE_STRING結構。讀者應該能夠注意到,這個結構中並不含有字符串緩衝的空間。這是一個初學者常見的出問題的來源。如下的代碼是徹底錯誤的,內核會馬上崩潰:
UNICODE_STRINGstr;
wcscpy(str.Buffer,L」myfirst 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」myfirst string!」) – sizeof((L」my first string!」)[0]),
sizeof(L」myfirst string!」),
L」myfirst_string!」 };
可是這樣定義一個字符串實在太繁瑣了。可是在頭文件ntdef.h中有一個宏方便這種定義。使用這個宏以後,咱們就能夠簡單的定義一個常數字符串以下:
#include<ntdef.h>
UNICODE_STRING str = RTL_CONSTANT_STRING(L「myfirst string!」);
這隻能在定義這個字符串的時候使用。爲了隨時初始化一個字符串,能夠使用RtlInitUnicodeString。示例以下:
UNICODE_STRINGstr;
RtlInitUnicodeString(&str,L」myfirst string!」);
用本小節的方法初始化的字符串,不用擔憂內存釋放方面的問題。由於咱們並無分配任何內存。
1.3 字符串的拷貝設計模式
由於字符串再也不是空結束的,因此使用wcscpy來拷貝字符串是不行的。UNICODE_STRING能夠用RtlCopyUnicodeString來 進行拷貝。在進行這種拷貝的時候,最須要注意的一點是:拷貝目的字符串的Buffer必須有足夠的空間。若是Buffer的空間不足,字符串會拷貝不完 全。這是一個比較隱蔽的錯誤。
下面舉一個例子。
UNICODE_STRINGdst; // 目標字符串
WCHARdst_buf[256]; // 咱們如今還不會分配內存,因此先定義緩衝區
UNICODE_STRINGsrc = 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。雖然程序沒有崩潰,卻實際上沒有拷貝任何內容。
在拷貝以前,最謹慎的方法是根據源字符串的長度動態分配空間。在1.2節「內存與鏈表」中,讀者會看到動態分配內存處理字符串的方法。
1.4 字符串的鏈接數組
UNICODE_STRING再也不是簡單的字符串。操做這個數據結構每每須要更多的耐心。讀者會經常碰到這樣的需求:要把兩個字符串鏈接到一塊兒。簡單的追加一個字符串並不困難。重要的依然是保證目標字符串的空間大小。下面是範例:
NTSTATUSstatus;
UNICODE_STRINGdst; // 目標字符串
WCHARdst_buf[256]; // 咱們如今還不會分配內存,因此先定義緩衝區
UNICODE_STRINGsrc = RTL_CONST_STRING(L」My source string!」);
// 把目標字符串初始化爲擁有緩衝區長度爲256的UNICODE_STRING空串
RtlInitEmptyString(dst,dst_buf,256*sizeof(WCHAR));
RtlCopyUnicodeString(&dst,&src); // 字符串拷貝!
status= RtlAppendUnicodeToString(
&dst,L」mysecond string!」);
if(status!= STATUS_SUCCESS)
{
……
}
NTSTATUS是常見的返回值類型。若是函數成功,返回STATUS_SUCCESS。不然的話,是一個錯誤碼。 RtlAppendUnicodeToString在目標字符串空間不足的時候依然能夠鏈接字符串,可是會返回一個警告性的錯誤 STATUS_BUFFER_TOO_SMALL。
另一種狀況是但願鏈接兩個UNICODE_STRING,這種狀況請調用RtlAppendUnicodeStringToString。這個函數的第二個參數也是一個UNICODE_STRING的指針。
1.5 字符串的打印安全
字符串的鏈接另外一種常見的狀況是字符串和數字的組合。有時數字須要被轉換爲字符串。有時須要把若干個數字和字符串混合組合起來。這每每用於打印日誌的時候。日誌中可能含有文件名、時間、和行號,以及其餘的信息。
熟悉C語言的讀者會使用sprintf。這個函數的寬字符版本爲swprintf。該函數在驅動開發中依然能夠使用,可是不安全。微軟建議使用 RtlStringCbPrintfW來代替它。RtlStringCbPrintfW須要包含頭文件ntstrsafe.h。在鏈接的時候,還須要鏈接 庫ntsafestr.lib。
下面的代碼生成一個字符串,字符串中包含文件的路徑,和這個文件的大小。
#include<ntstrsafe.h>
// 任什麼時候候,假設文件路徑的長度爲有限的都是不對的。應該動態的分配
// 內存。可是動態分配內存的方法尚未講述,因此這裏再次把內存空間
// 定義在局部變量中,也就是所謂的「在棧中」
WCHARbuf[512] = { 0 };
UNICODE_STRINGdst;
NTSTATUSstatus;
……
// 字符串初始化爲空串。緩衝區長度爲512*sizeof(WCHAR)
RtlInitEmptyString(dst,dst_buf,512*sizeof(WCHAR));
// 調用RtlStringCbPrintfW來進行打印
status= RtlStringCbPrintfW(
dst->Buffer,L」filepath = %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」filepath = %wZ file size = %d \r\n」,
&file_path,file_size));
這個宏沒有必要本身定義,WDK包中已有。因此能夠直接使用KdPrint來代替DbgPrint取得更方便的效果。
第二章 內存與鏈表
2.1內存的分配與釋放
內存泄漏是C語言中一個臭名昭著的問題。可是做爲內核開發者,讀者將有必要本身來面對它。在傳統的C語言中,分配內存經常使用的函數是malloc。這個 函數的使用很是簡單,傳入長度參數就獲得內存空間。在驅動中使用內存分配,這個函數再也不有效。驅動中分配內存,最經常使用的是調用 ExAllocatePoolWithTag。其餘的方法在本章範圍內所有忽略。回憶前一小節關於字符串的處理的狀況。一個字符串被複制到另外一個字符串的 時候,最好根據源字符串的空間長度來分配目標字符串的長度。下面的舉例,是把一個字符串src拷貝到字符串dst。
// 定義一個內存分配標記
#defineMEM_TAG ‘MyTt’
// 目標字符串,接下來它須要分配空間。
UNICODE_STRINGdst = { 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_STRINGsrc = RTL_CONST_STRING(L」My source string!」);
ExFreePool(src.Buffer);
會招來馬上藍屏。因此請務必保持ExAllocatePoolWithTag和ExFreePool的成對關係。
2.2 使用LIST_ENTRY
Windows的內核開發者們本身開發了部分數據結構,好比說LIST_ENTRY。
LIST_ENTRY是一個雙向鏈表結構。它老是在使用的時候,被插入到已有的數據結構中。下面舉一個例子。我構築一個鏈表,這個鏈表的每一個節點,是一個 文件名和一個文件大小兩個數據成員組成的結構。此外有一個FILE_OBJECT的指針對象。在驅動中,這表明一個文件對象。本書後面的章節會詳細解釋。 這個鏈表的做用是:保存了文件的文件名和長度。只要傳入FILE_OBJECT的指針,使用者就能夠遍歷鏈表找到文件名和文件長度。
typedefstruct {
PFILE_OBJECTfile_object;
UNICODE_STRINGfile_name;
LARGE_INTEGERfile_length;
}MY_FILE_INFOR, *PMY_FILE_INFOR;
一些讀者會立刻注意到文件的長度用LARGE_INTEGER表示。這是一個表明長長整型的數據結構。這個結構咱們在下一小小節「使用長長整型數據」中介紹。
爲了讓上面的結構成爲鏈表節點,我必須在裏面插入一個LIST_ENTRY結構。至於插入的位置並沒有所謂。能夠放在最前,也能夠放中間,或者最後面。可是實際上讀者很快會發現把LIST_ENTRY放在開頭是最簡單的作法:
typedefstruct {
LIST_ENTRYlist_entry;
PFILE_OBJECTfile_object;
UNICODE_STRINGfile_name;
LARGE_INTEGERfile_length;
} MY_FILE_INFOR, *PMY_FILE_INFOR;
list_entry若是是做爲鏈表的頭,在使用以前,必須調用InitializeListHead來初始化。下面是示例的代碼:
// 咱們的鏈表頭
LIST_ENTRY my_list_head;
// 鏈表頭初始化。通常的說在應該在程序入口處調用一下
voidMyFileInforInilt()
{
InitializeListHead(&my_list_head);
}
// 咱們的鏈表節點。裏面保存一個文件名和一個文件長度信息。
typedefstruct {
LIST_ENTRYlist_entry;
PFILE_OBJECTfile_object;
PUNICODE_STRINGfile_name;
LARGE_INTEGERfile_length;
} MY_FILE_INFOR, *PMY_FILE_INFOR;
// 追加一條信息。也就是增長一個鏈表節點。請注意file_name是外面分配的。
// 內存由使用者管理。本鏈表並無論理它。
NTSTATUS MyFileInforAppendNode(
PFILE_OBJECTfile_object,
PUNICODE_STRINGfile_name,
PLARGE_INTEGERfile_length)
{
PMY_FILE_INFORmy_file_infor =
(PMY_FILE_INFOR)ExAllocatePoolWithTag(
PagedPool,sizeof(MY_FILE_INFOR),MEM_TAG);
if(my_file_infor== NULL)
returnSTATUS_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);
returnSTATUS_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_INFORelem =
CONTAINING_RECORD(p,MY_FILE_INFOR,list_entry);
// 在這裏作須要作的事…
}
}
其中的CONTAINING_RECORD是一個WDK中已經定義的宏,做用是經過一個LIST_ENTRY結構的指針,找到這個結構所在的節點的指針。定義以下:
#defineCONTAINING_RECORD(address, type, field) ((type *)( \
(PCHAR)(address) - \
(ULONG_PTR)(&((type *)0)->field)))
從上面的代碼中能夠總結以下的信息:
LIST_ENTRY中的數據成員Flink指向下一個LIST_ENTRY。
整個鏈表中的最後一個LIST_ENTRY的Flink不是空。而是指向頭節點。
獲得LIST_ENTRY以後,要用CONTAINING_RECORD來獲得鏈表節點中的數據。
2.3 使用長長整型數據
這裏解釋前面碰到的LARGE_INTEGER結構。與可能的誤解不一樣,64位數據並不是要在64位操做系統下才能使用。在VC中,64位數據的類型爲__int64。定義寫法以下:
__int64file_offset;
上面之因此定義的變量名爲file_offset,是由於文件中的偏移量是一種常見的要使用64位數據的狀況。同時,文件的大小也是如此(回憶上一小節中 定義的文件大小)。32位數據無符號整型只能表示到4GB。而衆所周知,如今超過4GB的文件絕對不罕見了。可是實際上__int64這個類型在驅動開發 中不多被使用。基本上被使用到的是一個共用體:LARGE_INTEGER。這個共用體定義以下:
typedef__int64 LONGLONG;
typedefunion _LARGE_INTEGER {
struct{
ULONGLowPart;
LONGHighPart;
};
struct{
ULONGLowPart;
LONGHighPart;
} u;
LONGLONG QuadPart;
}LARGE_INTEGER;
這個共用體的方便之處在於,既能夠很方便的獲得高32位,低32位,也能夠方便的獲得整個64位。進行運算和比較的時候,使用QuadPart便可。
LARGE_INTEGERa,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類型的。
2.4使用自旋鎖
鏈表之類的結構老是涉及到惱人的多線程同步問題,這時候就必須使用鎖。這裏只介紹最簡單的自選鎖。
有些讀者可能疑惑鎖存在的意義。這和多線程操做有關。在驅動開發的代碼中,大可能是存在於多線程執行環境的。就是說可能有幾個線程在同時調用當前函數。
這樣一來,前文1.2.2中說起的追加鏈表節點函數就根本沒法使用了。由於MyFileInforAppendNode這個函數只是簡單的操做鏈表。若是 兩個線程同時調用這個函數來操做鏈表的話:注意這個函數操做的是一個全局變量鏈表。換句話說,不管有多少個線程同時執行,他們操做的都是同一個鏈表。這就 可能發生,一個線程插入一個節點的同時,另外一個線程也同時插入。他們都插入同一個鏈表節點的後邊。這時鏈表就會發生問題。到底最後插入的是哪個呢?要麼 一個丟失了。要麼鏈表被損壞了。
以下的代碼初始化獲取一個自選鎖:
KSPIN_LOCKmy_spin_lock;
KeInitializeSpinLock(&my_spin_lock);
KeInitializeSpinLock這個函數沒有返回值。下面的代碼展現瞭如何使用這個SpinLock。在KeAcquireSpinLock和 KeReleaseSpinLock之間的代碼是隻有單線程執行的。其餘的線程會停留在KeAcquireSpinLock等候。直到 KeReleaseSpinLock被調用。KIRQL是一箇中斷級。KeAcquireSpinLock會提升當前的中斷級。可是目前忽略這個問題。中 斷級在後面講述。
KIRQLirql;
KeAcquireSpinLock(&my_spin_lock,&irql);
// Todo something …
KeReleaseSpinLock(&my_spin_lock,irql);
初學者要注意的是,像下面寫的這樣的「加鎖」代碼是沒有意義的,等於沒加鎖:
voidMySafeFunction()
{
KSPIN_LOCKmy_spin_lock;
KIRQLirql;
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; //鏈表的鎖
// 鏈表初始化函數
voidMyFileInforInilt()
{
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中。
第三章 文件操做
在內核中不能調用用戶層的Win32 API函數來操做文件。在這裏必須改用一系列與之對應的內核函數。
3.1 使用OBJECT_ATTRIBUTES
通常的想法是,打開文件應該傳入這個文件的路徑。可是實際上這個函數並不直接接受一個字符串。使用者必須首先填寫一個OBJECT_ATTRIBUTES 結構。在文檔中並無公開這個OBJECT_ATTRIBUTES結構。這個結構老是被InitializeObjectAttributes初始化。
下面專門說明InitializeObjectAttributes。
VOIDInitializeObjectAttributes(
OUTPOBJECT_ATTRIBUTES InitializedAttributes,
INPUNICODE_STRING ObjectName,
INULONG Attributes,
INHANDLE RootDirectory,
INPSECURITY_DESCRIPTOR SecurityDescriptor);
讀者須要注意的如下的幾點:InitializedAttributes是要初始化的OBJECT_ATTRIBUTES結構的指針。ObjectName則是對象名字字符串。也就是前文所描述的文件的路徑(若是要打開的對象是一個文件的話)。
Attributes則只須要填寫OBJ_CASE_INSENSITIVE| OBJ_KERNEL_HANDLE便可(若是讀者是想要方便的簡潔的打開一個文件的話)。OBJ_CASE_INSENSITIVE意味着名字字符串是 不區分大小寫的。因爲Windows的文件系統原本就不區分字母大小寫,因此筆者並無嘗試過若是不設置這個標記會有什麼後果。 OBJ_KERNEL_HANDLE代表打開的文件句柄一個「內核句柄」。內核文件句柄比應用層句柄使用更方便,能夠不受線程和進程的限制。在任何線程中 均可以讀寫。同時打開內核文件句柄不須要顧及當前進程是否有權限訪問該文件的問題(若是是有安全權限限制的文件系統)。若是不使用內核句柄,則有時不得不 填寫後面的的SecurityDescriptor參數。
RootDirectory用於相對打開的狀況。目前省略。請讀者傳入NULL便可。
SecurityDescriptor用於設置安全描述符。因爲筆者老是打開內核句柄,因此不多設置這個參數。
3.2 打開和關閉文件
下面的函數用於打開一個文件:
NTSTATUSZwCreateFile(
OUTPHANDLE FileHandle,
INACCESS_MASK DesiredAccess,
INPOBJECT_ATTRIBUTES ObjectAttribute,
OUTPIO_STATUS_BLOCK IoStatusBlock,
INPLARGE_INTEGER AllocationSize OPTIONAL,
INULONG FileAttributes,
INULONG ShareAccess,
INULONG CreateDisposition,
INULONG createOptions,
INPVOID EaBuffer OPTIONAL,
INULONG EaLength);
這個函數的參數異常複雜。下面逐個的說明以下:
FileHandle:是一個句柄的指針。若是這個函數調用返回成成功(STATUS_SUCCESS),那就麼打開的文件句柄就返回在這個地址內。
DesiredAccess: 申請的權限。若是打開寫文件內容,請使用FILE_WRITE_DATA。若是須要讀文件內容,請使用FILE_READ_DATA。若是須要刪除文件或 者把文件更名,請使用DELETE。若是想設置文件屬性,請使用FILE_WRITE_ATTRIBUTES。反之,讀文件屬性則使用 FILE_READ_ATTRIBUTES。這些條件能夠用|(位或)來組合。有兩個宏分別組合了經常使用的讀權限和經常使用的寫權限。分別爲 GENERIC_READ和GENERIC_WRITE。此外還有一個宏表明所有權限,是GENERIC_ALL。此外,若是想同步的打開文件,請加上 SYNCHRONIZE。同步打開文件詳見後面對CreateOptions的說明。
ObjectAttribute:對象描述。見前一小節。
IoStatusBlock也是一個結構。這個結構在內核開發中常用。它每每用於表示一個操做的結果。這個結構在文檔中是公開的,以下:
typedefstruct _IO_STATUS_BLOCK {
union {
NTSTATUS Status;
PVOID Pointer;
};
ULONG_PTR Information;
}IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;
實際編程中不多用到Pointer。通常的說,返回的結果在Status中。成功則爲STATUS_SUCCESS。不然則是一個錯誤碼。進一步的信息在 Information中。不一樣的狀況下返回的Information的信息意義不一樣。針對ZwCreateFile調用的狀況,Information 的返回值有如下幾種可能:
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_CREATED:文件被成功的新建了。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_OPENED: 文件被打開了。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_OVERWRITTEN:文件被覆蓋了。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_SUPERSEDED: 文件被替代了。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_EXISTS:文件已存在。(於是打開失敗了)。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_DOES_NOT_EXIST:文件不存在。(於是打開失敗了)。
這些返回值和打開文件的意圖有關(有時但願打開已存在的文件,有時則但願創建新的文件等等。這些意圖在本小節稍後的內容中詳細說明。
ZwCreateFile的下一個參數是AllocationSize。這個參數不多使用,請設置爲NULL。 再接下來的一個參數爲FileAttributes。這個參數控制新創建的文件的屬性。通常的說,設置爲FILE_ATTRIBUTE_NORMAL即 可。在實際編程中,筆者沒有嘗試過其餘的值。
ShareAccess是一個很是容易被人誤解的參數。實際上,這是在本代碼打開這個文件的時候,容許別的代碼同時打開這個文件所持有的權限。因此稱爲共 享訪問。一共有三種共享標記能夠設置:FILE_SHARE_READ、FILE_SHARE_WRITE、FILE_SHARE_DELETE。這三個 標記能夠用|(位或)來組合。舉例以下:若是本次打開只使用了FILE_SHARE_READ,那麼這個文件在本次打開以後,關閉以前,別次打開試圖以讀 權限打開,則被容許,能夠成功打開。若是別次打開試圖以寫權限打開,則必定失敗。返回共享衝突。
同時,若是本次打開只只用了FILE_SHARE_READ,而以前這個文件已經被另外一次打開用寫權限打開着。那麼本次打開必定失敗,返回共享衝突。其中的邏輯關係貌似比較複雜,讀者應耐心理解。
CreateDisposition參數說明了此次打開的意圖。可能的選擇以下(請注意這些選擇不能組合):
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_CREATE:新建文件。若是文件已經存在,則這個請求失敗。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_OPEN:打開文件。若是文件不存在,則請求失敗。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_OPEN_IF:打開或新建。若是文件存在,則打開。若是不存在,則失敗。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_OVERWRITE:覆蓋。若是文件存在,則打開並覆蓋其內容。若是文件不存在,這個請求返回失敗。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_OVERWRITE_IF:新建或覆蓋。若是要打開的文件已存在,則打開它,並覆蓋其內存。若是不存在,則簡單的新建新文件。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_SUPERSEDE:新建或取代。若是要打開的文件已存在。則生成一個新文件替代之。若是不存在,則簡單的生成新文件。
請聯繫上面的IoStatusBlock參數中的Information的說明。
最後一個重要的參數是CreateOptions。在慣常的編程中,筆者使用 FILE_NON_DIRECTORY_FILE|FILE_SYNCHRONOUS_IO_NONALERT。此時文件被同步的打開。並且打開的是文件 (而不是目錄。建立目錄請用FILE_ DIRECTORY_FILE)。所謂同步的打開的意義在於,之後每次操做文件的時候,好比寫入文件,調用ZwWriteFile,在 ZwWriteFile返回時,文件寫操做已經獲得了完成。而不會有返回STATUS_PENDING(未決)的狀況。在非同步文件的狀況下,返回未決是 常見的。此時文件請求沒有完成,使用者須要等待事件來等待請求的完成。固然,好處是使用者能夠先去作別的事情。
要同步打開,前面的DesiredAccess必須含有SYNCHRONIZE。
此外還有一些其餘的狀況。好比不經過緩衝操做文件。但願每次讀寫文件都是直接往磁盤上操做的。此時CreateOptions中應該帶標記 FILE_NO_INTERMEDIATE_BUFFERING。帶了這個標記後,請注意操做文件每次讀寫都必須以磁盤扇區大小(最多見的是512字節) 對齊。不然會返回錯誤。
這個函數是如此的繁瑣,以致於再多的文檔也不如一個能夠利用的例子。早期筆者調用這個函數每每由於參數設置不對而致使打開失敗。很是渴望找到一個實際能夠使用的參數的範例。下面舉例以下:
// 要返回的文件句柄
HANDLEfile_handle = NULL;
// 返回值
NTSTATUSstatus;
// 首先初始化含有文件路徑的OBJECT_ATTRIBUTES
OBJECT_ATTRIBUTESobject_attributes;
UNICODE_STRINGufile_name = RTL_CONST_STRING(L」\\??\\C:\\a.dat」);
InitializeObjectAttributes(
&object_attributes,
&ufile_name,
OBJ_CASE_INSENSITIVE|OBJ_KERNEL_HANDLE,
NULL,
NULL);
// 以OPEN_IF方式打開文件。
status= ZwCreateFile(
&file_handle,
GENERIC_READ | GENERIC_WRITE,
&object_attributes,
&io_status,
NULL,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ,
FILE_OPEN_IF,
FILE_NON_DIRECTORY_FILE |
FILE_RANDOM_ACCESS |
FILE_SYNCHRONOUS_IO_NONALERT,
NULL,
0);
值得注意的是路徑的寫法。並非像應用層同樣直接寫「C:\\a.dat」。而是寫成了「\\??\\C:\\a.dat」。這是由於ZwCreateFile使用的是對象路徑。「C:」是一個符號連接對象。符號連接對象通常都在「\\??\\」路徑下。
這種文件句柄的關閉很是簡單。調用ZwClose便可。內核句柄的關閉不須要和打開在同一進程中。示例以下:
ZwClose(file_handle);
3.3 文件的讀寫操做
打開文件以後,最重要的操做是對文件的讀寫。讀與寫的方法是對稱的。只是參數輸入與輸出的方向不一樣。讀取文件內容通常用ZwReadFile,寫文件通常使用ZwWriteFile。
NTSTATUS
ZwReadFile(
IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID Buffer,
IN ULONG Length,
IN PLARGE_INTEGER ByteOffset OPTIONAL,
INPULONG Key OPTIONAL);
FileHandle:是前面ZwCreateFile成功後所獲得的FileHandle。若是是內核句柄,ZwReadFile和ZwCreateFile並不須要在同一個進程中。句柄是各進程通用的。
Event :一個事件。用於異步完成讀時。下面的舉例始終用同步讀,因此忽略這個參數。請始終填寫NULL。
ApcRoutine Apc:回調例程。用於異步完成讀時。下面的舉例始終用同步讀,因此忽略這個參數。請始終填寫NULL。
IoStatusBlock:返回結果狀態。同ZwCreateFile中的同名參數。
Buffer:緩衝區。若是讀文件的內容成功,則內容被被讀到這個緩衝裏。
Length:描述緩衝區的長度。這個長度也就是試圖讀取文件的長度。
ByteOffset:要讀取的文件的偏移量。也就是要讀取的內容在文件中的位置。通常的說,不要設置爲NULL。文件句柄不必定支持直接讀取當前偏移。
Key:讀取文件時用的一種附加信息,通常不使用。設置NULL。
返 回值:成功的返回值是STATUS_SUCCESS。只要讀取到任意多個字節(無論是否符合輸入的Length的要求),返回值都是 STATUS_SUCCESS。即便試圖讀取的長度範圍超出了文件原本的大小。可是,若是僅讀取文件長度以外的部分,則返回 STATUS_END_OF_FILE。
ZwWriteFile 的參數與ZwReadFile徹底相同。固然,除了讀寫文件外,有的讀者可能會問是否提供一個ZwCopyFile用來拷貝一個文件。這個要求未能被滿 足。若是有這個需求,這個函數必須本身來編寫。下面是一個例子,用來拷貝一個文件。利用到了ZwCreateFile,ZwReadFile和 ZwWrite這三個函數。不過做爲本節的例子,只舉出ZwReadFile和ZwWriteFile的部分:
NTSTATUS MyCopyFile(
PUNICODE_STRING target_path,
PUNICODE_STRING source_path)
{
// 源和目標的文件句柄
HANDLE target = NULL,source = NULL;
// 用來拷貝的緩衝區
PVOID buffer = NULL;
LARGE_INTEGER offset = { 0 };
IO_STATUS_BLOCK io_status = { 0 };
do {
// 這裏請用前一小節說到的例子打開target_path和source_path所對應的
// 句柄target和source,併爲buffer分配一個頁面也就是4k的內存。
… …
// 而後用一個循環來讀取文件。每次從源文件中讀取4k內容,而後往
// 目標文件中寫入4k,直到拷貝結束爲止。
while(1) {
length = 4*1024; //每次讀取4k。
// 讀取舊文件。注意status。
status = ZwReadFile (
source,NULL,NULL,NULL,
&my_io_status,buffer, length,&offset,
NULL);
if(!NT_SUCCESS(status))
{
// 若是狀態爲STATUS_END_OF_FILE,則說明文件
// 的拷貝已經成功的結束了。
if(status == STATUS_END_OF_FILE)
status = STATUS_SUCCESS;
break;
}
// 得到實際讀取到的長度。
length = IoStatus.Information;
// 如今讀取了內容。讀出的長度爲length.那麼我寫入
// 的長度也應該是length。寫入必須成功。若是失敗,
// 則返回錯誤。
status = ZwWriteFile(
target,NULL,NULL,NULL,
&my_io_status,
buffer,length,&offset,
NULL);
if(!NT_SUCCESS(status))
break;
// offset移動,而後繼續。直到出現STATUS_END_OF_FILE
// 的時候才結束。
offset.QuadPart += length;
}
} while(0);
// 在退出以前,釋放資源,關閉全部的句柄。
if(target != NULL)
ZwClose(target);
if(source != NULL)
ZwClose(source);
if(buffer != NULL)
ExFreePool(buffer);
return STATUS_SUCCESS;
}
除了讀寫以外,文件還有不少的操做。好比刪除、從新命名、枚舉。這些操做將在後面實例中用到時,再詳細講解。
第四章 操做註冊表
4.1 註冊鍵的打開操做
和在應用程序中編程的方式相似,註冊表是一個巨大的樹形結構。操做通常都是打開某個子鍵。子鍵下有若干個值能夠得到。每個值有一個名字。值有不一樣的類型。通常須要查詢才能得到其類型。
子鍵通常用一個路徑來表示。和應用程序編程的一點重大不一樣是這個路徑的寫法不同。通常應用編程中須要提供一個根子鍵的句柄。而驅動中則所有用路徑表示。相應的有一張表表示以下:
應用編程中對應的子鍵
HKEY_LOCAL_MACHINE
HKEY_USERS
HKEY_CLASSES_ROOT
HKEY_CURRENT_USER
實際上應用程序和驅動程序很大的一個不一樣在於應用程序老是由某個「當前用戶」啓動的。所以能夠直接讀取HKEY_CLASSES_ROOT和 HKEY_CURRENT_USER。而驅動程序和用戶無關,因此直接去打開HKEY_CURRENT_USER也就不符合邏輯了。
打開註冊表鍵使用函數ZwOpenKey。新建或者打開則使用ZwCreateKey。通常在驅動編程中,使用ZwOpenKey的狀況比較多見。下面以此爲例講解。ZwOpenKey的原型以下:
NTSTATUS
ZwOpenKey(
OUT PHANDLE KeyHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes
);
這個函數和ZwCreateFile是相似的。它並不接受直接傳入一個字符串來表示一個子鍵。而是要求輸入一個OBJECT_ATTRIBUTES的指針。如何初始化一個OBJECT_ATTRIBUTES請參考前面的講解ZwCreateFile的章節。
DesiredAccess支持一系列的組合權限。能夠是下表中全部權限的任何組合:
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
KEY_QUERY_VALUE:讀取鍵下的值。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
KEY_SET_VALUE:設置鍵下的值。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
KEY_CREATE_SUB_KEY:生成子鍵。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
KEY_ENUMERATE_SUB_KEYS:枚舉子鍵。
不過實際上能夠用KEY_READ來作爲通用的讀權限組合。這是一個組合宏。此外對應的有KEY_WRITE。若是須要得到所有的權限,能夠使用KEY_ALL_ACCESS。
下面是一個例子,這個例子很是的有實用價值。它讀取註冊表中保存的Windows系統目錄(指Windows目錄)的位置。不過這裏只涉及打開子鍵。並不讀取值。讀取具體的值在後面的小節中再完成。
Windows目錄的位置被稱爲SystemRoot,這一值保存在註冊表中,路徑是「HKEY_LOCAL_MACHINE\SOFTWARE \Microsoft\WindowsNT\CurrentVersion」。固然,請注意注意在驅動編程中的寫法有所不一樣。下面的代碼初始化一個 OBJECT_ATTRIBUTES。
HANDLEmy_key = NULL;
NTSTATUSstatus;
// 定義要獲取的路徑
UNICODE_STRINGmy_key_path =
RTL_CONSTANT_STRING(
L」\\ Registry\\Machine\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion」);
OBJECT_ATTRIBUTEmy_obj_attr = { 0 };
// 初始化OBJECT_ATTRIBUTE
InitializeObjectAttributes(
&my_obj_attr,
&my_key_path,
OBJ_CASE_INSENSITIVE,
NULL,
NULL);
// 接下來是打開Key
status= ZwOpenKey(&my_key,KEY_READ,&my_obj_attr);
if(!NT_SUCCESS(status))
{
// 失敗處理
……
}
上面的代碼獲得了my_key。子鍵已經打開。而後的步驟是讀取下面的SystemRoot值。這在後面一個小節中講述。
4.2 註冊值的讀
通常使用ZwQueryValueKey來讀取註冊表中鍵的值。要注意的是註冊表中的值可能有多種數據類型。並且長度也是沒有定數的。爲此,在讀取過程當中,就可能要面對不少種可能的狀況。ZwQueryValueKey這個函數的原型以下:
NTSTATUSZwQueryValueKey(
INHANDLE KeyHandle,
INPUNICODE_STRING ValueName,
INKEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,
OUTPVOID KeyValueInformation,
INULONG Length,
OUTPULONG ResultLength
);
KeyHandle:這是用ZwCreateKey或者ZwOpenKey所打開的一個註冊表鍵句柄。
ValueName:要讀取的值的名字。
KeyValueInformationClass:本次查詢所須要查詢的信息類型。這有以下的三種可能。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
KeyValueBasicInformation:得到基礎信息,包含值名和類型。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
KeyValueFullInformation:得到完整信息。包含值名、類型和值的數據。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
KeyValuePartialInformation:得到局部信息。包含類型和值數據。
很容易看出實際上名字是已知的,得到基礎信息是畫蛇添足。一樣得到完整信息也是浪費內存空間。由於調用ZwQueryValueKey的目的是爲了獲得類 型和值數據。所以使用KeyValuePartialInformation最多見。當採用KeyValuePartialInformation的時 候,一個類型爲KEY_VALUE_PARTIAL_INFORMATION的結構將被返回到參數KeyValueInformation所指向的內存 中。
KeyValueInformation:當KeyValueInformationClass被設置爲 KeyValuePartialInformation時,KEY_VALUE_PARTIAL_INFORMATION結構將被返回到這個指針所指內存 中。下面是結構KEY_VALUE_PARTIAL_INFORMATION的原型。
typedefstruct _KEY_VALUE_PARTIAL_INFORMATION {
ULONG TitleIndex; //請忽略這個成員
ULONG Type; //數據類型
ULONG DataLength; //數據長度
UCHAR Data[1]; // 可變長度的數據
}KEY_VALUE_PARTIAL_INFORMATION,*PKEY_VALUE_PARTIAL_INFORMATIO;
上面的數據類型Type有不少種可能,可是最多見的幾種以下:
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
REG_BINARY:十六進制數據。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
REG_DWORD:四字節整數。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
REG_SZ:以空結束的Unicode字符串。
Length:用戶傳入的輸出空間KeyValueInformation的長度。
ResultLength:返回實際須要的長度。
返回值:若是說實際須要的長度比Length要大,那麼返回STATUS_BUFFER_OVERFLOW或者是STATUS_BUFFER_TOO_SMALL。若是成功讀出了所有數據,那麼返回STATUS_SUCCESS。其餘的狀況,返回一個錯誤碼。
下面請讀者考慮如何把上一小節的函數寫完整。這其中比較常見的一個問題是在讀取註冊表鍵下的值以前,每每不知道這個值有多長。因此有些比較偷懶的程序員總 是定義一個足夠的大小的空間(好比512字節)。這樣的壞處是浪費內存(通常都是在堆棧中定義,而內核編程中堆棧空間被耗盡又是另外一個常見的藍屏問題)。 此外也沒法避免值實際上大於該長度的狀況。爲此應該耐心的首先獲取長度,而後不足時再動態分配內存進行讀取。下面是示例代碼:
// 要讀取的值的名字
UNICODE_STRINGmy_key_name =
RTL_CONSTANT_STRING(L」SystemRoot」);
// 用來試探大小的key_infor
KEY_VALUE_PARTIAL_INFORMATIONkey_infor;
// 最後實際用到的key_infor指針。內存分配在堆中
PKEY_VALUE_PARTIAL_INFORMATIONac_key_infor;
ULONGac_length;
……
// 前面已經打開了句柄my_key,下面如此來讀取值:
status= ZwQueryValueKey(
my_key,
&my_key_name,
KeyValuePartialInformation,
&key_infor,
sizeof(KEY_VALUE_PARTIAL_INFORMATION),
&ac_length);
if(!NT_SUCCESS(status)&&
status!= STATUS_BUFFER_OVERFLOW &&
status!= STATUS_BUFFER_TOO_SMALL)
{
// 錯誤處理
…
}
// 若是沒失敗,那麼分配足夠的空間,再次讀取
ac_key_infor= (PKEY_VALUE_PARTIAL_INFORMATION)
ExAllocatePoolWithTag(NonpagedPool,ac_length,MEM_TAG);
if(ac_key_infor== NULL)
{
stauts= STATUS_INSUFFICIENT_RESOURCES;
// 錯誤處理
…
}
status= ZwQueryValueKey(
my_key,
&my_key_name,
KeyValuePartialInformation,
ac_key_infor,
ac_length,
&ac_length);
// 到此爲止,若是status爲STATUS_SUCCESS,則要讀取的數據已經
// 在ac_key_infor->Data中。請利用前面學到的知識,轉換爲
//UNICODE_STRING
……
4.3 註冊值的寫
實際上註冊表的寫入比讀取要簡單。由於這省略了一個嘗試數據的大小的過程。直接將數據寫入便可。寫入值通常使用函數ZwSetValueKey 。這個函數的原型以下:
NTSTATUSZwSetValueKey(
IN HANDLE KeyHandle,
IN PUNICODE_STRING ValueName,
IN ULONG TitleIndex OPTIONAL,
IN ULONG Type,
IN PVOID Data,
IN ULONG DataSize
);
其中的TileIndex參數請始終填入0。
KeyHandle、ValueName、Type這三個參數和ZwQueryValueKey中對應的參數相同。不一樣的是Data和DataSize。 Data是要寫入的數據的開始地址,而DataSize是要寫入的數據的長度。因爲Data是一個空指針,所以,Data能夠指向任何數據。也就是說,不 管Type是什麼,均可以在Data中填寫相應的數據寫入。
ZwSetValueKey的時候,並不須要該Value已經存在。若是該Value已經存在,那麼其值會被此次寫入覆蓋。若是不存在,則會新建一個。下 面的例子寫入一個名字爲「Test」,並且值爲「My Test Value」的字符串值。假設my_key是一個已經打開的子鍵的句柄。
UNICODE_STRINGname = RTL_CONSTANT_STRING(L」Test」);
PWCHARvalue = { L」My Test Value」 };
…
// 寫入數據。數據長度之因此要將字符串長度加上1,是爲了把最後一個空結束符
// 寫入。我不肯定若是不寫入空結束符會不會有錯,有興趣的讀者請本身測試一下。
status= ZwSetValueKey(my_key,
&name,0,REG_SZ,value,(wcslen(value)+1)*sizeof(WCHAR));
if(!NT_SUCCESS(status))
{
// 錯誤處理
……
}
關於註冊表的操做就介紹到這裏了。若是有進一步的需求,建議讀者閱讀WDK相關的文檔。
第五章 時間與定時器
5.1 得到當前滴答數
在編程中,得到當前的系統日期和時間,或者是得到一個從啓動開始的毫秒數,是很常見的需求。得到系統日期和時間每每是爲了寫日誌。得到啓動毫秒數很適合用來作一個隨機數的種子。有時也使用時間相關的函數來尋找程序的性能瓶頸。
熟悉Win32應用程序開發的讀者會知道有一個函數GetTickCount(),這個函數返回系統自啓動以後經歷的毫秒數。在驅動開發中有一個對應的函數KeQueryTickCount(),這個函數的原型以下:
VOID
KeQueryTickCount(
OUTPLARGE_INTEGER TickCount
);
遺憾的是,被返回到TickCount中的並非一個簡單的毫秒數。這是一個「滴答」數。可是一個「滴答」到底爲多長的時間,在不一樣的硬件環境下可能有所不一樣。爲此,必須結合另外一個函數使用。下面這個函數得到一個「滴答」的具體的100納秒數。
ULONG
KeQueryTimeIncrement(
);
得知以上的關係以後,下面的代碼能夠求得實際的毫秒數:
void MyGetTickCount(PULONG msec)
{
LARGE_INTEGER tick_count;
ULONG myinc = KeQueryTimeIncrement();
KeQueryTickCount(&tick_count);
tick_count.QuadPart *= myinc;
tick_count.QuadPart /= 10000;
*msec = tick_count.LowPart;
}
這不是一個簡單的過程。不過所幸的是,如今有代碼能夠拷貝了。
5.2 得到當前系統時間
接下來的一個需求是獲得當前的能夠供人類理解的時間。包括年、月、日、時、分、秒這些要素。在驅動中不能使用諸如CTime之類的MFC類。不過與之對應的有TIME_FIELDS,這個結構中含有對應的時間要素。
KeQuerySystemTime()獲得當前時間。可是獲得的並非當地時間,而是一個格林威治時間。以後請使用ExSystemTimeToLocalTime()轉換能夠當地時間。這兩個函數的原型以下:
VOID
KeQuerySystemTime(
OUT PLARGE_INTEGER CurrentTime
);
VOID
ExSystemTimeToLocalTime(
IN PLARGE_INTEGER SystemTime,
OUT PLARGE_INTEGER LocalTime
);
這兩個函數使用的「時間」都是長長整型數據結構。這不是人類能夠閱讀的。必須經過函數RtlTimeToTimeFields轉換爲TIME_FIELDS。這個函數原型以下:
VOID
RtlTimeToTimeFields(
IN PLARGE_INTEGER Time,
IN PTIME_FIELDS TimeFields
);
讀者須要實際應用一下來加深印象。下面寫出一個函數:這個函數返回一個字符串。這個字符串寫出當前的年、月、日、時、分、秒,這些數字之間用「-」號隔開。這是一個頗有用的函數。並且同時用到上面三個函數,此外,請讀者回憶前面關於字符串的打印的相關章節。
{
LARGE_INTEGER snow,now;
TIME_FIELDS now_fields;
static WCHAR time_str[32] = { 0 };
// 得到標準時間
KeQuerySystemTime(&snow);
// 轉換爲當地時間
ExSystemTimeToLocalTime(&snow,&now);
// 轉換爲人類能夠理解的時間要素
RtlTimeToTimeFields(&now,&now_fields);
// 打印到字符串中
RtlStringCchPrintfW(
time_str,
32*2,
L"%4d-%2d-%2d %2d-%2d-%2d",
now_fields.Year,now_fields.Month,now_fields.Day,
now_fields.Hour,now_fields.Minute,now_fields.Second);
return time_str;
}
請注意time_str是靜態變量。這使得這個函數不具有多線程安全性。請讀者考慮一下,如何保證多個線程同時調用這個函數的時候,不出現衝突?
5.3 使用定時器
使用過Windows應用程序編程的讀者的讀者必定對SetTimer()映像尤深。當須要定時執行任務的時候,SetTimer()變得很是重要。這個 功能在驅動開發中能夠經過一些不一樣的替代方法來實現。比較經典的對應是KeSetTimer(),這個函數的原型以下:
BOOLEAN
KeSetTimer(
IN PKTIMER Timer, // 定時器
IN LARGE_INTEGER DueTime, // 延後執行的時間
IN PKDPC Dpc OPTIONAL // 要執行的回調函數結構
);
其中的定時器Timer和要執行的回調函數結構Dpc都必須先初始化。其中Timer的初始化比較簡單。下面的代碼能夠初始化一個Timer:
KTIMERmy_timer;
KeInitializeTimer(&my_timer);
Dpc的初始化比較麻煩。這是由於須要提供一個回調函數。初始化Dpc的函數原型以下:
VOID
KeInitializeDpc(
IN PRKDPC Dpc,
IN PKDEFERRED_ROUTINE DeferredRoutine,
IN PVOID DeferredContext
);
PKDEFERRED_ROUTINE這個函數指針類型所對應的函數的類型其實是這樣的:
VOID
CustomDpc(
IN struct _KDPC *Dpc,
IN PVOID DeferredContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2
);
讀者須要關心的只是DeferredContext。這個參數是KeInitializeDpc調用時傳入的參數。用來提供給CustomDpc被調用的時候,讓用戶傳入一些參數。
至於後面的SystemArgument1和SystemArgument2則請不要理會。Dpc是回調這個函數的KDPC結構。
請注意這是一個「延時執行」的過程。而不是一個定時執行的過程。所以每次執行了以後,下次就不會再被調用了。若是想要定時反覆執行,就必須在每次CustomDpc函數被調用的時候,再次調用KeSetTimer,來保證下次還能夠執行。
值得注意的是,CustomDpc將運行在APC中斷級。所以並非全部的事情均可以作(在調用任何內核系統函數的時候,請注意WDK說明文檔中標明的中斷級要求。)
這些事情很是的煩惱,所以要徹底實現定時器的功能,須要本身封裝一些東西。下面的結構封裝了所有須要的信息:
// 內部時鐘結構
typedef struct MY_TIMER_
{
KDPC dpc;
KTIMER timer;
PKDEFERRED_ROUTINE func;
PVOID private_context;
}MY_TIMER,*PMY_TIMER;
// 初始化這個結構:
voidMyTimerInit(PMY_TIMER timer, PKDEFERRED_ROUTINE func)
{
//請注意,我把回調函數的上下文參數設置爲timer,爲何要
//這樣作呢?
KeInitializeDpc(&timer->dpc,sf_my_dpc_routine,timer);
timer->func = func;
KeInitializeTimer(&timer->timer);
return (wd_timer_h)timer;
}
// 讓這個結構中的回調函數在n毫秒以後開始運行:
BOOLEAN MyTimerSet(PMY_TIMER timer,ULONG msec,PVOID context)
{
LARGE_INTEGER due;
//注意時間單位的轉換。這裏msec是毫秒。
due.QuadPart = -10000*msec;
//用戶私有上下文。
timer->private_context = context;
return KeSetTimer(&timer->timer,due,&mytimer->dpc);
};
// 中止執行
VOIDMyTimerDestroy(PMY_TIMER timer)
{
KeCancelTimer(&mytimer->timer);
};
使用結構PMY_TIMER已經比結合使用KDPC和KTIMER簡便許多。可是仍是有一些要注意的地方。真正的OnTimer回調函數中,要得到上下 文,必需要從timer->private_context中得到。此外,OnTimer中還有必要再次調用MyTimerSet(),來保證下次 依然獲得執行。
VOID
MyOnTimer (
IN struct _KDPC *Dpc,
IN PVOID DeferredContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2
)
{
//這裏傳入的上下文是timer結構,用來下次再啓動延時調用
PMY_TIMER timer = (PMY_TIMER)DeferredContext;
//得到用戶上下文
PVOID my_context = timer->private_context;
//在這裏作OnTimer中要作的事情
……
//再次調用。這裏假設每1秒執行一次
MyTimerSet(timer,1000,my_context);
};
關於定時器就介紹到這裏了。
第六章 內核線程
6.1 使用線程
有時候須要使用線程來完成一個或者一組任務。這些任務可能耗時過長,而開發者又不想讓當前系統中止下來等待。在驅動中中止等待很容易使整個系統陷入「停 頓」,最後可能只能重啓電腦。但一個單獨的線程長期等待,還不至於對系統形成致命的影響。另外一些任務是但願長期、不斷的執行,好比不斷寫入日誌。爲此啓動 一個特殊的線程來執行它們是最好的方法。
在驅動中生成的線程通常是系統線程。系統線程所在的進程名爲「System」。用到的內核API函數原型以下:
NTSTATUS
PsCreateSystemThread(
OUT PHANDLE ThreadHandle,
IN ULONG DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle OPTIONAL,
OUT PCLIENT_ID ClientId OPTIONAL,
IN PKSTART_ROUTINE StartRoutine,
IN PVOID StartContext);
這個函數的參數也不少。不過做者本人的使用經驗以下:ThreadHandle用來返回句柄。放入一個句柄指針便可。DesiredAccess老是填寫 0。後面三個參數都填寫NULL。最後的兩個參數一個用於改線程啓動的時候執行的函數。一個用於傳入該函數的參數。
下面要關心的就是那個啓動函數的原型。這個原型比起定時器回調函數卻是異常的簡單,沒有任何多餘的東西:
VOID CustomThreadProc(INPVOID context)
能夠傳入一個參數,就是那個context。context就是PsCreateSystemThread中的StartContext。值得注意的是, 線程的結束應該在線程中本身調用PsTerminateSystemThread來完成。此外獲得的句柄也必需要用ZwClose來關閉。可是請注意:關 閉句柄並不結束線程。
下面舉一個例子。這個例子傳遞一個字符串指針到一個線程中打印一下。而後結束該線程。固然打印字符串這種事情沒有必要單獨開一個線程來作。這裏只是一個簡單的示例。請注意,這個代碼中有一個隱藏的錯誤,請讀者指出這個錯誤是什麼:
// 個人線程函數。傳入一個參數,這個參數是一個字符串。
VOIDMyThreadProc(PVOID context)
{
PUNICODE_STRINGstr = (PUNICODE_STRING)context;
// 打印字符串
KdPrint((「PrintInMyThread:%wZ\r\n」,str));
// 結束本身。
PsTerminateSystemThread(STATUS_SUCCESS);
}
VOIDMyFunction()
{
UNICODE_STRINGstr = RTL_CONSTANT_STRING(L「Hello!」);
HANDLEthread = NULL;
NTSTATUSstatus;
status= PsCreateSystemThread(
&thread,0L,NULL,NULL,NULL,MyThreadProc,(PVOID)&str);
if(!NT_SUCCESS(status))
{
//錯誤處理。
…
}
// 若是成功了,能夠繼續作本身的事。以後獲得的句柄要關閉
ZwClose(thread);
}
以上錯誤之處在於:MyThreadProc執行的時候,MyFunction可能已經執行完畢了。執行完畢以後,堆棧中的str已經無效。此時再執行KdPrint去打印str必定會藍屏。這也是一個很是隱蔽,可是很是容易犯下的錯誤。
合理的方法是是在堆中分配str的空間。或者str必須在全局空間中。請讀者本身寫出正確的方法。
可是讀者會發現,以上的寫法在正確的代碼中也是常見的。緣由是這樣作的時候,在PsCreateSystemThread結束以後,開發者會在後面加上一個等待線程結束的語句。
這樣就沒有任何問題了,由於在這個線程結束以前,這個函數都不會執行完畢,因此棧內存空間不會失效。
這樣作的目的通常不是爲了讓任務併發。而是爲了利用線程上下文環境而作的特殊處理。好比防止重入等等。在後面的章節讀者會學到這方面的技巧。
如何等待線程結束在後面1.6.3「使用事件通知」中進一步的講述。
6.2 在線程中睡眠
許多讀者必定使用過Sleep函數。這能使程序停下一段時間。許多須要連續、長期執行,可是又不但願佔太多CPU使用率的任務,能夠在中間加入睡眠。這樣能使CPU使用率大大下降。即便睡眠的時間很是短(幾十個毫秒)。
在驅動中也能夠睡眠。使用到的內核函數的原型以下:
NTSTATUS
KeDelayExecutionThread(
IN KPROCESSOR_MODE WaitMode,
IN BOOLEAN Alertable,
IN PLARGE_INTEGER Interval);
這個函數的參數簡單明瞭。WaitMode請老是填寫KernelMode,由於如今是在內核編程中使用。Alertable表示是否容許線程報警(用於 從新喚醒)。可是目前沒有必要用到這麼高級的功能,請老是填寫FALSE。剩下的就是Interval了,代表要睡眠多久。
可是這個看似簡單的參數說明起來卻異常的複雜。爲此做者建議讀者使用下面簡單的睡眠函數,這個函數能夠指定睡眠多少毫秒,而沒有必要本身去換算時間(這個函數中有睡眠時間的轉換):
#defineDELAY_ONE_MICROSECOND (-10)
#defineDELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000)
VOIDMySleep(LONG msec)
{
LARGE_INTEGERmy_interval;
my_interval.QuadPart= DELAY_ONE_MILLISECOND;
my_interval.QuadPart*= msec;
KeDelayExecutionThread(KernelMode,0,&my_interval);
}
固然要睡眠幾秒也是能夠的,1毫秒爲千分之一秒。因此乘以1000就能夠表示秒數。
在一個線程中用循環進行睡眠,也能夠實現一個本身的定時器。考慮前面說的定時器的缺點:中斷級較高,有一些事情不能作。在線程中用循環睡眠,每次睡眠結束 以後調用本身的回調函數,也能夠起到相似的效果。並且系統線程執行中是Passive中斷級。睡眠以後依然是這個中斷級,因此不像前面提到的定時器那樣有 限制。
請讀者本身寫出用線程+睡眠來實現定時器的例子。
6.3 使用事件通知
一些讀者可能熟悉「事件驅動」編程技術。可是這裏的「事件」與之不一樣。內核中的事件是一個數據結構。這個結構的指針能夠看成一個參數傳入一個等待函數中。 若是這個事件不被「設置」,則這個等待函數不會返回,這個線程被阻塞。若是這個事件被「設置」,則等待結束,能夠繼續下去。
這經常用於多個線程之間的同步。若是一個線程須要等待另外一個線程完成某過後才能作某事,則能夠使用事件等待。另外一個線程完成後設置事件便可。
這個數據結構是KEVENT。讀者沒有必要去了解其內部結構。這個結構老是用KeInitlizeEvent初始化。這個函數原型以下:
VOID
KeInitializeEvent(
IN PRKEVENT Event,
IN EVENT_TYPE Type,
IN BOOLEAN State
);
第一個參數是要初始化的事件。第二個參數是事件類型,這個詳見於後面的解釋。第三個參數是初始化狀態。通常的說設置爲FALSE。也就是未設狀態。這樣等待者須要等待設置以後才能經過。
事件不須要銷燬。
設置事件使用函數KeSetEvent。這個函數原型以下:
LONG
KeSetEvent(
IN PRKEVENT Event,
IN KPRIORITY Increment,
IN BOOLEAN Wait
);
Event是要設置的事件。Increment用於提高優先權。目前設置爲0便可。Wait表示是否後面立刻緊接着一個KeWaitSingleObject來等待這個事件。通常設置爲TRUE。(事件初始化以後,通常就要開始等待了。)
使用事件的簡單代碼以下:
// 定義一個事件
KEVENTevent;
// 事件初始化
KeInitializeEvent(&event,SynchronizationEvent,TRUE);
……
// 事件初始化以後就能夠使用了。在一個函數中,你能夠等待某
// 個事件。若是這個事件沒有被人設置,那就會阻塞在這裏繼續
// 等待。
KeWaitForSingleObject(&event,Executive,KernelMode,0,0);
……
// 這是另外一個地方,有人設置這個事件。只要一設置這個事件,
// 前面等待的地方,將繼續執行。
KeSetEvent(&event);
因爲在KeInitializeEvent中使用了SynchronizationEvent,致使這個事件成爲所謂的「自動重設」事件。一個事件若是被 設置,那麼全部KeWaitForSingleObject等待這個事件的地方都會經過。若是要能繼續重複使用這個時間,必須重設(Reset)這個事 件。當KeInitializeEvent中第二個參數被設置爲NotificationEvent的時候,這個事件必需要手動重設才能使用。手動重設使 用函數KeResetEvent。
LONG
KeResetEvent(
IN PRKEVENT Event
);
若是這個事件初始化的時候是SynchronizationEvent事件,那麼只有一個線程的KeWaitForSingleObject能夠經過。通 過以後被自動重設。那麼其餘的線程就只能繼續等待了。這能夠起到一個同步做用。因此叫作同步事件。不能起到同步做用的是通知事件 (NotificationEvent)。請注意不能用手工設置通知事件的方法來取代同步事件。請讀者思考一下這是爲何。
回憶前面的1.6.1 「使用線程」的最後的例子。在那裏曾經有一個需求:就是等待線程中的函數KdPrint結束以後,外面生成線程的函數再返回。 這能夠經過一個事件來實 現:線程中打印結束以後,設置事件。外面的函數再返回。爲了編碼簡單我使用了一個靜態變量作事件。這種方法在線程同步中用得極多,請務必熟練掌握:
staticKEVENT s_event;
// 個人線程函數。傳入一個參數,這個參數是一個字符串。
VOIDMyThreadProc(PVOID context)
{
PUNICODE_STRINGstr = (PUNICODE_STRING)context;
KdPrint((「PrintInMyThread:%wZ\r\n」,str));
KeSetEvent(&s_event); // 在這裏設置事件。
PsTerminateSystemThread(STATUS_SUCCESS);
}
// 生成線程的函數:
VOIDMyFunction()
{
UNICODE_STRINGstr = RTL_CONSTANT_STRING(L「Hello!」);
HANDLEthread = NULL;
NTSTATUSstatus;
KeInitializeEvent(&event,SynchronizationEvent,TRUE); // 初始化事件
status= PsCreateSystemThread(
&thread,0L,NULL,NULL,NULL,MyThreadProc,(PVOID)&str);
if(!NT_SUCCESS(status))
{
//錯誤處理。
…
}
ZwClose(thread);
// 等待事件結束再返回:
KeWaitForSingleObject(&s_event,Executive,KernelMode,0,0);
}
實際上等待線程結束並不必定要用事件。線程自己也能夠看成一個事件來等待。可是這裏爲了演示事件的用法而使用了事件。以上的方法調用線程則沒必要擔憂str 的內存空間會無效了。由於這個函數在線程執行完KdPrint以後才返回。缺點是這個函數不能起到併發執行的做用。
第七章 驅動與設備
7.1 驅動入口與驅動對象
驅動開發程序員所編寫的驅動程序對應有一個結構。這個結構名爲DRIVER_OBJECT。對應一個「驅動程序」。下面的代碼展現的是一個最簡單的驅動程序。
#include<ntddk.h>
NTSTATUS
DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
NTSTATUSstatus = STATUS_UNSUCCESSFUL;
return status;
}
函數DriverEntry是每一個驅動程序中必須的。如同Win32應用程序裏的WinMain。DriverEntry的第一個參數就是一個 DRIVER_OBJECT的指針。這個DRIVER_OBJECT結構就對應當前編寫的驅動程序。其內存是Windows系統已經分配的。
第二個參數RegistryPath是一個字符串。表明一個註冊表子鍵。這個子鍵是專門分配給這個驅動程序使用的。用於保存驅動配置信息到註冊表中。至於讀寫註冊表的方法,請參照前面章節中的內容。
DriverEntry的返回值決定這個驅動的加載是否成功。若是返回爲STATUS_SUCCESS,則驅動將成功加載。不然,驅動加載失敗。
7.2 分發函數與卸載函數
DRIVER_OBJECT中含有分發函數指針。這些函數用來處理髮到這個驅動的各類請求。Windows老是本身調用DRIVER_OBJECT下的分發函數來處理這些請求。因此編寫一個驅動程序,本質就是本身編寫這些處理請求的分發函數。
DRIVER_OBJECT下的分發函數指針的個數爲IRP_MJ_MAXIMUM_FUNCTION。保存在一個數組中。下面的代碼設置全部分發函數的地址爲同一個函數:
NTSTATUS
DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
ULONGi;
for(i=0;i<IRP_MJ_MAXIMUM_FUNCTION;++i)
{
DriverObject->MajorFunctions= MyDispatchFunction;
}
…
}
這個設置當然不難。難的工做都在編寫MyDispatchFunction這個函數上。由於全部的分發函數指針都指向這一個函數,那麼這個函數固然要完成本驅動全部的功能。下面是這個函數的原型。這個原型是Windows驅動編程的規範,不能更改:
NTSTATUSMyDispatchFunction(PDEVICE_OBJECT device,PIRP irp)
{
……
}
這裏出現了DEVICE_OBJECT和IRP這兩大結構。前一個表示一個由本驅動生成的設備對象。後一個表示一個系統請求。也就是說,如今要處理的是:發給設備device的請求irp。請完成這個處理吧。這兩個結構在後面再進一步描述。
還有一個不放在分發函數數組中的函數,稱爲卸載函數也很是重要。若是存在這個函數,則該驅動程序能夠動態卸載。在卸載時,該函數會被執行。該函數原型以下:
VOIDMyDriverUnload(PDRIVER_OBJECT driver)
{
……
}
這個函數的地址設置到DriverObject->DriverUnload便可。
因爲沒有返回值,因此實際上在DriverUnload中,已經沒法決定這個驅動可否卸載。只能作善後處理。
7.3 設備與符號連接
驅動程序和系統其餘組件之間的交互是經過給設備發送或者接受發給設備的請求來交互的。換句話說,一個沒有任何設備的驅動是不能按規範方式和系統交互的。固然也不會收到任何IRP,分發函數也失去了意義。
但並不意味着這樣的驅動程序不存在。若是一個驅動程序只是想寫寫日誌文件、Hook某些內核函數或者是作一些其餘的小動做,也能夠不生成任何設備,也不須要關心分發函數的設置。
若是驅動程序要和應用程序之間通訊,則應該生成設備。此外還必須爲設備生成應用程序能夠訪問的符號連接。下面的驅動程序生成了一個設備,並設置了分發函數:
#include<ntifs.h> // 之因此用ntifs.h而不是ntddk.h是由於我習慣開發文件
//系統驅動,實際上目前對讀者來講這兩個頭文件沒區別。
NTSTATUSDriverEntry(
PDRIVER_OBJECTdriver,
PUNICODE_STRINGreg_path)
{
NTSTATUS status;
PDEVICE_OBJECT device;
// 設備名
UNICODE_STRINGdevice_name =
RTL_CONSTANT_STRING("\\Device\\MyCDO");
// 符號連接名
UNICODE_STRINGsymb_link =
RTL_CONSTANT_STRING("\\DosDevices\\MyCDOSL");
// 生成設備對象
status= IoCreateDevice(
driver,
0,
device_name,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&device);
// 若是不成功,就返回。
if(!NT_SUCCESS(status))
returnstatus;
// 生成符號連接
status= IoCreateSymbolicLink(
&symb_link,
&device_name);
if(!NT_SUCCESS(status))
{
IoDeleteDevice(device);
returnstatus;
}
// 設備生成以後,打開初始化完成標記
device->Flags&= ~DO_DEVICE_INITIALIZING;
returnstatus;
}
這個驅動成功加載以後,生成一個名叫「\Device\MyCDO」的設備。而後在給這個設備生成了一個符號連接名字叫作「\DosDevices \MyCDOSL」。應用層能夠經過打開這個符號連接來打開設備。應用層能夠調用CreateFile就像打開文件同樣打開。只是路徑應該是「"\\.\ MyCDOSL」。前面的「\\.\」意味後面是一個符號連接名,而不是一個普通的文件。請注意,因爲C語言中斜槓要雙寫,因此正確的寫法應該是「 \\\\.\\」。與應用層交互的例子在下一節「IRP和IO_STACK_LOCATION」中。
7.4 設備的生成安全性限制
上一節的例子只是簡單的例子。不少狀況下那些代碼會不起做用。爲了不讀者在實際編程中遇到哪些特殊狀況的困繞,下面詳細說明生成設備和符號連接須要注意的地方。生成設備的函數原型以下:
NTSTATUS
IoCreateDevice(
IN PDRIVER_OBJECT DriverObject,
IN ULONG DeviceExtensionSize,
IN PUNICODE_STRING DeviceName OPTIONAL,
IN DEVICE_TYPE DeviceType,
IN ULONG DeviceCharacteristics,
IN BOOLEAN Exclusive,
OUT PDEVICE_OBJECT *DeviceObject
);
這個函數的參數也很是複雜。可是實際上須要注意的並很少。
第一個參數是生成這個設備的驅動對象。
第二個參數DeviceExtensionSize很是重要。因爲分發函數中獲得的老是設備的指針。當用戶須要在每一個設備上記錄一些額外的信息(好比用於 判斷這個設備是哪一個設備的信息、以及不一樣的實際設備所須要記錄的實際信息,好比網卡上數據包的流量、過濾器所綁定真實設備指針等等),須要指定的設備擴展 區內存的大小。若是DeviceExtensionSize設置爲非0,IoCreateDevice會分配這個大小的內存在 DeviceObject->DeviceExtension中。之後用戶就能夠從根據DeviceObject-> DeviceExtension來得到這些預先保存的信息。
DeviceName如前例,是設備的名字。目前生成設備,請老是生成在\Device\目錄下。因此前面寫的名字是「\Device\MyCDO」。其餘路徑也是能夠的,可是這在本書描述範圍以外。
DeviceType表示設備類型。目前的範例無所謂設備類型,因此填寫FILE_DEVICE_UNKNOWN便可。
DeviceCharacteristics目前請簡單的填寫0便可。
Exclusive這個參數必須設置FALSE。文檔沒有作任何解釋。
最後生成的設備對象指針返回到DeviceObject中。
這種設備生成以後,必須有系統權限的用戶才能打開(好比管理員)。因此若是編程者寫了一個普通的用戶態的應用程序去打開這個設備進行交互,那麼不少狀況下能夠(用管理員登陸的時候)。可是偶爾又不行(用普通用戶登陸的時候)。結果困繞好久。實際上是權限問題。
爲了保證交互的成功與安全性,應該用服務程序與之交互。
可是依然有時候必須用普通用戶打開設備。爲了這個目的,設備必須是對全部的用戶開放的。此時不能用IoCreateDevice。必須用IoCreateDeviceSecure。這個函數的原型以下:
NTSTATUS
IoCreateDeviceSecure(
IN PDRIVER_OBJECT DriverObject,
IN ULONG DeviceExtensionSize,
IN PUNICODE_STRING DeviceName OPTIONAL,
IN DEVICE_TYPE DeviceType,
IN ULONG DeviceCharacteristics,
IN BOOLEAN Exclusive,
IN PCUNICODE_STRING DefaultSDDLString,
IN LPCGUID DeviceClassGuid,
OUT PDEVICE_OBJECT *DeviceObject
)
這個函數增長了兩個參數(其餘的沒變)。一個是DefaultSDDLString。這個一個用於描述權限的字符串。描述這個字符串的格式須要大量的篇幅。可是沒有這個必要。字符串「D:P(A;;GA;;;WD)」將知足「人人皆能夠打開」的需求。
另外一個參數是一個設備的GUID。請隨機手寫一個GUID。不要和其餘設備的GUID衝突(不要複製粘貼便可)。
下面是例子:
// 隨機手寫一個GUID
const GUIDDECLSPEC_SELECTANY MYGUID_CLASS_MYCDO =
{0x26e0d1e0L, 0x8189, 0x12e0, {0x99,0x14, 0x08,0x00, 0x22, 0x30, 0x19, 0x03}};
// 全用戶可讀寫權限
UNICODE_STRING sddl =
RLT_CONSTANT_STRING(L"D:P(A;;GA;;;WD)");
// 生成設備
status =IoCreateDeviceSecure( DriverObject,
0,
&device_name,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&sddl,
(LPCGUID)&SFGUID_CLASS_MYCDO,
&device);
使用這個函數的時候,必須鏈接庫wdmsec.lib。
7.5 符號連接的用戶相關性
從前面的例子看,符號連接的命名貌似很簡單。簡單的符號連接(之因此稱爲簡單,是由於還有一種使用GUID的符號連接,這在本書討論範圍以外)老是命名在\DosDevices\之下。可是實際上這會有一些問題。
比較高級的Windows系統(哪一個版本的操做系統很難講,可能必須斷定補丁號),符號連接也帶有用戶相關性。換句話說,若是一個普通用戶建立了符號連接「\DosDevices\MyCDOSL」,那麼,其實其餘的用戶是看不見這個符號連接的。
可是讀者又會發現,若是在DriverEntry中生成符號連接,則全部用戶均可以看見。緣由是DriverEntry老是在進程「System」中執行。系統用戶生成的符號連接是你們均可以看見的。
當前用戶老是取決於當前啓動當前進程的用戶。實際編程中並不必定要在DriverEntry中生成符號連接。一旦在一個不明用戶環境下生成符號連接,就可 能出現註銷而後換用戶登陸以後,符號連接「不見了」的嚴重錯誤。這也是經常讓初學者抓狂幾周都不知道如何解決的一個問題。
其實解決的方案很簡單,任何用戶均可以生成全局符號連接,讓全部其餘用戶都能看見。路徑「\DosDevices\MyCDOSL」改成「\DosDevices\Global\MyCDOSL」便可。
可是在不支持符號連接用戶相關性的系統上,生成「\DosDevices\Global\MyCDOSL」這樣的符號連接是一種錯誤。爲此必須先判斷一下。幸運的是,這個判斷並不難。下面是一個例子,這個例子生成的符號連接老是隨時能夠使用,不用擔憂用戶註銷:
UNICODE_STRINGdevice_name;
UNICODE_STRINGsymbl_name;
if(IoIsWdmVersionAvailable(1, 0x10))
{
// 若是是支持符號連接用戶相關性的版本的系統,用\DosDevices\Global.
RtlInitUnicodeString(&symbl_name,L"\\DosDevices\\Global\\SymbolicLinkName");
}
else
{
// 若是是不支持的,則用\DosDevices
RtlInitUnicodeString(&symbl,L"\\DosDevices\\SymbolicLinkName");
}
// 生成符號連接
IoCreateSymbolicLink(&symbl_name,&device_name);
第八章 處理請求
8.1 IRP與IO_STACK_LOCATION
開發一個驅動要有可能要處理各類IRP。可是本書範圍內,只處理爲了應用程序和驅動交互而產生的IRP。IRP的結構很是複雜,可是目前的需求下沒有必要 去深究它。應用程序爲了和驅動通訊,首先必須打開設備。而後發送或者接收信息。最後關閉它。這至少須要三個IRP:第一個是打開請求。第二個發送或者接收 信息。第三個是關閉請求。
IRP的種類取決於主功能號。主功能號就是前面的說的DRIVER_OBJECT中的分發函數指針數組中的索引。打開請求的主功能號是IRP_MJ_CREATE,而關閉請求的主功能號是IRP_MJ_CLOSE。
若是寫有獨立的處理IRP_MJ_CREATE和IRP_MJ_CLOSE的分發函數,就沒有必要天然判斷IRP的主功能號。若是像前面的例子同樣,使用一個函數處理全部的IRP,那麼首先就要獲得IRP的主功能號。IRP的主功能號在IRP的當前棧空間中。
IRP老是發送給一個設備棧。到每一個設備上的時候擁有一個「當前棧空間」來保存在這個設備上的請求信息。讀者請暫時忽略這些細節。下面的代碼在MyDispatch中得到主功能號,同時展現了幾個常見的主功能號:
NTSTATUSMyDispatchFunction(PDEVICE_OBJECT device,PIRP irp)
{
// 得到當前irp調用棧空間
PIO_STACK_LOCATIONirpsp = IoGetCurrentIrpStackLocation(irp);
NTSTATUSstatus = STATUS_UNSUCCESSFUL;
swtich(irpsp->MajorFunction)
{
// 處理打開請求
caseIRP_MJ_CREATE:
……
break;
// 處理關閉請求
caseIRP_MJ_CLOSE:
……
break;
// 處理設備控制信息
caseIRP_MJ_DEVICE_CONTROL:
……
break;
// 處理讀請求
caseIRP_MJ_READ:
……
break;
// 處理寫請求
caseIRP_MJ_WRITE:
……
break;
default:
…
break;
}
returnstatus;
}
用於與應用程序通訊時,上面這些請求都由應用層API引起。對應的關係大體以下:
應用層調用的API
CreateFile
CloseHandle
DeviceIoControl
ReadFile
WriteFile
瞭解以上信息的狀況下,完成相關IRP的處理,就能夠實現應用層和驅動層的通訊了。具體的編程在緊接後面的兩小節裏完成。
8.2 打開與關閉的處理
若是打開不能成功,則通訊沒法實現。要打開成功,只須要簡單的返回成功就能夠了。在一些有同步限制的驅動中(好比每次只容許一個進程打開設備)編程要更加 複雜一點。可是如今忽略這些問題。暫時認爲咱們生成的設備任何進程均可以隨時打開,不須要擔憂和其餘進程衝突的問題。
簡單的返回一個IRP成功(或者直接失敗)是三部曲,以下:
1. 設置irp->IoStatus.Information爲0。關於Information的描述,請聯繫前面關於IO_STATUS_BLOCK結構的解釋。
2. 設置irp->IoStatus.Status的狀態。若是成功則設置STATUS_SUCCESS,不然設置錯誤碼。
3. 調用IoCompleteRequest (irp,IO_NO_INCREMENT)。這個函數完成IRP。
以上三步完成後,直接返回irp->IoStatus.Status便可。示例代碼以下。這個函數能完成打開和關閉請求。
NTSTATUS
MyCreateClose(
IN PDEVICE_OBJECT device,
IN PIRP irp)
{
irp->IoStatus.Information = 0;
irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest (irp,IO_NO_INCREMENT);
return irp->IoStatus.Status;
}
固然,在前面設置分發函數的時候,應該加上:
DriverObject->MajorFunctions[IRP_MJ_CREATE]= MyCreateClose;
DriverObject->MajorFunctions[IRP_MJ_CLOSE]= MyCreateClose;
在應用層,打開和關閉這個設備的代碼以下:
HANDLE device=CreateFile("\\\\.\\MyCDOSL",
GENERIC_READ|GENERIC_WRITE,0,0,
OPEN_EXISTING,
FILE_ATTRIBUTE_SYSTEM,0);
if (device ==INVALID_HANDLE_VALUE)
{
// …. 打開失敗,說明驅動沒加載,報錯便可
}
// 關閉
CloseHandle(device);
8.3 應用層信息傳入
應用層傳入信息的時候,能夠使用WriteFile,也能夠使用DeviceIoControl。DeviceIoControl是雙向的,在讀取設備的 信息也能夠使用。所以本書以DeviceIoControl爲例子進行說明。DeviceIoControl稱爲設備控制接口。其特色是能夠發送一個帶有 特定控制碼的IRP。同時提供輸入和輸出緩衝區。應用程序能夠定義一個控制碼,而後把相應的參數填寫在輸入緩衝區中。同時能夠從輸出緩衝區獲得返回的更多 信息。
當驅動獲得一個DeviceIoControl產生的IRP的時候,須要瞭解的有當前的控制碼、輸入緩衝區的位置和長度,以及輸出緩衝區的位置和長度。其中控制碼必須預先用一個宏定義。定義的示例以下:
#defineMY_DVC_IN_CODE \
(ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN,\
0xa01,\
METHOD_BUFFERED,\
FILE_READ_DATA|FILE_WRITE_DATA)
其中0xa01這個數字是用戶能夠自定義的。其餘的參數請照抄。
下面是得到這三個要素的例子:
NTSTATUSMyDeviceIoControl(
PDEVICE_OBJECT dev,
PIRP irp)
{
// 獲得irpsp的目的是爲了獲得功能號、輸入輸出緩衝
// 長度等信息。
PIO_STACK_LOCATIONirpsp =
IoGetCurrentIrpStackLocation(irp);
// 首先要獲得功能號
ULONGcode = irpsp->Parameters.DeviceIoControl.IoControlCode;
// 獲得輸入輸出緩衝長度
ULONGin_len =
irpsp->Parameters.DeviceIoControl.InputBufferLength;
ULONGout_len =
irpsp->Parameters.DeviceIoControl.OutputBufferLength;
// 請注意輸入輸出緩衝是公用內存空間的
PVOIDbuffer = irp->AssociatedIrp.SystemBuffer;
// 若是是符合定義的控制碼,處理完後返回成功
if(code== MY_DVC_IN_CODE)
{
…在這裏進行須要的處理動做
//由於不返回信息給應用,因此直接返回成功便可。
//沒有用到輸出緩衝
irp->IoStatus.Information= 0;
irp->IoStatus.Status= STATUS_SUCCESS;
}
else
{
// 其餘的請求不接受。直接返回錯誤。請注意這裏返
// 回錯誤和前面返回成功的區別。
irp->IoStatus.Information = 0;
irp->IoStatus.Status= STATUS_INVALID_PARAMETER;
}
IoCompleteRequest (irp,IO_NO_INCREMENT);
returnirp->IoStatus.Status;
}
在前面設置分發函數的時候,要加上:
DriverObject->MajorFunctions[IRP_MJ_DEVICE_CONTROL]= MyCreateClose;
應用程序方面,進行DeviceIoControl的代碼以下:
HANDLE device=CreateFile("\\\\.\\MyCDOSL",
GENERIC_READ|GENERIC_WRITE,0,0,
OPEN_EXISTING,
FILE_ATTRIBUTE_SYSTEM,0);
BOOLret;
DWORDlength = 0; // 返回的長度
if (device ==INVALID_HANDLE_VALUE)
{
// … 打開失敗,說明驅動沒加載,報錯便可
}
BOOLret = DeviceIoControl(device,
MY_DVC_IN_CODE, // 功能號
in_buffer, // 輸入緩衝,要傳遞的信息,預先填好
in_buffer_len, // 輸入緩衝長度
NULL, // 沒有輸出緩衝
0, // 輸出緩衝的長度爲0
&length, // 返回的長度
NULL);
if(!ret)
{
// … DeviceIoControl失敗。報錯。
}
// 關閉
CloseHandle(device);
8.4 驅動層信息傳出
驅動主動通知應用和應用通知驅動的通道是同一個。只是方向反過來。應用程序須要開啓一個線程調用DeviceIoControl,(調用ReadFile亦可)。而驅動在沒有消息的時候,則阻塞這個IRP的處理。等待有信息的時候返回。
有的讀者可能據說過在應用層生成一個事件,而後把事件傳遞給驅動。驅動有消息要通知應用的時候,則設置這個事件。可是實際上這種方法和上述方法本質相同: 應用都必須開啓一個線程去等待(等待事件)。並且這樣使應用和驅動之間交互變得複雜(須要傳遞事件句柄)。這毫無必要。
讓應用程序簡單的調用DeviceIoControl就能夠了。當沒有消息的時候,這個調用不返回。應用程序自動等待(至關於等待事件)。有消息的時候這個函數返回。並從緩衝區中讀到消息。
實際上,驅動內部要實現這個功能,仍是要用事件的。只是不用在應用和驅動之間傳遞事件了。
驅動內部須要製做一個鏈表。當有消息要通知應用的時候,則把消息放入鏈表中(請參考前面的「使用LIST_ENTRY」),並設置事件(請參考前面的「使 用事件」)。在DeviceIoControl的處理中等待事件。下面是一個例子:這個例子展現的是驅動中處理DeviceIoControl的控制碼爲 MY_DVC_OUT_CODE的部分。實際上驅動若是有消息要通知應用,必須把消息放入隊列尾並設置事件g_my_notify_event。 MyGetPendingHead得到第一條消息。請讀者用之前的知識本身完成其餘的部分。
NTSTATUSMyDeviceIoCtrlOut(PIRP irp,ULONG out_len)
{
MY_NODE*node;
ULONGpack_len;
// 得到輸出緩衝區。
PVOIDbuffer = irp->AssociatedIrp.SystemBuffer;
// 從隊列中取得第一個。若是爲空,則等待直到不爲空。
while((node= MyGetPendingHead()) == NULL)
{
KeWaitForSingleObject(
&g_my_notify_event,//一個用來通知有請求的事件
Executive,KernelMode,FALSE,0);
}
// 有請求了。此時請求是node。得到PACK要多長。
pack_len= MyGetPackLen(node);
if(out_len< pack_len)
{
irp->IoStatus.Information= pack_len; // 這裏寫須要的長度
irp->IoStatus.Status =STATUS_INVALID_BUFFER_SIZE;
IoCompleteRequest (irp,IO_NO_INCREMENT);
return irp->IoStatus.Status;
}
// 長度足夠,填寫輸出緩衝區。
MyWritePackContent(node,buffer);
// 頭節點被髮送出去了,能夠刪除了
MyPendingHeadRemove();
// 返回成功
irp->IoStatus.Information= pack_len; // 這裏寫填寫的長度
irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest (irp,IO_NO_INCREMENT);
return irp->IoStatus.Status;
}
這個函數的處理要追加到MyDeviceIoControl中。以下:
NTSTATUSMyDeviceIoControl(
PDEVICE_OBJECT dev,
PIRP irp)
{
…
if(code== MY_DVC_OUT_CODE)
returnMyDeviceIoCtrlOut(dev,irp);
…
}
在這種狀況下,應用能夠循環調用DeviceIoControl,來取得驅動驅動通知它的信息。
後記:個人閒言碎語
寫這本小冊子的時候,我正在NED-LS辦離職手續。
想當初在東京的時候,NED的田上每夜好酒好肉的招待。北京幾個同事跳槽,搞得項目特別尷尬。田上特地說道:「拜託大家不要轉職......」。
半年不到,我就拋出一紙辭職信,真是負心人啊......
一眨眼間,就在NED-LS混了三年了。不是我非要走人,一方面匯率節節攀升,外包愈來愈困難。歐美企業不退反進,紛紛把更高檔的玩意搬來國內來發。許多日本公司卻還把外包當主業。另外一方面我日語暴爛,又不願學。繼續擺爛顯然不是辦法。
相對於Intel,我其實一直是看好AMD的。上次幫小D選筆記本,還特地選了AMD的CPU。都說AMD的東西便宜量又足,最適合國內的勞苦大衆,這可 不是吹的。只惋惜貌似每次都被Intel揍得鼻青臉腫。因此我便去面試了。跟我去後面那天去的還有那個寫「碟中諜虛擬光驅」的說話有點像唐僧的萬春。
上海的AMD在浦東的荒郊。先把2號線坐到終點站,而後打的到一處無人知道的野外。只看見長長的公路和茂盛的野草。兩邊有無數片工地,橫七豎八的堆着許多建築材料,卻沒有一我的。好處是內急的時候不用找公廁(也不可能找獲得)。直接在路邊就能夠解決。
工地的旁邊有一部分紅品。AMD的綠色標記就坐落其中。有兩棟樓,都不大,袖珍型的。他們的面試沒有筆試(說原本是有,但我去的時候卷子還沒準備好,就免了)。是四我的輪番上陣,前三個是工程師,最後一個是經理。
我面的是存儲芯片驅 動開發。問到驅動開發相關或者純C語言的問題,我天然是對答如流,這許多年的苦工不是白乾的。恰恰他們對效率頗有興趣,老是不時拋出幾個位運算的妙用之類 的幾個優化題。我只好明說了平時並不怎麼關心效率。因此這個不擅長。待加盟了大家項目組以後,必定好好學習每天向上云云。
最終固然是沒過了。他們Pending了好長一段時間。最終結論是我不適合作硬件驅動開發。由於我之前的經驗比較上層。作虛擬SCSI設備的萬春按理比我 好點,可是他死得更慘:結論是隻在小公司幹過,組織性、紀律性會比較差(其實這也沒說錯==!)。雜牌軍被BS了。
萬春沒多久就去廣州了。真是「浮雲遊子意,落日故人情」啊。不過話撂這兒了,莫怪勞資之後不支持AMD...
個人零八年的春夏真是愜意。作的幾個程序都還沒出大問題。有空還寫寫書,《天書夜讀》扔給出版社了。只惋惜一審再編的沒完沒了。到如今也尚未頭。我又開始寫新書。但不知道怎麼說,由於題目尚未擬好。而後又見到小D,一個不當心就掉到蜜罐子裏了。
後來又面試了幾個。Mavell的面試官實在太強了,無所不知,成功的鄙視了我。MS的面試官是老外。雖然我不懂他說什麼,可是我說的他也未見得明白。
不過MS的職位真的是很棒啊。作Windows Kernel,並且還在上海。一經過立刻先送去美利堅培訓。很美啊。
後來又去面Intel(屢敗屢面==!)。Intel的環境真是不錯。我到的時候正是早上。天氣又好,一我的也沒有,對面是一大片幾乎望不到邊的淺水,長 着人深的草。幾隻長腳的蒼鷺站在水裏。還有一些棕色的像小鴨子的玩意在水裏搶食吃。一些小鳥在空中掠過,發出銅鈴同樣的聲音。Intel就在那邊沼澤地的 對面。比AMD大。幾棟大樓。有上千人在那裏工做。吃飯都在食堂。方圓2千米內沒有飯店。
Intel的面試和AMD很像。沒有筆試。四人輪番上陣。不過他們四我的稍微有些分工。每一個人問的方向都不大同樣。另外一個狀況是他們喜歡給你水筆,而後請直接在白板上寫代碼。還不能寫簡意,非要一行一行寫出來才行。寫白板手抖得厲害,沒點心理素質還真不行。
第一我的問內核編程。問到個人得意之處了。不過這幫人還真的有兩把刷子——他們能看Windows的代碼,我還得本身反彙編。世界真不公平啊。
而後來一我的問了不少設計模式和代碼管理之類的問題。這方面我固然是更口若懸河了。最有挑戰性的是第三人,髮型很像愛因斯坦那個,進來坐定以後,也不說什 麼,就給出一張白紙,讓我寫一個矩形相交判斷以及一個形狀覆蓋的算法。時間又大概只有二十分鐘,大腦一片空白,汗就出來了。空白了大概十分鐘,還一個字沒 寫。面試官都急了,就說:「你若是有什麼中間結果,就先拿出來。」意思就是你多少寫點,別交白卷啊。
不過好歹我之前是作過3D引擎的(雖然作得很爛的說)。慢慢冷靜下來,和他說了計算的步驟。不算優秀也不算高效(十幾分鍾哪裏有空考慮那麼多啊)。可是面 試官說也算是邏輯完整。ok,又出了一道圖論算法題。這時候我已經緩過勁來,五分鐘內輕鬆搞定。愛因斯坦滿意的走了。
最後一個是部門經理。相與言歡。而後他請客在他們食堂吃飯。可是真的很難吃的說。下午回NEC-AS繼續上班,一面構思辭職信的措辭。
告別lu0,告別wowocock。
本書獻給小D。她是我今夏遇到的,生命裏最美的一縷陽光。
譚文 於 2008年端午
(全書完)
版權聲明
本書是免費電子書。 做者保留一切權利。但在保證本書完整性(包括版權聲明、前言、正文內容、後記、以及做者的信息),並不增刪、改變其中任何文字內容的前提下,歡迎任何讀者 以任何形式(包括各類格式的文檔)複製和轉載本書。同時不限制利用此書贏利的行爲(如收費註冊下載,或者出售光盤或打印版本)。不知足此前提的任何轉載、 複製、贏利行爲則是侵犯版權的行爲。
發現本書的錯漏之處,請聯繫做者。請不要修改本文中任何內容,不通過做者的贊成發佈修改後的版本。
做者信息
做者網名楚狂人。真名譚文。在上海從事Windows驅動開發相關的工做。對本書任何內容有任何疑問的讀者,能夠用下列方式和做者取得聯繫:
QQ:16191935
MSN:walled_river@hotmail.com
前言
本書很是適合熟悉Windows應用編程的讀者轉向驅動開發。全部的內容都從最基礎的編程方法入手。介紹相關的內核API,而後舉出示範的例子。這本書只 有不到70頁,是一本很是精簡的小冊子。因此它並不直接指導讀者開發某種特定類型的驅動程序。而是起到一個入門指導的做用。
即便都是使用C/C++語言的代碼,在不一樣的應用環境中,經常看起來仍是截然不同。好比用TurboC++編寫的DOS程序代碼和用VC++編寫的MFC應用程序的代碼,看起來就幾乎不像是同一種語言。這是因爲它們所依賴的開發包不相同的緣故。
在任何狀況下都以寫出避免依賴的代碼爲最佳。這樣能夠避免重複勞動。可是咱們在學習一種開發包的使用時,必須習慣這個環境的編碼方式,以便得到充分利用這個開發包的能力。
本書的代碼幾乎都依賴於WDK(Windows Driver Kit)。可是不限WDK的版本。WDK還在不斷的升級中。這個開發包是由微軟公司免費提供的。讀者能夠在微軟的網站上下載。
固然讀者必須把WDK安裝的計算機上並配置好開發環境。具體的安裝和配置方法本書沒有提供。由於網上已經有很是多的中文文檔介紹它們。
讀完這本書以後,讀者必定能夠更輕鬆的閱讀其餘專門的驅動程序開發的文檔和相關書籍。而不至於看到大量沒法理解的代碼而中途放棄。若是有任何關於本書的內 容的問題,讀者能夠隨時發郵件到mfc_tan_wen@163.com或者walled_river@hotmail.com。可以回答的問題我通常都 會答覆。
寫本書的時候,我和wowocock合做的一本名爲《天書夜讀》(在網上有一個大約20%內容的縮減電子版本)正在電子工業出版社編輯。預計還有不到一個 月左右就會出版。這也是我本身所見的惟一一本中文原創的從彙編和反彙編角度來學習Windows內核編程和信息安全軟件開發的書。但願讀者多多支持。有想 購買的讀者請發郵件給我。我會在本書出版的第一時間,回覆郵件告知購買的方法。
此外我正在寫另外一本關於Windows安全軟件的驅動編程的書。可是題目尚未擬好。實際上,讀者如今見到的免費版本的《Windows驅動編程基礎教程》是從這本書的第一部分中節選出來的。這本書篇幅比較大,大約有600-800頁。主要內容以下:
第一章驅動編程基礎
第二章磁盤設備驅動
第三章磁盤還原與加密
第四章傳統文件系統過濾
第五章小端口文件系統過濾
第六章文件系統保護與加密
第七章協議網絡驅動
第八章物理網絡驅動
第九章網絡防火牆與安全鏈接
第十章打印機驅動與虛擬打印
第十一章視頻驅動與過濾
附錄A WDK的安裝與驅動開發的環境配置
附錄B 用WinDbg調試Windows驅動程序
這本書尚未完成。可是確定要付出巨大的精力,因此請讀者不要來郵件索取完整的免費的電子版本。但願讀者支持本書的紙版出版。由於沒有完成,因此尚未聯繫出版商。有願意合做出版本書的讀者請發郵件與我聯繫。
凡是發送郵件給個人讀者,我將會發送郵件提供本人做品最新的出版信息,以及最新發布的驅動開發相關的免費電子書。若是不須要這些信息的,請在郵件裏註明,或者回復郵件給我來取消訂閱。
譚文
2008年6月9日
目錄
第一章 字符串
1.1 使用字符串結構
經常使用傳統C語言的程序員比較喜歡用以下的方法定義和使用字符串:
char *str = { 「my first string」 }; // ansi字符串
wchar_t *wstr = { L」my first string」 }; // unicode字符串
size_tlen = strlen(str); //ansi字符串求長度
size_twlen = wcslen(wstr); //unicode字符串求長度
printf(「%s%ws %d %d」,str,wstr,len,wlen); // 打印兩種字符串
可是實際上這種字符串至關的不安全。很容易致使緩衝溢出漏洞。這是由於沒有任何地方確切的代表一個字符串的長度。僅僅用一個’\0’字符來標明這個字符串 的結束。一旦碰到根本就沒有空結束的字符串(多是攻擊者惡意的輸入、或者是編程錯誤致使的意外),程序就可能陷入崩潰。
使用高級C++特性的編碼者則容易忽略這個問題。由於經常使用std::string和CString這樣高級的類。不用去擔心字符串的安全性了。
在驅動開發中,通常再也不用空來表示一個字符串的結束。而是定義了以下的一個結構:
typedefstruct _UNICODE_STRING {
USHORTLength; // 字符串的長度(字節數)
USHORTMaximumLength; // 字符串緩衝區的長度(字節數)
PWSTR Buffer; //字符串緩衝區
}UNICODE_STRING, *PUNICODE_STRING;
以上是Unicode字符串,一個字符爲雙字節。與之對應的還有一個Ansi字符串。Ansi字符串就是C語言中經常使用的單字節表示一個字符的窄字符串。
typedefstruct _STRING {
USHORTLength;
USHORTMaximumLength;
PSTRBuffer;
}ANSI_STRING, *PANSI_STRING;
在驅動開發中四處可見的是Unicode字符串。所以能夠說:Windows的內核是使用Uincode編碼的。ANSI_STRING僅僅在某些碰到窄字符的場合使用。並且這種場合很是罕見。
UNICODE_STRING並不保證Buffer中的字符串是以空結束的。所以,相似下面的作法都是錯誤的,可能會會致使內核崩潰:
UNICODE_STRINGstr;
…
len =wcslen(str.Buffer); // 試圖求長度。
DbgPrint(「%ws」,str.Buffer); // 試圖打印str.Buffer。
若是要用以上的方法,必須在編碼中保證Buffer始終是以空結束。但這又是一個麻煩的問題。因此,使用微軟提供的Rtl系列函數來操做字符串,纔是正確的方法。下文逐步的講述這個系列的函數的使用。
1.2 字符串的初始化
請回顧以前的UNICODE_STRING結構。讀者應該能夠注意到,這個結構中並不含有字符串緩衝的空間。這是一個初學者常見的出問題的來源。如下的代碼是徹底錯誤的,內核會馬上崩潰:
UNICODE_STRINGstr;
wcscpy(str.Buffer,L」myfirst 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」myfirst string!」) – sizeof((L」my first string!」)[0]),
sizeof(L」myfirst string!」),
L」myfirst_string!」 };
可是這樣定義一個字符串實在太繁瑣了。可是在頭文件ntdef.h中有一個宏方便這種定義。使用這個宏以後,咱們就能夠簡單的定義一個常數字符串以下:
#include<ntdef.h>
UNICODE_STRING str = RTL_CONSTANT_STRING(L「myfirst string!」);
這隻能在定義這個字符串的時候使用。爲了隨時初始化一個字符串,能夠使用RtlInitUnicodeString。示例以下:
UNICODE_STRINGstr;
RtlInitUnicodeString(&str,L」myfirst string!」);
用本小節的方法初始化的字符串,不用擔憂內存釋放方面的問題。由於咱們並無分配任何內存。
1.3 字符串的拷貝
由於字符串再也不是空結束的,因此使用wcscpy來拷貝字符串是不行的。UNICODE_STRING能夠用RtlCopyUnicodeString來 進行拷貝。在進行這種拷貝的時候,最須要注意的一點是:拷貝目的字符串的Buffer必須有足夠的空間。若是Buffer的空間不足,字符串會拷貝不完 全。這是一個比較隱蔽的錯誤。
下面舉一個例子。
UNICODE_STRINGdst; // 目標字符串
WCHARdst_buf[256]; // 咱們如今還不會分配內存,因此先定義緩衝區
UNICODE_STRINGsrc = 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。雖然程序沒有崩潰,卻實際上沒有拷貝任何內容。
在拷貝以前,最謹慎的方法是根據源字符串的長度動態分配空間。在1.2節「內存與鏈表」中,讀者會看到動態分配內存處理字符串的方法。
1.4 字符串的鏈接
UNICODE_STRING再也不是簡單的字符串。操做這個數據結構每每須要更多的耐心。讀者會經常碰到這樣的需求:要把兩個字符串鏈接到一塊兒。簡單的追加一個字符串並不困難。重要的依然是保證目標字符串的空間大小。下面是範例:
NTSTATUSstatus;
UNICODE_STRINGdst; // 目標字符串
WCHARdst_buf[256]; // 咱們如今還不會分配內存,因此先定義緩衝區
UNICODE_STRINGsrc = RTL_CONST_STRING(L」My source string!」);
// 把目標字符串初始化爲擁有緩衝區長度爲256的UNICODE_STRING空串
RtlInitEmptyString(dst,dst_buf,256*sizeof(WCHAR));
RtlCopyUnicodeString(&dst,&src); // 字符串拷貝!
status= RtlAppendUnicodeToString(
&dst,L」mysecond string!」);
if(status!= STATUS_SUCCESS)
{
……
}
NTSTATUS是常見的返回值類型。若是函數成功,返回STATUS_SUCCESS。不然的話,是一個錯誤碼。 RtlAppendUnicodeToString在目標字符串空間不足的時候依然能夠鏈接字符串,可是會返回一個警告性的錯誤 STATUS_BUFFER_TOO_SMALL。
另一種狀況是但願鏈接兩個UNICODE_STRING,這種狀況請調用RtlAppendUnicodeStringToString。這個函數的第二個參數也是一個UNICODE_STRING的指針。
1.5 字符串的打印
字符串的鏈接另外一種常見的狀況是字符串和數字的組合。有時數字須要被轉換爲字符串。有時須要把若干個數字和字符串混合組合起來。這每每用於打印日誌的時候。日誌中可能含有文件名、時間、和行號,以及其餘的信息。
熟悉C語言的讀者會使用sprintf。這個函數的寬字符版本爲swprintf。該函數在驅動開發中依然能夠使用,可是不安全。微軟建議使用 RtlStringCbPrintfW來代替它。RtlStringCbPrintfW須要包含頭文件ntstrsafe.h。在鏈接的時候,還須要鏈接 庫ntsafestr.lib。
下面的代碼生成一個字符串,字符串中包含文件的路徑,和這個文件的大小。
#include<ntstrsafe.h>
// 任什麼時候候,假設文件路徑的長度爲有限的都是不對的。應該動態的分配
// 內存。可是動態分配內存的方法尚未講述,因此這裏再次把內存空間
// 定義在局部變量中,也就是所謂的「在棧中」
WCHARbuf[512] = { 0 };
UNICODE_STRINGdst;
NTSTATUSstatus;
……
// 字符串初始化爲空串。緩衝區長度爲512*sizeof(WCHAR)
RtlInitEmptyString(dst,dst_buf,512*sizeof(WCHAR));
// 調用RtlStringCbPrintfW來進行打印
status= RtlStringCbPrintfW(
dst->Buffer,L」filepath = %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」filepath = %wZ file size = %d \r\n」,
&file_path,file_size));
這個宏沒有必要本身定義,WDK包中已有。因此能夠直接使用KdPrint來代替DbgPrint取得更方便的效果。
第二章 內存與鏈表
2.1內存的分配與釋放
內存泄漏是C語言中一個臭名昭著的問題。可是做爲內核開發者,讀者將有必要本身來面對它。在傳統的C語言中,分配內存經常使用的函數是malloc。這個 函數的使用很是簡單,傳入長度參數就獲得內存空間。在驅動中使用內存分配,這個函數再也不有效。驅動中分配內存,最經常使用的是調用 ExAllocatePoolWithTag。其餘的方法在本章範圍內所有忽略。回憶前一小節關於字符串的處理的狀況。一個字符串被複制到另外一個字符串的 時候,最好根據源字符串的空間長度來分配目標字符串的長度。下面的舉例,是把一個字符串src拷貝到字符串dst。
// 定義一個內存分配標記
#defineMEM_TAG ‘MyTt’
// 目標字符串,接下來它須要分配空間。
UNICODE_STRINGdst = { 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_STRINGsrc = RTL_CONST_STRING(L」My source string!」);
ExFreePool(src.Buffer);
會招來馬上藍屏。因此請務必保持ExAllocatePoolWithTag和ExFreePool的成對關係。
2.2 使用LIST_ENTRY
Windows的內核開發者們本身開發了部分數據結構,好比說LIST_ENTRY。
LIST_ENTRY是一個雙向鏈表結構。它老是在使用的時候,被插入到已有的數據結構中。下面舉一個例子。我構築一個鏈表,這個鏈表的每一個節點,是一個 文件名和一個文件大小兩個數據成員組成的結構。此外有一個FILE_OBJECT的指針對象。在驅動中,這表明一個文件對象。本書後面的章節會詳細解釋。 這個鏈表的做用是:保存了文件的文件名和長度。只要傳入FILE_OBJECT的指針,使用者就能夠遍歷鏈表找到文件名和文件長度。
typedefstruct {
PFILE_OBJECTfile_object;
UNICODE_STRINGfile_name;
LARGE_INTEGERfile_length;
}MY_FILE_INFOR, *PMY_FILE_INFOR;
一些讀者會立刻注意到文件的長度用LARGE_INTEGER表示。這是一個表明長長整型的數據結構。這個結構咱們在下一小小節「使用長長整型數據」中介紹。
爲了讓上面的結構成爲鏈表節點,我必須在裏面插入一個LIST_ENTRY結構。至於插入的位置並沒有所謂。能夠放在最前,也能夠放中間,或者最後面。可是實際上讀者很快會發現把LIST_ENTRY放在開頭是最簡單的作法:
typedefstruct {
LIST_ENTRYlist_entry;
PFILE_OBJECTfile_object;
UNICODE_STRINGfile_name;
LARGE_INTEGERfile_length;
} MY_FILE_INFOR, *PMY_FILE_INFOR;
list_entry若是是做爲鏈表的頭,在使用以前,必須調用InitializeListHead來初始化。下面是示例的代碼:
// 咱們的鏈表頭
LIST_ENTRY my_list_head;
// 鏈表頭初始化。通常的說在應該在程序入口處調用一下
voidMyFileInforInilt()
{
InitializeListHead(&my_list_head);
}
// 咱們的鏈表節點。裏面保存一個文件名和一個文件長度信息。
typedefstruct {
LIST_ENTRYlist_entry;
PFILE_OBJECTfile_object;
PUNICODE_STRINGfile_name;
LARGE_INTEGERfile_length;
} MY_FILE_INFOR, *PMY_FILE_INFOR;
// 追加一條信息。也就是增長一個鏈表節點。請注意file_name是外面分配的。
// 內存由使用者管理。本鏈表並無論理它。
NTSTATUS MyFileInforAppendNode(
PFILE_OBJECTfile_object,
PUNICODE_STRINGfile_name,
PLARGE_INTEGERfile_length)
{
PMY_FILE_INFORmy_file_infor =
(PMY_FILE_INFOR)ExAllocatePoolWithTag(
PagedPool,sizeof(MY_FILE_INFOR),MEM_TAG);
if(my_file_infor== NULL)
returnSTATUS_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);
returnSTATUS_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_INFORelem =
CONTAINING_RECORD(p,MY_FILE_INFOR,list_entry);
// 在這裏作須要作的事…
}
}
其中的CONTAINING_RECORD是一個WDK中已經定義的宏,做用是經過一個LIST_ENTRY結構的指針,找到這個結構所在的節點的指針。定義以下:
#defineCONTAINING_RECORD(address, type, field) ((type *)( \
(PCHAR)(address) - \
(ULONG_PTR)(&((type *)0)->field)))
從上面的代碼中能夠總結以下的信息:
LIST_ENTRY中的數據成員Flink指向下一個LIST_ENTRY。
整個鏈表中的最後一個LIST_ENTRY的Flink不是空。而是指向頭節點。
獲得LIST_ENTRY以後,要用CONTAINING_RECORD來獲得鏈表節點中的數據。
2.3 使用長長整型數據
這裏解釋前面碰到的LARGE_INTEGER結構。與可能的誤解不一樣,64位數據並不是要在64位操做系統下才能使用。在VC中,64位數據的類型爲__int64。定義寫法以下:
__int64file_offset;
上面之因此定義的變量名爲file_offset,是由於文件中的偏移量是一種常見的要使用64位數據的狀況。同時,文件的大小也是如此(回憶上一小節中 定義的文件大小)。32位數據無符號整型只能表示到4GB。而衆所周知,如今超過4GB的文件絕對不罕見了。可是實際上__int64這個類型在驅動開發 中不多被使用。基本上被使用到的是一個共用體:LARGE_INTEGER。這個共用體定義以下:
typedef__int64 LONGLONG;
typedefunion _LARGE_INTEGER {
struct{
ULONGLowPart;
LONGHighPart;
};
struct{
ULONGLowPart;
LONGHighPart;
} u;
LONGLONG QuadPart;
}LARGE_INTEGER;
這個共用體的方便之處在於,既能夠很方便的獲得高32位,低32位,也能夠方便的獲得整個64位。進行運算和比較的時候,使用QuadPart便可。
LARGE_INTEGERa,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類型的。
2.4使用自旋鎖
鏈表之類的結構老是涉及到惱人的多線程同步問題,這時候就必須使用鎖。這裏只介紹最簡單的自選鎖。
有些讀者可能疑惑鎖存在的意義。這和多線程操做有關。在驅動開發的代碼中,大可能是存在於多線程執行環境的。就是說可能有幾個線程在同時調用當前函數。
這樣一來,前文1.2.2中說起的追加鏈表節點函數就根本沒法使用了。由於MyFileInforAppendNode這個函數只是簡單的操做鏈表。若是 兩個線程同時調用這個函數來操做鏈表的話:注意這個函數操做的是一個全局變量鏈表。換句話說,不管有多少個線程同時執行,他們操做的都是同一個鏈表。這就 可能發生,一個線程插入一個節點的同時,另外一個線程也同時插入。他們都插入同一個鏈表節點的後邊。這時鏈表就會發生問題。到底最後插入的是哪個呢?要麼 一個丟失了。要麼鏈表被損壞了。
以下的代碼初始化獲取一個自選鎖:
KSPIN_LOCKmy_spin_lock;
KeInitializeSpinLock(&my_spin_lock);
KeInitializeSpinLock這個函數沒有返回值。下面的代碼展現瞭如何使用這個SpinLock。在KeAcquireSpinLock和 KeReleaseSpinLock之間的代碼是隻有單線程執行的。其餘的線程會停留在KeAcquireSpinLock等候。直到 KeReleaseSpinLock被調用。KIRQL是一箇中斷級。KeAcquireSpinLock會提升當前的中斷級。可是目前忽略這個問題。中 斷級在後面講述。
KIRQLirql;
KeAcquireSpinLock(&my_spin_lock,&irql);
// Todo something …
KeReleaseSpinLock(&my_spin_lock,irql);
初學者要注意的是,像下面寫的這樣的「加鎖」代碼是沒有意義的,等於沒加鎖:
voidMySafeFunction()
{
KSPIN_LOCKmy_spin_lock;
KIRQLirql;
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; //鏈表的鎖
// 鏈表初始化函數
voidMyFileInforInilt()
{
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中。
第三章 文件操做
在內核中不能調用用戶層的Win32 API函數來操做文件。在這裏必須改用一系列與之對應的內核函數。
3.1 使用OBJECT_ATTRIBUTES
通常的想法是,打開文件應該傳入這個文件的路徑。可是實際上這個函數並不直接接受一個字符串。使用者必須首先填寫一個OBJECT_ATTRIBUTES 結構。在文檔中並無公開這個OBJECT_ATTRIBUTES結構。這個結構老是被InitializeObjectAttributes初始化。
下面專門說明InitializeObjectAttributes。
VOIDInitializeObjectAttributes(
OUTPOBJECT_ATTRIBUTES InitializedAttributes,
INPUNICODE_STRING ObjectName,
INULONG Attributes,
INHANDLE RootDirectory,
INPSECURITY_DESCRIPTOR SecurityDescriptor);
讀者須要注意的如下的幾點:InitializedAttributes是要初始化的OBJECT_ATTRIBUTES結構的指針。ObjectName則是對象名字字符串。也就是前文所描述的文件的路徑(若是要打開的對象是一個文件的話)。
Attributes則只須要填寫OBJ_CASE_INSENSITIVE| OBJ_KERNEL_HANDLE便可(若是讀者是想要方便的簡潔的打開一個文件的話)。OBJ_CASE_INSENSITIVE意味着名字字符串是 不區分大小寫的。因爲Windows的文件系統原本就不區分字母大小寫,因此筆者並無嘗試過若是不設置這個標記會有什麼後果。 OBJ_KERNEL_HANDLE代表打開的文件句柄一個「內核句柄」。內核文件句柄比應用層句柄使用更方便,能夠不受線程和進程的限制。在任何線程中 均可以讀寫。同時打開內核文件句柄不須要顧及當前進程是否有權限訪問該文件的問題(若是是有安全權限限制的文件系統)。若是不使用內核句柄,則有時不得不 填寫後面的的SecurityDescriptor參數。
RootDirectory用於相對打開的狀況。目前省略。請讀者傳入NULL便可。
SecurityDescriptor用於設置安全描述符。因爲筆者老是打開內核句柄,因此不多設置這個參數。
3.2 打開和關閉文件
下面的函數用於打開一個文件:
NTSTATUSZwCreateFile(
OUTPHANDLE FileHandle,
INACCESS_MASK DesiredAccess,
INPOBJECT_ATTRIBUTES ObjectAttribute,
OUTPIO_STATUS_BLOCK IoStatusBlock,
INPLARGE_INTEGER AllocationSize OPTIONAL,
INULONG FileAttributes,
INULONG ShareAccess,
INULONG CreateDisposition,
INULONG createOptions,
INPVOID EaBuffer OPTIONAL,
INULONG EaLength);
這個函數的參數異常複雜。下面逐個的說明以下:
FileHandle:是一個句柄的指針。若是這個函數調用返回成成功(STATUS_SUCCESS),那就麼打開的文件句柄就返回在這個地址內。
DesiredAccess: 申請的權限。若是打開寫文件內容,請使用FILE_WRITE_DATA。若是須要讀文件內容,請使用FILE_READ_DATA。若是須要刪除文件或 者把文件更名,請使用DELETE。若是想設置文件屬性,請使用FILE_WRITE_ATTRIBUTES。反之,讀文件屬性則使用 FILE_READ_ATTRIBUTES。這些條件能夠用|(位或)來組合。有兩個宏分別組合了經常使用的讀權限和經常使用的寫權限。分別爲 GENERIC_READ和GENERIC_WRITE。此外還有一個宏表明所有權限,是GENERIC_ALL。此外,若是想同步的打開文件,請加上 SYNCHRONIZE。同步打開文件詳見後面對CreateOptions的說明。
ObjectAttribute:對象描述。見前一小節。
IoStatusBlock也是一個結構。這個結構在內核開發中常用。它每每用於表示一個操做的結果。這個結構在文檔中是公開的,以下:
typedefstruct _IO_STATUS_BLOCK {
union {
NTSTATUS Status;
PVOID Pointer;
};
ULONG_PTR Information;
}IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;
實際編程中不多用到Pointer。通常的說,返回的結果在Status中。成功則爲STATUS_SUCCESS。不然則是一個錯誤碼。進一步的信息在 Information中。不一樣的狀況下返回的Information的信息意義不一樣。針對ZwCreateFile調用的狀況,Information 的返回值有如下幾種可能:
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_CREATED:文件被成功的新建了。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_OPENED: 文件被打開了。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_OVERWRITTEN:文件被覆蓋了。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_SUPERSEDED: 文件被替代了。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_EXISTS:文件已存在。(於是打開失敗了)。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_DOES_NOT_EXIST:文件不存在。(於是打開失敗了)。
這些返回值和打開文件的意圖有關(有時但願打開已存在的文件,有時則但願創建新的文件等等。這些意圖在本小節稍後的內容中詳細說明。
ZwCreateFile的下一個參數是AllocationSize。這個參數不多使用,請設置爲NULL。 再接下來的一個參數爲FileAttributes。這個參數控制新創建的文件的屬性。通常的說,設置爲FILE_ATTRIBUTE_NORMAL即 可。在實際編程中,筆者沒有嘗試過其餘的值。
ShareAccess是一個很是容易被人誤解的參數。實際上,這是在本代碼打開這個文件的時候,容許別的代碼同時打開這個文件所持有的權限。因此稱爲共 享訪問。一共有三種共享標記能夠設置:FILE_SHARE_READ、FILE_SHARE_WRITE、FILE_SHARE_DELETE。這三個 標記能夠用|(位或)來組合。舉例以下:若是本次打開只使用了FILE_SHARE_READ,那麼這個文件在本次打開以後,關閉以前,別次打開試圖以讀 權限打開,則被容許,能夠成功打開。若是別次打開試圖以寫權限打開,則必定失敗。返回共享衝突。
同時,若是本次打開只只用了FILE_SHARE_READ,而以前這個文件已經被另外一次打開用寫權限打開着。那麼本次打開必定失敗,返回共享衝突。其中的邏輯關係貌似比較複雜,讀者應耐心理解。
CreateDisposition參數說明了此次打開的意圖。可能的選擇以下(請注意這些選擇不能組合):
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_CREATE:新建文件。若是文件已經存在,則這個請求失敗。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_OPEN:打開文件。若是文件不存在,則請求失敗。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_OPEN_IF:打開或新建。若是文件存在,則打開。若是不存在,則失敗。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_OVERWRITE:覆蓋。若是文件存在,則打開並覆蓋其內容。若是文件不存在,這個請求返回失敗。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_OVERWRITE_IF:新建或覆蓋。若是要打開的文件已存在,則打開它,並覆蓋其內存。若是不存在,則簡單的新建新文件。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
FILE_SUPERSEDE:新建或取代。若是要打開的文件已存在。則生成一個新文件替代之。若是不存在,則簡單的生成新文件。
請聯繫上面的IoStatusBlock參數中的Information的說明。
最後一個重要的參數是CreateOptions。在慣常的編程中,筆者使用 FILE_NON_DIRECTORY_FILE|FILE_SYNCHRONOUS_IO_NONALERT。此時文件被同步的打開。並且打開的是文件 (而不是目錄。建立目錄請用FILE_ DIRECTORY_FILE)。所謂同步的打開的意義在於,之後每次操做文件的時候,好比寫入文件,調用ZwWriteFile,在 ZwWriteFile返回時,文件寫操做已經獲得了完成。而不會有返回STATUS_PENDING(未決)的狀況。在非同步文件的狀況下,返回未決是 常見的。此時文件請求沒有完成,使用者須要等待事件來等待請求的完成。固然,好處是使用者能夠先去作別的事情。
要同步打開,前面的DesiredAccess必須含有SYNCHRONIZE。
此外還有一些其餘的狀況。好比不經過緩衝操做文件。但願每次讀寫文件都是直接往磁盤上操做的。此時CreateOptions中應該帶標記 FILE_NO_INTERMEDIATE_BUFFERING。帶了這個標記後,請注意操做文件每次讀寫都必須以磁盤扇區大小(最多見的是512字節) 對齊。不然會返回錯誤。
這個函數是如此的繁瑣,以致於再多的文檔也不如一個能夠利用的例子。早期筆者調用這個函數每每由於參數設置不對而致使打開失敗。很是渴望找到一個實際能夠使用的參數的範例。下面舉例以下:
// 要返回的文件句柄
HANDLEfile_handle = NULL;
// 返回值
NTSTATUSstatus;
// 首先初始化含有文件路徑的OBJECT_ATTRIBUTES
OBJECT_ATTRIBUTESobject_attributes;
UNICODE_STRINGufile_name = RTL_CONST_STRING(L」\\??\\C:\\a.dat」);
InitializeObjectAttributes(
&object_attributes,
&ufile_name,
OBJ_CASE_INSENSITIVE|OBJ_KERNEL_HANDLE,
NULL,
NULL);
// 以OPEN_IF方式打開文件。
status= ZwCreateFile(
&file_handle,
GENERIC_READ | GENERIC_WRITE,
&object_attributes,
&io_status,
NULL,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ,
FILE_OPEN_IF,
FILE_NON_DIRECTORY_FILE |
FILE_RANDOM_ACCESS |
FILE_SYNCHRONOUS_IO_NONALERT,
NULL,
0);
值得注意的是路徑的寫法。並非像應用層同樣直接寫「C:\\a.dat」。而是寫成了「\\??\\C:\\a.dat」。這是由於ZwCreateFile使用的是對象路徑。「C:」是一個符號連接對象。符號連接對象通常都在「\\??\\」路徑下。
這種文件句柄的關閉很是簡單。調用ZwClose便可。內核句柄的關閉不須要和打開在同一進程中。示例以下:
ZwClose(file_handle);
3.3 文件的讀寫操做
打開文件以後,最重要的操做是對文件的讀寫。讀與寫的方法是對稱的。只是參數輸入與輸出的方向不一樣。讀取文件內容通常用ZwReadFile,寫文件通常使用ZwWriteFile。
NTSTATUS
ZwReadFile(
IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID Buffer,
IN ULONG Length,
IN PLARGE_INTEGER ByteOffset OPTIONAL,
INPULONG Key OPTIONAL);
FileHandle:是前面ZwCreateFile成功後所獲得的FileHandle。若是是內核句柄,ZwReadFile和ZwCreateFile並不須要在同一個進程中。句柄是各進程通用的。
Event :一個事件。用於異步完成讀時。下面的舉例始終用同步讀,因此忽略這個參數。請始終填寫NULL。
ApcRoutine Apc:回調例程。用於異步完成讀時。下面的舉例始終用同步讀,因此忽略這個參數。請始終填寫NULL。
IoStatusBlock:返回結果狀態。同ZwCreateFile中的同名參數。
Buffer:緩衝區。若是讀文件的內容成功,則內容被被讀到這個緩衝裏。
Length:描述緩衝區的長度。這個長度也就是試圖讀取文件的長度。
ByteOffset:要讀取的文件的偏移量。也就是要讀取的內容在文件中的位置。通常的說,不要設置爲NULL。文件句柄不必定支持直接讀取當前偏移。
Key:讀取文件時用的一種附加信息,通常不使用。設置NULL。
返 回值:成功的返回值是STATUS_SUCCESS。只要讀取到任意多個字節(無論是否符合輸入的Length的要求),返回值都是 STATUS_SUCCESS。即便試圖讀取的長度範圍超出了文件原本的大小。可是,若是僅讀取文件長度以外的部分,則返回 STATUS_END_OF_FILE。
ZwWriteFile 的參數與ZwReadFile徹底相同。固然,除了讀寫文件外,有的讀者可能會問是否提供一個ZwCopyFile用來拷貝一個文件。這個要求未能被滿 足。若是有這個需求,這個函數必須本身來編寫。下面是一個例子,用來拷貝一個文件。利用到了ZwCreateFile,ZwReadFile和 ZwWrite這三個函數。不過做爲本節的例子,只舉出ZwReadFile和ZwWriteFile的部分:
NTSTATUS MyCopyFile(
PUNICODE_STRING target_path,
PUNICODE_STRING source_path)
{
// 源和目標的文件句柄
HANDLE target = NULL,source = NULL;
// 用來拷貝的緩衝區
PVOID buffer = NULL;
LARGE_INTEGER offset = { 0 };
IO_STATUS_BLOCK io_status = { 0 };
do {
// 這裏請用前一小節說到的例子打開target_path和source_path所對應的
// 句柄target和source,併爲buffer分配一個頁面也就是4k的內存。
… …
// 而後用一個循環來讀取文件。每次從源文件中讀取4k內容,而後往
// 目標文件中寫入4k,直到拷貝結束爲止。
while(1) {
length = 4*1024; //每次讀取4k。
// 讀取舊文件。注意status。
status = ZwReadFile (
source,NULL,NULL,NULL,
&my_io_status,buffer, length,&offset,
NULL);
if(!NT_SUCCESS(status))
{
// 若是狀態爲STATUS_END_OF_FILE,則說明文件
// 的拷貝已經成功的結束了。
if(status == STATUS_END_OF_FILE)
status = STATUS_SUCCESS;
break;
}
// 得到實際讀取到的長度。
length = IoStatus.Information;
// 如今讀取了內容。讀出的長度爲length.那麼我寫入
// 的長度也應該是length。寫入必須成功。若是失敗,
// 則返回錯誤。
status = ZwWriteFile(
target,NULL,NULL,NULL,
&my_io_status,
buffer,length,&offset,
NULL);
if(!NT_SUCCESS(status))
break;
// offset移動,而後繼續。直到出現STATUS_END_OF_FILE
// 的時候才結束。
offset.QuadPart += length;
}
} while(0);
// 在退出以前,釋放資源,關閉全部的句柄。
if(target != NULL)
ZwClose(target);
if(source != NULL)
ZwClose(source);
if(buffer != NULL)
ExFreePool(buffer);
return STATUS_SUCCESS;
}
除了讀寫以外,文件還有不少的操做。好比刪除、從新命名、枚舉。這些操做將在後面實例中用到時,再詳細講解。
第四章 操做註冊表
4.1 註冊鍵的打開操做
和在應用程序中編程的方式相似,註冊表是一個巨大的樹形結構。操做通常都是打開某個子鍵。子鍵下有若干個值能夠得到。每個值有一個名字。值有不一樣的類型。通常須要查詢才能得到其類型。
子鍵通常用一個路徑來表示。和應用程序編程的一點重大不一樣是這個路徑的寫法不同。通常應用編程中須要提供一個根子鍵的句柄。而驅動中則所有用路徑表示。相應的有一張表表示以下:
應用編程中對應的子鍵
HKEY_LOCAL_MACHINE
HKEY_USERS
HKEY_CLASSES_ROOT
HKEY_CURRENT_USER
實際上應用程序和驅動程序很大的一個不一樣在於應用程序老是由某個「當前用戶」啓動的。所以能夠直接讀取HKEY_CLASSES_ROOT和 HKEY_CURRENT_USER。而驅動程序和用戶無關,因此直接去打開HKEY_CURRENT_USER也就不符合邏輯了。
打開註冊表鍵使用函數ZwOpenKey。新建或者打開則使用ZwCreateKey。通常在驅動編程中,使用ZwOpenKey的狀況比較多見。下面以此爲例講解。ZwOpenKey的原型以下:
NTSTATUS
ZwOpenKey(
OUT PHANDLE KeyHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes
);
這個函數和ZwCreateFile是相似的。它並不接受直接傳入一個字符串來表示一個子鍵。而是要求輸入一個OBJECT_ATTRIBUTES的指針。如何初始化一個OBJECT_ATTRIBUTES請參考前面的講解ZwCreateFile的章節。
DesiredAccess支持一系列的組合權限。能夠是下表中全部權限的任何組合:
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
KEY_QUERY_VALUE:讀取鍵下的值。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
KEY_SET_VALUE:設置鍵下的值。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
KEY_CREATE_SUB_KEY:生成子鍵。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
KEY_ENUMERATE_SUB_KEYS:枚舉子鍵。
不過實際上能夠用KEY_READ來作爲通用的讀權限組合。這是一個組合宏。此外對應的有KEY_WRITE。若是須要得到所有的權限,能夠使用KEY_ALL_ACCESS。
下面是一個例子,這個例子很是的有實用價值。它讀取註冊表中保存的Windows系統目錄(指Windows目錄)的位置。不過這裏只涉及打開子鍵。並不讀取值。讀取具體的值在後面的小節中再完成。
Windows目錄的位置被稱爲SystemRoot,這一值保存在註冊表中,路徑是「HKEY_LOCAL_MACHINE\SOFTWARE \Microsoft\WindowsNT\CurrentVersion」。固然,請注意注意在驅動編程中的寫法有所不一樣。下面的代碼初始化一個 OBJECT_ATTRIBUTES。
HANDLEmy_key = NULL;
NTSTATUSstatus;
// 定義要獲取的路徑
UNICODE_STRINGmy_key_path =
RTL_CONSTANT_STRING(
L」\\ Registry\\Machine\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion」);
OBJECT_ATTRIBUTEmy_obj_attr = { 0 };
// 初始化OBJECT_ATTRIBUTE
InitializeObjectAttributes(
&my_obj_attr,
&my_key_path,
OBJ_CASE_INSENSITIVE,
NULL,
NULL);
// 接下來是打開Key
status= ZwOpenKey(&my_key,KEY_READ,&my_obj_attr);
if(!NT_SUCCESS(status))
{
// 失敗處理
……
}
上面的代碼獲得了my_key。子鍵已經打開。而後的步驟是讀取下面的SystemRoot值。這在後面一個小節中講述。
4.2 註冊值的讀
通常使用ZwQueryValueKey來讀取註冊表中鍵的值。要注意的是註冊表中的值可能有多種數據類型。並且長度也是沒有定數的。爲此,在讀取過程當中,就可能要面對不少種可能的狀況。ZwQueryValueKey這個函數的原型以下:
NTSTATUSZwQueryValueKey(
INHANDLE KeyHandle,
INPUNICODE_STRING ValueName,
INKEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,
OUTPVOID KeyValueInformation,
INULONG Length,
OUTPULONG ResultLength
);
KeyHandle:這是用ZwCreateKey或者ZwOpenKey所打開的一個註冊表鍵句柄。
ValueName:要讀取的值的名字。
KeyValueInformationClass:本次查詢所須要查詢的信息類型。這有以下的三種可能。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
KeyValueBasicInformation:得到基礎信息,包含值名和類型。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
KeyValueFullInformation:得到完整信息。包含值名、類型和值的數據。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
KeyValuePartialInformation:得到局部信息。包含類型和值數據。
很容易看出實際上名字是已知的,得到基礎信息是畫蛇添足。一樣得到完整信息也是浪費內存空間。由於調用ZwQueryValueKey的目的是爲了獲得類 型和值數據。所以使用KeyValuePartialInformation最多見。當採用KeyValuePartialInformation的時 候,一個類型爲KEY_VALUE_PARTIAL_INFORMATION的結構將被返回到參數KeyValueInformation所指向的內存 中。
KeyValueInformation:當KeyValueInformationClass被設置爲 KeyValuePartialInformation時,KEY_VALUE_PARTIAL_INFORMATION結構將被返回到這個指針所指內存 中。下面是結構KEY_VALUE_PARTIAL_INFORMATION的原型。
typedefstruct _KEY_VALUE_PARTIAL_INFORMATION {
ULONG TitleIndex; //請忽略這個成員
ULONG Type; //數據類型
ULONG DataLength; //數據長度
UCHAR Data[1]; // 可變長度的數據
}KEY_VALUE_PARTIAL_INFORMATION,*PKEY_VALUE_PARTIAL_INFORMATIO;
上面的數據類型Type有不少種可能,可是最多見的幾種以下:
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
REG_BINARY:十六進制數據。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
REG_DWORD:四字節整數。
file:///C:/Users/SHEN/AppData/Local/Temp/msohtml1/03/clip_image002.gif
REG_SZ:以空結束的Unicode字符串。
Length:用戶傳入的輸出空間KeyValueInformation的長度。
ResultLength:返回實際須要的長度。
返回值:若是說實際須要的長度比Length要大,那麼返回STATUS_BUFFER_OVERFLOW或者是STATUS_BUFFER_TOO_SMALL。若是成功讀出了所有數據,那麼返回STATUS_SUCCESS。其餘的狀況,返回一個錯誤碼。
下面請讀者考慮如何把上一小節的函數寫完整。這其中比較常見的一個問題是在讀取註冊表鍵下的值以前,每每不知道這個值有多長。因此有些比較偷懶的程序員總 是定義一個足夠的大小的空間(好比512字節)。這樣的壞處是浪費內存(通常都是在堆棧中定義,而內核編程中堆棧空間被耗盡又是另外一個常見的藍屏問題)。 此外也沒法避免值實際上大於該長度的狀況。爲此應該耐心的首先獲取長度,而後不足時再動態分配內存進行讀取。下面是示例代碼:
// 要讀取的值的名字
UNICODE_STRINGmy_key_name =
RTL_CONSTANT_STRING(L」SystemRoot」);
// 用來試探大小的key_infor
KEY_VALUE_PARTIAL_INFORMATIONkey_infor;
// 最後實際用到的key_infor指針。內存分配在堆中
PKEY_VALUE_PARTIAL_INFORMATIONac_key_infor;
ULONGac_length;
……
// 前面已經打開了句柄my_key,下面如此來讀取值:
status= ZwQueryValueKey(
my_key,
&my_key_name,
KeyValuePartialInformation,
&key_infor,
sizeof(KEY_VALUE_PARTIAL_INFORMATION),
&ac_length);
if(!NT_SUCCESS(status)&&
status!= STATUS_BUFFER_OVERFLOW &&
status!= STATUS_BUFFER_TOO_SMALL)
{
// 錯誤處理
…
}
// 若是沒失敗,那麼分配足夠的空間,再次讀取
ac_key_infor= (PKEY_VALUE_PARTIAL_INFORMATION)
ExAllocatePoolWithTag(NonpagedPool,ac_length,MEM_TAG);
if(ac_key_infor== NULL)
{
stauts= STATUS_INSUFFICIENT_RESOURCES;
// 錯誤處理
…
}
status= ZwQueryValueKey(
my_key,
&my_key_name,
KeyValuePartialInformation,
ac_key_infor,
ac_length,
&ac_length);
// 到此爲止,若是status爲STATUS_SUCCESS,則要讀取的數據已經
// 在ac_key_infor->Data中。請利用前面學到的知識,轉換爲
//UNICODE_STRING
……
4.3 註冊值的寫
實際上註冊表的寫入比讀取要簡單。由於這省略了一個嘗試數據的大小的過程。直接將數據寫入便可。寫入值通常使用函數ZwSetValueKey 。這個函數的原型以下:
NTSTATUSZwSetValueKey(
IN HANDLE KeyHandle,
IN PUNICODE_STRING ValueName,
IN ULONG TitleIndex OPTIONAL,
IN ULONG Type,
IN PVOID Data,
IN ULONG DataSize
);
其中的TileIndex參數請始終填入0。
KeyHandle、ValueName、Type這三個參數和ZwQueryValueKey中對應的參數相同。不一樣的是Data和DataSize。 Data是要寫入的數據的開始地址,而DataSize是要寫入的數據的長度。因爲Data是一個空指針,所以,Data能夠指向任何數據。也就是說,不 管Type是什麼,均可以在Data中填寫相應的數據寫入。
ZwSetValueKey的時候,並不須要該Value已經存在。若是該Value已經存在,那麼其值會被此次寫入覆蓋。若是不存在,則會新建一個。下 面的例子寫入一個名字爲「Test」,並且值爲「My Test Value」的字符串值。假設my_key是一個已經打開的子鍵的句柄。
UNICODE_STRINGname = RTL_CONSTANT_STRING(L」Test」);
PWCHARvalue = { L」My Test Value」 };
…
// 寫入數據。數據長度之因此要將字符串長度加上1,是爲了把最後一個空結束符
// 寫入。我不肯定若是不寫入空結束符會不會有錯,有興趣的讀者請本身測試一下。
status= ZwSetValueKey(my_key,
&name,0,REG_SZ,value,(wcslen(value)+1)*sizeof(WCHAR));
if(!NT_SUCCESS(status))
{
// 錯誤處理
……
}
關於註冊表的操做就介紹到這裏了。若是有進一步的需求,建議讀者閱讀WDK相關的文檔。
第五章 時間與定時器
5.1 得到當前滴答數
在編程中,得到當前的系統日期和時間,或者是得到一個從啓動開始的毫秒數,是很常見的需求。得到系統日期和時間每每是爲了寫日誌。得到啓動毫秒數很適合用來作一個隨機數的種子。有時也使用時間相關的函數來尋找程序的性能瓶頸。
熟悉Win32應用程序開發的讀者會知道有一個函數GetTickCount(),這個函數返回系統自啓動以後經歷的毫秒數。在驅動開發中有一個對應的函數KeQueryTickCount(),這個函數的原型以下:
VOID
KeQueryTickCount(
OUTPLARGE_INTEGER TickCount
);
遺憾的是,被返回到TickCount中的並非一個簡單的毫秒數。這是一個「滴答」數。可是一個「滴答」到底爲多長的時間,在不一樣的硬件環境下可能有所不一樣。爲此,必須結合另外一個函數使用。下面這個函數得到一個「滴答」的具體的100納秒數。
ULONG
KeQueryTimeIncrement(
);
得知以上的關係以後,下面的代碼能夠求得實際的毫秒數:
void MyGetTickCount(PULONG msec)
{
LARGE_INTEGER tick_count;
ULONG myinc = KeQueryTimeIncrement();
KeQueryTickCount(&tick_count);
tick_count.QuadPart *= myinc;
tick_count.QuadPart /= 10000;
*msec = tick_count.LowPart;
}
這不是一個簡單的過程。不過所幸的是,如今有代碼能夠拷貝了。
5.2 得到當前系統時間
接下來的一個需求是獲得當前的能夠供人類理解的時間。包括年、月、日、時、分、秒這些要素。在驅動中不能使用諸如CTime之類的MFC類。不過與之對應的有TIME_FIELDS,這個結構中含有對應的時間要素。
KeQuerySystemTime()獲得當前時間。可是獲得的並非當地時間,而是一個格林威治時間。以後請使用ExSystemTimeToLocalTime()轉換能夠當地時間。這兩個函數的原型以下:
VOID
KeQuerySystemTime(
OUT PLARGE_INTEGER CurrentTime
);
VOID
ExSystemTimeToLocalTime(
IN PLARGE_INTEGER SystemTime,
OUT PLARGE_INTEGER LocalTime
);
這兩個函數使用的「時間」都是長長整型數據結構。這不是人類能夠閱讀的。必須經過函數RtlTimeToTimeFields轉換爲TIME_FIELDS。這個函數原型以下:
VOID
RtlTimeToTimeFields(
IN PLARGE_INTEGER Time,
IN PTIME_FIELDS TimeFields
);
讀者須要實際應用一下來加深印象。下面寫出一個函數:這個函數返回一個字符串。這個字符串寫出當前的年、月、日、時、分、秒,這些數字之間用「-」號隔開。這是一個頗有用的函數。並且同時用到上面三個函數,此外,請讀者回憶前面關於字符串的打印的相關章節。
{
LARGE_INTEGER snow,now;
TIME_FIELDS now_fields;
static WCHAR time_str[32] = { 0 };
// 得到標準時間
KeQuerySystemTime(&snow);
// 轉換爲當地時間
ExSystemTimeToLocalTime(&snow,&now);
// 轉換爲人類能夠理解的時間要素
RtlTimeToTimeFields(&now,&now_fields);
// 打印到字符串中
RtlStringCchPrintfW(
time_str,
32*2,
L"%4d-%2d-%2d %2d-%2d-%2d",
now_fields.Year,now_fields.Month,now_fields.Day,
now_fields.Hour,now_fields.Minute,now_fields.Second);
return time_str;
}
請注意time_str是靜態變量。這使得這個函數不具有多線程安全性。請讀者考慮一下,如何保證多個線程同時調用這個函數的時候,不出現衝突?
5.3 使用定時器
使用過Windows應用程序編程的讀者的讀者必定對SetTimer()映像尤深。當須要定時執行任務的時候,SetTimer()變得很是重要。這個 功能在驅動開發中能夠經過一些不一樣的替代方法來實現。比較經典的對應是KeSetTimer(),這個函數的原型以下:
BOOLEAN
KeSetTimer(
IN PKTIMER Timer, // 定時器
IN LARGE_INTEGER DueTime, // 延後執行的時間
IN PKDPC Dpc OPTIONAL // 要執行的回調函數結構
);
其中的定時器Timer和要執行的回調函數結構Dpc都必須先初始化。其中Timer的初始化比較簡單。下面的代碼能夠初始化一個Timer:
KTIMERmy_timer;
KeInitializeTimer(&my_timer);
Dpc的初始化比較麻煩。這是由於須要提供一個回調函數。初始化Dpc的函數原型以下:
VOID
KeInitializeDpc(
IN PRKDPC Dpc,
IN PKDEFERRED_ROUTINE DeferredRoutine,
IN PVOID DeferredContext
);
PKDEFERRED_ROUTINE這個函數指針類型所對應的函數的類型其實是這樣的:
VOID
CustomDpc(
IN struct _KDPC *Dpc,
IN PVOID DeferredContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2
);
讀者須要關心的只是DeferredContext。這個參數是KeInitializeDpc調用時傳入的參數。用來提供給CustomDpc被調用的時候,讓用戶傳入一些參數。
至於後面的SystemArgument1和SystemArgument2則請不要理會。Dpc是回調這個函數的KDPC結構。
請注意這是一個「延時執行」的過程。而不是一個定時執行的過程。所以每次執行了以後,下次就不會再被調用了。若是想要定時反覆執行,就必須在每次CustomDpc函數被調用的時候,再次調用KeSetTimer,來保證下次還能夠執行。
值得注意的是,CustomDpc將運行在APC中斷級。所以並非全部的事情均可以作(在調用任何內核系統函數的時候,請注意WDK說明文檔中標明的中斷級要求。)
這些事情很是的煩惱,所以要徹底實現定時器的功能,須要本身封裝一些東西。下面的結構封裝了所有須要的信息:
// 內部時鐘結構
typedef struct MY_TIMER_
{
KDPC dpc;
KTIMER timer;
PKDEFERRED_ROUTINE func;
PVOID private_context;
}MY_TIMER,*PMY_TIMER;
// 初始化這個結構:
voidMyTimerInit(PMY_TIMER timer, PKDEFERRED_ROUTINE func)
{
//請注意,我把回調函數的上下文參數設置爲timer,爲何要
//這樣作呢?
KeInitializeDpc(&timer->dpc,sf_my_dpc_routine,timer);
timer->func = func;
KeInitializeTimer(&timer->timer);
return (wd_timer_h)timer;
}
// 讓這個結構中的回調函數在n毫秒以後開始運行:
BOOLEAN MyTimerSet(PMY_TIMER timer,ULONG msec,PVOID context)
{
LARGE_INTEGER due;
//注意時間單位的轉換。這裏msec是毫秒。
due.QuadPart = -10000*msec;
//用戶私有上下文。
timer->private_context = context;
return KeSetTimer(&timer->timer,due,&mytimer->dpc);
};
// 中止執行
VOIDMyTimerDestroy(PMY_TIMER timer)
{
KeCancelTimer(&mytimer->timer);
};
使用結構PMY_TIMER已經比結合使用KDPC和KTIMER簡便許多。可是仍是有一些要注意的地方。真正的OnTimer回調函數中,要得到上下 文,必需要從timer->private_context中得到。此外,OnTimer中還有必要再次調用MyTimerSet(),來保證下次 依然獲得執行。
VOID
MyOnTimer (
IN struct _KDPC *Dpc,
IN PVOID DeferredContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2
)
{
//這裏傳入的上下文是timer結構,用來下次再啓動延時調用
PMY_TIMER timer = (PMY_TIMER)DeferredContext;
//得到用戶上下文
PVOID my_context = timer->private_context;
//在這裏作OnTimer中要作的事情
……
//再次調用。這裏假設每1秒執行一次
MyTimerSet(timer,1000,my_context);
};
關於定時器就介紹到這裏了。
第六章 內核線程
6.1 使用線程
有時候須要使用線程來完成一個或者一組任務。這些任務可能耗時過長,而開發者又不想讓當前系統中止下來等待。在驅動中中止等待很容易使整個系統陷入「停 頓」,最後可能只能重啓電腦。但一個單獨的線程長期等待,還不至於對系統形成致命的影響。另外一些任務是但願長期、不斷的執行,好比不斷寫入日誌。爲此啓動 一個特殊的線程來執行它們是最好的方法。
在驅動中生成的線程通常是系統線程。系統線程所在的進程名爲「System」。用到的內核API函數原型以下:
NTSTATUS
PsCreateSystemThread(
OUT PHANDLE ThreadHandle,
IN ULONG DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle OPTIONAL,
OUT PCLIENT_ID ClientId OPTIONAL,
IN PKSTART_ROUTINE StartRoutine,
IN PVOID StartContext);
這個函數的參數也不少。不過做者本人的使用經驗以下:ThreadHandle用來返回句柄。放入一個句柄指針便可。DesiredAccess老是填寫 0。後面三個參數都填寫NULL。最後的兩個參數一個用於改線程啓動的時候執行的函數。一個用於傳入該函數的參數。
下面要關心的就是那個啓動函數的原型。這個原型比起定時器回調函數卻是異常的簡單,沒有任何多餘的東西:
VOID CustomThreadProc(INPVOID context)
能夠傳入一個參數,就是那個context。context就是PsCreateSystemThread中的StartContext。值得注意的是, 線程的結束應該在線程中本身調用PsTerminateSystemThread來完成。此外獲得的句柄也必需要用ZwClose來關閉。可是請注意:關 閉句柄並不結束線程。
下面舉一個例子。這個例子傳遞一個字符串指針到一個線程中打印一下。而後結束該線程。固然打印字符串這種事情沒有必要單獨開一個線程來作。這裏只是一個簡單的示例。請注意,這個代碼中有一個隱藏的錯誤,請讀者指出這個錯誤是什麼:
// 個人線程函數。傳入一個參數,這個參數是一個字符串。
VOIDMyThreadProc(PVOID context)
{
PUNICODE_STRINGstr = (PUNICODE_STRING)context;
// 打印字符串
KdPrint((「PrintInMyThread:%wZ\r\n」,str));
// 結束本身。
PsTerminateSystemThread(STATUS_SUCCESS);
}
VOIDMyFunction()
{
UNICODE_STRINGstr = RTL_CONSTANT_STRING(L「Hello!」);
HANDLEthread = NULL;
NTSTATUSstatus;
status= PsCreateSystemThread(
&thread,0L,NULL,NULL,NULL,MyThreadProc,(PVOID)&str);
if(!NT_SUCCESS(status))
{
//錯誤處理。
…
}
// 若是成功了,能夠繼續作本身的事。以後獲得的句柄要關閉
ZwClose(thread);
}
以上錯誤之處在於:MyThreadProc執行的時候,MyFunction可能已經執行完畢了。執行完畢以後,堆棧中的str已經無效。此時再執行KdPrint去打印str必定會藍屏。這也是一個很是隱蔽,可是很是容易犯下的錯誤。
合理的方法是是在堆中分配str的空間。或者str必須在全局空間中。請讀者本身寫出正確的方法。
可是讀者會發現,以上的寫法在正確的代碼中也是常見的。緣由是這樣作的時候,在PsCreateSystemThread結束以後,開發者會在後面加上一個等待線程結束的語句。
這樣就沒有任何問題了,由於在這個線程結束以前,這個函數都不會執行完畢,因此棧內存空間不會失效。
這樣作的目的通常不是爲了讓任務併發。而是爲了利用線程上下文環境而作的特殊處理。好比防止重入等等。在後面的章節讀者會學到這方面的技巧。
如何等待線程結束在後面1.6.3「使用事件通知」中進一步的講述。
6.2 在線程中睡眠
許多讀者必定使用過Sleep函數。這能使程序停下一段時間。許多須要連續、長期執行,可是又不但願佔太多CPU使用率的任務,能夠在中間加入睡眠。這樣能使CPU使用率大大下降。即便睡眠的時間很是短(幾十個毫秒)。
在驅動中也能夠睡眠。使用到的內核函數的原型以下:
NTSTATUS
KeDelayExecutionThread(
IN KPROCESSOR_MODE WaitMode,
IN BOOLEAN Alertable,
IN PLARGE_INTEGER Interval);
這個函數的參數簡單明瞭。WaitMode請老是填寫KernelMode,由於如今是在內核編程中使用。Alertable表示是否容許線程報警(用於 從新喚醒)。可是目前沒有必要用到這麼高級的功能,請老是填寫FALSE。剩下的就是Interval了,代表要睡眠多久。
可是這個看似簡單的參數說明起來卻異常的複雜。爲此做者建議讀者使用下面簡單的睡眠函數,這個函數能夠指定睡眠多少毫秒,而沒有必要本身去換算時間(這個函數中有睡眠時間的轉換):
#defineDELAY_ONE_MICROSECOND (-10)
#defineDELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000)
VOIDMySleep(LONG msec)
{
LARGE_INTEGERmy_interval;
my_interval.QuadPart= DELAY_ONE_MILLISECOND;
my_interval.QuadPart*= msec;
KeDelayExecutionThread(KernelMode,0,&my_interval);
}
固然要睡眠幾秒也是能夠的,1毫秒爲千分之一秒。因此乘以1000就能夠表示秒數。
在一個線程中用循環進行睡眠,也能夠實現一個本身的定時器。考慮前面說的定時器的缺點:中斷級較高,有一些事情不能作。在線程中用循環睡眠,每次睡眠結束 以後調用本身的回調函數,也能夠起到相似的效果。並且系統線程執行中是Passive中斷級。睡眠以後依然是這個中斷級,因此不像前面提到的定時器那樣有 限制。
請讀者本身寫出用線程+睡眠來實現定時器的例子。
6.3 使用事件通知
一些讀者可能熟悉「事件驅動」編程技術。可是這裏的「事件」與之不一樣。內核中的事件是一個數據結構。這個結構的指針能夠看成一個參數傳入一個等待函數中。 若是這個事件不被「設置」,則這個等待函數不會返回,這個線程被阻塞。若是這個事件被「設置」,則等待結束,能夠繼續下去。
這經常用於多個線程之間的同步。若是一個線程須要等待另外一個線程完成某過後才能作某事,則能夠使用事件等待。另外一個線程完成後設置事件便可。
這個數據結構是KEVENT。讀者沒有必要去了解其內部結構。這個結構老是用KeInitlizeEvent初始化。這個函數原型以下:
VOID
KeInitializeEvent(
IN PRKEVENT Event,
IN EVENT_TYPE Type,
IN BOOLEAN State
);
第一個參數是要初始化的事件。第二個參數是事件類型,這個詳見於後面的解釋。第三個參數是初始化狀態。通常的說設置爲FALSE。也就是未設狀態。這樣等待者須要等待設置以後才能經過。
事件不須要銷燬。
設置事件使用函數KeSetEvent。這個函數原型以下:
LONG
KeSetEvent(
IN PRKEVENT Event,
IN KPRIORITY Increment,
IN BOOLEAN Wait
);
Event是要設置的事件。Increment用於提高優先權。目前設置爲0便可。Wait表示是否後面立刻緊接着一個KeWaitSingleObject來等待這個事件。通常設置爲TRUE。(事件初始化以後,通常就要開始等待了。)
使用事件的簡單代碼以下:
// 定義一個事件
KEVENTevent;
// 事件初始化
KeInitializeEvent(&event,SynchronizationEvent,TRUE);
……
// 事件初始化以後就能夠使用了。在一個函數中,你能夠等待某
// 個事件。若是這個事件沒有被人設置,那就會阻塞在這裏繼續
// 等待。
KeWaitForSingleObject(&event,Executive,KernelMode,0,0);
……
// 這是另外一個地方,有人設置這個事件。只要一設置這個事件,
// 前面等待的地方,將繼續執行。
KeSetEvent(&event);
因爲在KeInitializeEvent中使用了SynchronizationEvent,致使這個事件成爲所謂的「自動重設」事件。一個事件若是被 設置,那麼全部KeWaitForSingleObject等待這個事件的地方都會經過。若是要能繼續重複使用這個時間,必須重設(Reset)這個事 件。當KeInitializeEvent中第二個參數被設置爲NotificationEvent的時候,這個事件必需要手動重設才能使用。手動重設使 用函數KeResetEvent。
LONG
KeResetEvent(
IN PRKEVENT Event
);
若是這個事件初始化的時候是SynchronizationEvent事件,那麼只有一個線程的KeWaitForSingleObject能夠經過。通 過以後被自動重設。那麼其餘的線程就只能繼續等待了。這能夠起到一個同步做用。因此叫作同步事件。不能起到同步做用的是通知事件 (NotificationEvent)。請注意不能用手工設置通知事件的方法來取代同步事件。請讀者思考一下這是爲何。
回憶前面的1.6.1 「使用線程」的最後的例子。在那裏曾經有一個需求:就是等待線程中的函數KdPrint結束以後,外面生成線程的函數再返回。 這能夠經過一個事件來實 現:線程中打印結束以後,設置事件。外面的函數再返回。爲了編碼簡單我使用了一個靜態變量作事件。這種方法在線程同步中用得極多,請務必熟練掌握:
staticKEVENT s_event;
// 個人線程函數。傳入一個參數,這個參數是一個字符串。
VOIDMyThreadProc(PVOID context)
{
PUNICODE_STRINGstr = (PUNICODE_STRING)context;
KdPrint((「PrintInMyThread:%wZ\r\n」,str));
KeSetEvent(&s_event); // 在這裏設置事件。
PsTerminateSystemThread(STATUS_SUCCESS);
}
// 生成線程的函數:
VOIDMyFunction()
{
UNICODE_STRINGstr = RTL_CONSTANT_STRING(L「Hello!」);
HANDLEthread = NULL;
NTSTATUSstatus;
KeInitializeEvent(&event,SynchronizationEvent,TRUE); // 初始化事件
status= PsCreateSystemThread(
&thread,0L,NULL,NULL,NULL,MyThreadProc,(PVOID)&str);
if(!NT_SUCCESS(status))
{
//錯誤處理。
…
}
ZwClose(thread);
// 等待事件結束再返回:
KeWaitForSingleObject(&s_event,Executive,KernelMode,0,0);
}
實際上等待線程結束並不必定要用事件。線程自己也能夠看成一個事件來等待。可是這裏爲了演示事件的用法而使用了事件。以上的方法調用線程則沒必要擔憂str 的內存空間會無效了。由於這個函數在線程執行完KdPrint以後才返回。缺點是這個函數不能起到併發執行的做用。
第七章 驅動與設備
7.1 驅動入口與驅動對象
驅動開發程序員所編寫的驅動程序對應有一個結構。這個結構名爲DRIVER_OBJECT。對應一個「驅動程序」。下面的代碼展現的是一個最簡單的驅動程序。
#include<ntddk.h>
NTSTATUS
DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
NTSTATUSstatus = STATUS_UNSUCCESSFUL;
return status;
}
函數DriverEntry是每一個驅動程序中必須的。如同Win32應用程序裏的WinMain。DriverEntry的第一個參數就是一個 DRIVER_OBJECT的指針。這個DRIVER_OBJECT結構就對應當前編寫的驅動程序。其內存是Windows系統已經分配的。
第二個參數RegistryPath是一個字符串。表明一個註冊表子鍵。這個子鍵是專門分配給這個驅動程序使用的。用於保存驅動配置信息到註冊表中。至於讀寫註冊表的方法,請參照前面章節中的內容。
DriverEntry的返回值決定這個驅動的加載是否成功。若是返回爲STATUS_SUCCESS,則驅動將成功加載。不然,驅動加載失敗。
7.2 分發函數與卸載函數
DRIVER_OBJECT中含有分發函數指針。這些函數用來處理髮到這個驅動的各類請求。Windows老是本身調用DRIVER_OBJECT下的分發函數來處理這些請求。因此編寫一個驅動程序,本質就是本身編寫這些處理請求的分發函數。
DRIVER_OBJECT下的分發函數指針的個數爲IRP_MJ_MAXIMUM_FUNCTION。保存在一個數組中。下面的代碼設置全部分發函數的地址爲同一個函數:
NTSTATUS
DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
ULONGi;
for(i=0;i<IRP_MJ_MAXIMUM_FUNCTION;++i)
{
DriverObject->MajorFunctions= MyDispatchFunction;
}
…
}
這個設置當然不難。難的工做都在編寫MyDispatchFunction這個函數上。由於全部的分發函數指針都指向這一個函數,那麼這個函數固然要完成本驅動全部的功能。下面是這個函數的原型。這個原型是Windows驅動編程的規範,不能更改:
NTSTATUSMyDispatchFunction(PDEVICE_OBJECT device,PIRP irp)
{
……
}
這裏出現了DEVICE_OBJECT和IRP這兩大結構。前一個表示一個由本驅動生成的設備對象。後一個表示一個系統請求。也就是說,如今要處理的是:發給設備device的請求irp。請完成這個處理吧。這兩個結構在後面再進一步描述。
還有一個不放在分發函數數組中的函數,稱爲卸載函數也很是重要。若是存在這個函數,則該驅動程序能夠動態卸載。在卸載時,該函數會被執行。該函數原型以下:
VOIDMyDriverUnload(PDRIVER_OBJECT driver)
{
……
}
這個函數的地址設置到DriverObject->DriverUnload便可。
因爲沒有返回值,因此實際上在DriverUnload中,已經沒法決定這個驅動可否卸載。只能作善後處理。
7.3 設備與符號連接
驅動程序和系統其餘組件之間的交互是經過給設備發送或者接受發給設備的請求來交互的。換句話說,一個沒有任何設備的驅動是不能按規範方式和系統交互的。固然也不會收到任何IRP,分發函數也失去了意義。
但並不意味着這樣的驅動程序不存在。若是一個驅動程序只是想寫寫日誌文件、Hook某些內核函數或者是作一些其餘的小動做,也能夠不生成任何設備,也不須要關心分發函數的設置。
若是驅動程序要和應用程序之間通訊,則應該生成設備。此外還必須爲設備生成應用程序能夠訪問的符號連接。下面的驅動程序生成了一個設備,並設置了分發函數:
#include<ntifs.h> // 之因此用ntifs.h而不是ntddk.h是由於我習慣開發文件
//系統驅動,實際上目前對讀者來講這兩個頭文件沒區別。
NTSTATUSDriverEntry(
PDRIVER_OBJECTdriver,
PUNICODE_STRINGreg_path)
{
NTSTATUS status;
PDEVICE_OBJECT device;
// 設備名
UNICODE_STRINGdevice_name =
RTL_CONSTANT_STRING("\\Device\\MyCDO");
// 符號連接名
UNICODE_STRINGsymb_link =
RTL_CONSTANT_STRING("\\DosDevices\\MyCDOSL");
// 生成設備對象
status= IoCreateDevice(
driver,
0,
device_name,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&device);
// 若是不成功,就返回。
if(!NT_SUCCESS(status))
returnstatus;
// 生成符號連接
status= IoCreateSymbolicLink(
&symb_link,
&device_name);
if(!NT_SUCCESS(status))
{
IoDeleteDevice(device);
returnstatus;
}
// 設備生成以後,打開初始化完成標記
device->Flags&= ~DO_DEVICE_INITIALIZING;
returnstatus;
}
這個驅動成功加載以後,生成一個名叫「\Device\MyCDO」的設備。而後在給這個設備生成了一個符號連接名字叫作「\DosDevices \MyCDOSL」。應用層能夠經過打開這個符號連接來打開設備。應用層能夠調用CreateFile就像打開文件同樣打開。只是路徑應該是「"\\.\ MyCDOSL」。前面的「\\.\」意味後面是一個符號連接名,而不是一個普通的文件。請注意,因爲C語言中斜槓要雙寫,因此正確的寫法應該是「 \\\\.\\」。與應用層交互的例子在下一節「IRP和IO_STACK_LOCATION」中。
7.4 設備的生成安全性限制
上一節的例子只是簡單的例子。不少狀況下那些代碼會不起做用。爲了不讀者在實際編程中遇到哪些特殊狀況的困繞,下面詳細說明生成設備和符號連接須要注意的地方。生成設備的函數原型以下:
NTSTATUS
IoCreateDevice(
IN PDRIVER_OBJECT DriverObject,
IN ULONG DeviceExtensionSize,
IN PUNICODE_STRING DeviceName OPTIONAL,
IN DEVICE_TYPE DeviceType,
IN ULONG DeviceCharacteristics,
IN BOOLEAN Exclusive,
OUT PDEVICE_OBJECT *DeviceObject
);
這個函數的參數也很是複雜。可是實際上須要注意的並很少。
第一個參數是生成這個設備的驅動對象。
第二個參數DeviceExtensionSize很是重要。因爲分發函數中獲得的老是設備的指針。當用戶須要在每一個設備上記錄一些額外的信息(好比用於 判斷這個設備是哪一個設備的信息、以及不一樣的實際設備所須要記錄的實際信息,好比網卡上數據包的流量、過濾器所綁定真實設備指針等等),須要指定的設備擴展 區內存的大小。若是DeviceExtensionSize設置爲非0,IoCreateDevice會分配這個大小的內存在 DeviceObject->DeviceExtension中。之後用戶就能夠從根據DeviceObject-> DeviceExtension來得到這些預先保存的信息。
DeviceName如前例,是設備的名字。目前生成設備,請老是生成在\Device\目錄下。因此前面寫的名字是「\Device\MyCDO」。其餘路徑也是能夠的,可是這在本書描述範圍以外。
DeviceType表示設備類型。目前的範例無所謂設備類型,因此填寫FILE_DEVICE_UNKNOWN便可。
DeviceCharacteristics目前請簡單的填寫0便可。
Exclusive這個參數必須設置FALSE。文檔沒有作任何解釋。
最後生成的設備對象指針返回到DeviceObject中。
這種設備生成以後,必須有系統權限的用戶才能打開(好比管理員)。因此若是編程者寫了一個普通的用戶態的應用程序去打開這個設備進行交互,那麼不少狀況下能夠(用管理員登陸的時候)。可是偶爾又不行(用普通用戶登陸的時候)。結果困繞好久。實際上是權限問題。
爲了保證交互的成功與安全性,應該用服務程序與之交互。
可是依然有時候必須用普通用戶打開設備。爲了這個目的,設備必須是對全部的用戶開放的。此時不能用IoCreateDevice。必須用IoCreateDeviceSecure。這個函數的原型以下:
NTSTATUS
IoCreateDeviceSecure(
IN PDRIVER_OBJECT DriverObject,
IN ULONG DeviceExtensionSize,
IN PUNICODE_STRING DeviceName OPTIONAL,
IN DEVICE_TYPE DeviceType,
IN ULONG DeviceCharacteristics,
IN BOOLEAN Exclusive,
IN PCUNICODE_STRING DefaultSDDLString,
IN LPCGUID DeviceClassGuid,
OUT PDEVICE_OBJECT *DeviceObject
)
這個函數增長了兩個參數(其餘的沒變)。一個是DefaultSDDLString。這個一個用於描述權限的字符串。描述這個字符串的格式須要大量的篇幅。可是沒有這個必要。字符串「D:P(A;;GA;;;WD)」將知足「人人皆能夠打開」的需求。
另外一個參數是一個設備的GUID。請隨機手寫一個GUID。不要和其餘設備的GUID衝突(不要複製粘貼便可)。
下面是例子:
// 隨機手寫一個GUID
const GUIDDECLSPEC_SELECTANY MYGUID_CLASS_MYCDO =
{0x26e0d1e0L, 0x8189, 0x12e0, {0x99,0x14, 0x08,0x00, 0x22, 0x30, 0x19, 0x03}};
// 全用戶可讀寫權限
UNICODE_STRING sddl =
RLT_CONSTANT_STRING(L"D:P(A;;GA;;;WD)");
// 生成設備
status =IoCreateDeviceSecure( DriverObject,
0,
&device_name,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&sddl,
(LPCGUID)&SFGUID_CLASS_MYCDO,
&device);
使用這個函數的時候,必須鏈接庫wdmsec.lib。
7.5 符號連接的用戶相關性
從前面的例子看,符號連接的命名貌似很簡單。簡單的符號連接(之因此稱爲簡單,是由於還有一種使用GUID的符號連接,這在本書討論範圍以外)老是命名在\DosDevices\之下。可是實際上這會有一些問題。
比較高級的Windows系統(哪一個版本的操做系統很難講,可能必須斷定補丁號),符號連接也帶有用戶相關性。換句話說,若是一個普通用戶建立了符號連接「\DosDevices\MyCDOSL」,那麼,其實其餘的用戶是看不見這個符號連接的。
可是讀者又會發現,若是在DriverEntry中生成符號連接,則全部用戶均可以看見。緣由是DriverEntry老是在進程「System」中執行。系統用戶生成的符號連接是你們均可以看見的。
當前用戶老是取決於當前啓動當前進程的用戶。實際編程中並不必定要在DriverEntry中生成符號連接。一旦在一個不明用戶環境下生成符號連接,就可 能出現註銷而後換用戶登陸以後,符號連接「不見了」的嚴重錯誤。這也是經常讓初學者抓狂幾周都不知道如何解決的一個問題。
其實解決的方案很簡單,任何用戶均可以生成全局符號連接,讓全部其餘用戶都能看見。路徑「\DosDevices\MyCDOSL」改成「\DosDevices\Global\MyCDOSL」便可。
可是在不支持符號連接用戶相關性的系統上,生成「\DosDevices\Global\MyCDOSL」這樣的符號連接是一種錯誤。爲此必須先判斷一下。幸運的是,這個判斷並不難。下面是一個例子,這個例子生成的符號連接老是隨時能夠使用,不用擔憂用戶註銷:
UNICODE_STRINGdevice_name;
UNICODE_STRINGsymbl_name;
if(IoIsWdmVersionAvailable(1, 0x10))
{
// 若是是支持符號連接用戶相關性的版本的系統,用\DosDevices\Global.
RtlInitUnicodeString(&symbl_name,L"\\DosDevices\\Global\\SymbolicLinkName");
}
else
{
// 若是是不支持的,則用\DosDevices
RtlInitUnicodeString(&symbl,L"\\DosDevices\\SymbolicLinkName");
}
// 生成符號連接
IoCreateSymbolicLink(&symbl_name,&device_name);
第八章 處理請求
8.1 IRP與IO_STACK_LOCATION
開發一個驅動要有可能要處理各類IRP。可是本書範圍內,只處理爲了應用程序和驅動交互而產生的IRP。IRP的結構很是複雜,可是目前的需求下沒有必要 去深究它。應用程序爲了和驅動通訊,首先必須打開設備。而後發送或者接收信息。最後關閉它。這至少須要三個IRP:第一個是打開請求。第二個發送或者接收 信息。第三個是關閉請求。
IRP的種類取決於主功能號。主功能號就是前面的說的DRIVER_OBJECT中的分發函數指針數組中的索引。打開請求的主功能號是IRP_MJ_CREATE,而關閉請求的主功能號是IRP_MJ_CLOSE。
若是寫有獨立的處理IRP_MJ_CREATE和IRP_MJ_CLOSE的分發函數,就沒有必要天然判斷IRP的主功能號。若是像前面的例子同樣,使用一個函數處理全部的IRP,那麼首先就要獲得IRP的主功能號。IRP的主功能號在IRP的當前棧空間中。
IRP老是發送給一個設備棧。到每一個設備上的時候擁有一個「當前棧空間」來保存在這個設備上的請求信息。讀者請暫時忽略這些細節。下面的代碼在MyDispatch中得到主功能號,同時展現了幾個常見的主功能號:
NTSTATUSMyDispatchFunction(PDEVICE_OBJECT device,PIRP irp)
{
// 得到當前irp調用棧空間
PIO_STACK_LOCATIONirpsp = IoGetCurrentIrpStackLocation(irp);
NTSTATUSstatus = STATUS_UNSUCCESSFUL;
swtich(irpsp->MajorFunction)
{
// 處理打開請求
caseIRP_MJ_CREATE:
……
break;
// 處理關閉請求
caseIRP_MJ_CLOSE:
……
break;
// 處理設備控制信息
caseIRP_MJ_DEVICE_CONTROL:
……
break;
// 處理讀請求
caseIRP_MJ_READ:
……
break;
// 處理寫請求
caseIRP_MJ_WRITE:
……
break;
default:
…
break;
}
returnstatus;
}
用於與應用程序通訊時,上面這些請求都由應用層API引起。對應的關係大體以下:
應用層調用的API
CreateFile
CloseHandle
DeviceIoControl
ReadFile
WriteFile
瞭解以上信息的狀況下,完成相關IRP的處理,就能夠實現應用層和驅動層的通訊了。具體的編程在緊接後面的兩小節裏完成。
8.2 打開與關閉的處理
若是打開不能成功,則通訊沒法實現。要打開成功,只須要簡單的返回成功就能夠了。在一些有同步限制的驅動中(好比每次只容許一個進程打開設備)編程要更加 複雜一點。可是如今忽略這些問題。暫時認爲咱們生成的設備任何進程均可以隨時打開,不須要擔憂和其餘進程衝突的問題。
簡單的返回一個IRP成功(或者直接失敗)是三部曲,以下:
1. 設置irp->IoStatus.Information爲0。關於Information的描述,請聯繫前面關於IO_STATUS_BLOCK結構的解釋。
2. 設置irp->IoStatus.Status的狀態。若是成功則設置STATUS_SUCCESS,不然設置錯誤碼。
3. 調用IoCompleteRequest (irp,IO_NO_INCREMENT)。這個函數完成IRP。
以上三步完成後,直接返回irp->IoStatus.Status便可。示例代碼以下。這個函數能完成打開和關閉請求。
NTSTATUS
MyCreateClose(
IN PDEVICE_OBJECT device,
IN PIRP irp)
{
irp->IoStatus.Information = 0;
irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest (irp,IO_NO_INCREMENT);
return irp->IoStatus.Status;
}
固然,在前面設置分發函數的時候,應該加上:
DriverObject->MajorFunctions[IRP_MJ_CREATE]= MyCreateClose;
DriverObject->MajorFunctions[IRP_MJ_CLOSE]= MyCreateClose;
在應用層,打開和關閉這個設備的代碼以下:
HANDLE device=CreateFile("\\\\.\\MyCDOSL",
GENERIC_READ|GENERIC_WRITE,0,0,
OPEN_EXISTING,
FILE_ATTRIBUTE_SYSTEM,0);
if (device ==INVALID_HANDLE_VALUE)
{
// …. 打開失敗,說明驅動沒加載,報錯便可
}
// 關閉
CloseHandle(device);
8.3 應用層信息傳入
應用層傳入信息的時候,能夠使用WriteFile,也能夠使用DeviceIoControl。DeviceIoControl是雙向的,在讀取設備的 信息也能夠使用。所以本書以DeviceIoControl爲例子進行說明。DeviceIoControl稱爲設備控制接口。其特色是能夠發送一個帶有 特定控制碼的IRP。同時提供輸入和輸出緩衝區。應用程序能夠定義一個控制碼,而後把相應的參數填寫在輸入緩衝區中。同時能夠從輸出緩衝區獲得返回的更多 信息。
當驅動獲得一個DeviceIoControl產生的IRP的時候,須要瞭解的有當前的控制碼、輸入緩衝區的位置和長度,以及輸出緩衝區的位置和長度。其中控制碼必須預先用一個宏定義。定義的示例以下:
#defineMY_DVC_IN_CODE \
(ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN,\
0xa01,\
METHOD_BUFFERED,\
FILE_READ_DATA|FILE_WRITE_DATA)
其中0xa01這個數字是用戶能夠自定義的。其餘的參數請照抄。
下面是得到這三個要素的例子:
NTSTATUSMyDeviceIoControl(
PDEVICE_OBJECT dev,
PIRP irp)
{
// 獲得irpsp的目的是爲了獲得功能號、輸入輸出緩衝
// 長度等信息。
PIO_STACK_LOCATIONirpsp =
IoGetCurrentIrpStackLocation(irp);
// 首先要獲得功能號
ULONGcode = irpsp->Parameters.DeviceIoControl.IoControlCode;
// 獲得輸入輸出緩衝長度
ULONGin_len =
irpsp->Parameters.DeviceIoControl.InputBufferLength;
ULONGout_len =
irpsp->Parameters.DeviceIoControl.OutputBufferLength;
// 請注意輸入輸出緩衝是公用內存空間的
PVOIDbuffer = irp->AssociatedIrp.SystemBuffer;
// 若是是符合定義的控制碼,處理完後返回成功
if(code== MY_DVC_IN_CODE)
{
…在這裏進行須要的處理動做
//由於不返回信息給應用,因此直接返回成功便可。
//沒有用到輸出緩衝
irp->IoStatus.Information= 0;
irp->IoStatus.Status= STATUS_SUCCESS;
}
else
{
// 其餘的請求不接受。直接返回錯誤。請注意這裏返
// 回錯誤和前面返回成功的區別。
irp->IoStatus.Information = 0;
irp->IoStatus.Status= STATUS_INVALID_PARAMETER;
}
IoCompleteRequest (irp,IO_NO_INCREMENT);
returnirp->IoStatus.Status;
}
在前面設置分發函數的時候,要加上:
DriverObject->MajorFunctions[IRP_MJ_DEVICE_CONTROL]= MyCreateClose;
應用程序方面,進行DeviceIoControl的代碼以下:
HANDLE device=CreateFile("\\\\.\\MyCDOSL",
GENERIC_READ|GENERIC_WRITE,0,0,
OPEN_EXISTING,
FILE_ATTRIBUTE_SYSTEM,0);
BOOLret;
DWORDlength = 0; // 返回的長度
if (device ==INVALID_HANDLE_VALUE)
{
// … 打開失敗,說明驅動沒加載,報錯便可
}
BOOLret = DeviceIoControl(device,
MY_DVC_IN_CODE, // 功能號
in_buffer, // 輸入緩衝,要傳遞的信息,預先填好
in_buffer_len, // 輸入緩衝長度
NULL, // 沒有輸出緩衝
0, // 輸出緩衝的長度爲0
&length, // 返回的長度
NULL);
if(!ret)
{
// … DeviceIoControl失敗。報錯。
}
// 關閉
CloseHandle(device);
8.4 驅動層信息傳出
驅動主動通知應用和應用通知驅動的通道是同一個。只是方向反過來。應用程序須要開啓一個線程調用DeviceIoControl,(調用ReadFile亦可)。而驅動在沒有消息的時候,則阻塞這個IRP的處理。等待有信息的時候返回。
有的讀者可能據說過在應用層生成一個事件,而後把事件傳遞給驅動。驅動有消息要通知應用的時候,則設置這個事件。可是實際上這種方法和上述方法本質相同: 應用都必須開啓一個線程去等待(等待事件)。並且這樣使應用和驅動之間交互變得複雜(須要傳遞事件句柄)。這毫無必要。
讓應用程序簡單的調用DeviceIoControl就能夠了。當沒有消息的時候,這個調用不返回。應用程序自動等待(至關於等待事件)。有消息的時候這個函數返回。並從緩衝區中讀到消息。
實際上,驅動內部要實現這個功能,仍是要用事件的。只是不用在應用和驅動之間傳遞事件了。
驅動內部須要製做一個鏈表。當有消息要通知應用的時候,則把消息放入鏈表中(請參考前面的「使用LIST_ENTRY」),並設置事件(請參考前面的「使 用事件」)。在DeviceIoControl的處理中等待事件。下面是一個例子:這個例子展現的是驅動中處理DeviceIoControl的控制碼爲 MY_DVC_OUT_CODE的部分。實際上驅動若是有消息要通知應用,必須把消息放入隊列尾並設置事件g_my_notify_event。 MyGetPendingHead得到第一條消息。請讀者用之前的知識本身完成其餘的部分。
NTSTATUSMyDeviceIoCtrlOut(PIRP irp,ULONG out_len)
{
MY_NODE*node;
ULONGpack_len;
// 得到輸出緩衝區。
PVOIDbuffer = irp->AssociatedIrp.SystemBuffer;
// 從隊列中取得第一個。若是爲空,則等待直到不爲空。
while((node= MyGetPendingHead()) == NULL)
{
KeWaitForSingleObject(
&g_my_notify_event,//一個用來通知有請求的事件
Executive,KernelMode,FALSE,0);
}
// 有請求了。此時請求是node。得到PACK要多長。
pack_len= MyGetPackLen(node);
if(out_len< pack_len)
{
irp->IoStatus.Information= pack_len; // 這裏寫須要的長度
irp->IoStatus.Status =STATUS_INVALID_BUFFER_SIZE;
IoCompleteRequest (irp,IO_NO_INCREMENT);
return irp->IoStatus.Status;
}
// 長度足夠,填寫輸出緩衝區。
MyWritePackContent(node,buffer);
// 頭節點被髮送出去了,能夠刪除了
MyPendingHeadRemove();
// 返回成功
irp->IoStatus.Information= pack_len; // 這裏寫填寫的長度
irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest (irp,IO_NO_INCREMENT);
return irp->IoStatus.Status;
}
這個函數的處理要追加到MyDeviceIoControl中。以下:
NTSTATUSMyDeviceIoControl(
PDEVICE_OBJECT dev,
PIRP irp)
{
…
if(code== MY_DVC_OUT_CODE)
returnMyDeviceIoCtrlOut(dev,irp);
…
}
在這種狀況下,應用能夠循環調用DeviceIoControl,來取得驅動驅動通知它的信息。
後記:個人閒言碎語
寫這本小冊子的時候,我正在NED-LS辦離職手續。
想當初在東京的時候,NED的田上每夜好酒好肉的招待。北京幾個同事跳槽,搞得項目特別尷尬。田上特地說道:「拜託大家不要轉職......」。
半年不到,我就拋出一紙辭職信,真是負心人啊......
一眨眼間,就在NED-LS混了三年了。不是我非要走人,一方面匯率節節攀升,外包愈來愈困難。歐美企業不退反進,紛紛把更高檔的玩意搬來國內來發。許多日本公司卻還把外包當主業。另外一方面我日語暴爛,又不願學。繼續擺爛顯然不是辦法。
相對於Intel,我其實一直是看好AMD的。上次幫小D選筆記本,還特地選了AMD的CPU。都說AMD的東西便宜量又足,最適合國內的勞苦大衆,這可 不是吹的。只惋惜貌似每次都被Intel揍得鼻青臉腫。因此我便去面試了。跟我去後面那天去的還有那個寫「碟中諜虛擬光驅」的說話有點像唐僧的萬春。
上海的AMD在浦東的荒郊。先把2號線坐到終點站,而後打的到一處無人知道的野外。只看見長長的公路和茂盛的野草。兩邊有無數片工地,橫七豎八的堆着許多建築材料,卻沒有一我的。好處是內急的時候不用找公廁(也不可能找獲得)。直接在路邊就能夠解決。
工地的旁邊有一部分紅品。AMD的綠色標記就坐落其中。有兩棟樓,都不大,袖珍型的。他們的面試沒有筆試(說原本是有,但我去的時候卷子還沒準備好,就免了)。是四我的輪番上陣,前三個是工程師,最後一個是經理。
我面的是存儲芯片驅 動開發。問到驅動開發相關或者純C語言的問題,我天然是對答如流,這許多年的苦工不是白乾的。恰恰他們對效率頗有興趣,老是不時拋出幾個位運算的妙用之類 的幾個優化題。我只好明說了平時並不怎麼關心效率。因此這個不擅長。待加盟了大家項目組以後,必定好好學習每天向上云云。
最終固然是沒過了。他們Pending了好長一段時間。最終結論是我不適合作硬件驅動開發。由於我之前的經驗比較上層。作虛擬SCSI設備的萬春按理比我 好點,可是他死得更慘:結論是隻在小公司幹過,組織性、紀律性會比較差(其實這也沒說錯==!)。雜牌軍被BS了。
萬春沒多久就去廣州了。真是「浮雲遊子意,落日故人情」啊。不過話撂這兒了,莫怪勞資之後不支持AMD...
個人零八年的春夏真是愜意。作的幾個程序都還沒出大問題。有空還寫寫書,《天書夜讀》扔給出版社了。只惋惜一審再編的沒完沒了。到如今也尚未頭。我又開始寫新書。但不知道怎麼說,由於題目尚未擬好。而後又見到小D,一個不當心就掉到蜜罐子裏了。
後來又面試了幾個。Mavell的面試官實在太強了,無所不知,成功的鄙視了我。MS的面試官是老外。雖然我不懂他說什麼,可是我說的他也未見得明白。
不過MS的職位真的是很棒啊。作Windows Kernel,並且還在上海。一經過立刻先送去美利堅培訓。很美啊。
後來又去面Intel(屢敗屢面==!)。Intel的環境真是不錯。我到的時候正是早上。天氣又好,一我的也沒有,對面是一大片幾乎望不到邊的淺水,長 着人深的草。幾隻長腳的蒼鷺站在水裏。還有一些棕色的像小鴨子的玩意在水裏搶食吃。一些小鳥在空中掠過,發出銅鈴同樣的聲音。Intel就在那邊沼澤地的 對面。比AMD大。幾棟大樓。有上千人在那裏工做。吃飯都在食堂。方圓2千米內沒有飯店。
Intel的面試和AMD很像。沒有筆試。四人輪番上陣。不過他們四我的稍微有些分工。每一個人問的方向都不大同樣。另外一個狀況是他們喜歡給你水筆,而後請直接在白板上寫代碼。還不能寫簡意,非要一行一行寫出來才行。寫白板手抖得厲害,沒點心理素質還真不行。
第一我的問內核編程。問到個人得意之處了。不過這幫人還真的有兩把刷子——他們能看Windows的代碼,我還得本身反彙編。世界真不公平啊。
而後來一我的問了不少設計模式和代碼管理之類的問題。這方面我固然是更口若懸河了。最有挑戰性的是第三人,髮型很像愛因斯坦那個,進來坐定以後,也不說什 麼,就給出一張白紙,讓我寫一個矩形相交判斷以及一個形狀覆蓋的算法。時間又大概只有二十分鐘,大腦一片空白,汗就出來了。空白了大概十分鐘,還一個字沒 寫。面試官都急了,就說:「你若是有什麼中間結果,就先拿出來。」意思就是你多少寫點,別交白卷啊。
不過好歹我之前是作過3D引擎的(雖然作得很爛的說)。慢慢冷靜下來,和他說了計算的步驟。不算優秀也不算高效(十幾分鍾哪裏有空考慮那麼多啊)。可是面 試官說也算是邏輯完整。ok,又出了一道圖論算法題。這時候我已經緩過勁來,五分鐘內輕鬆搞定。愛因斯坦滿意的走了。
最後一個是部門經理。相與言歡。而後他請客在他們食堂吃飯。可是真的很難吃的說。下午回NEC-AS繼續上班,一面構思辭職信的措辭。
告別lu0,告別wowocock。
本書獻給小D。她是我今夏遇到的,生命裏最美的一縷陽光。
譚文 於 2008年端午
(全書完)