- 竹林蹊徑:深入淺出windows驅動開發
- 張佩 馬勇 董鑒源
- 6906字
- 2019-01-04 12:53:45
第1章 Hello World驅動
1.1 從Hello World開始
國外的計算機領域有一個著名的組織,名字叫“計算機器協會”,它在互聯網上專門維護了一個網頁,上面列出了200多種版本的“Hello World”程序,仿佛代碼的羅塞塔石碑。
在使用一門新的編程語言時,人們習慣把“Hello World”作為第一個程序。漸漸地,人們也習慣用類似“Hello World”的程序作為一切程序的第一步,以此喚出程序員心中對于編程樂觀的一面。
如本節的題目一樣,如何編寫驅動程序是本節將要解決的問題。
那么,讓我們看一下驅動程序的“Hello World”吧!
代碼示例1-1 Hello World驅動程序
#001 /* #002 ******************************************************* #003 *= = 文件名稱:Hello World.c #004 *= = 文件描述:驅動程序的Hello World例子 #005 *= = 作 者:竹林蹊徑 #006 *= = 編寫時間:2009-04-23 21:16:00 #007 ******************************************************* #008 */ #009 #010 #include <NTDDK.h> #011 #012 //*==================================================== #013 //*= = 函數名稱:DriverEntry #014 //*= = 功能描述:驅動程序入口函數 #015 //*= = 入口參數:PDRIVER_OBJECT, PUNICODE_STRING #016 //*= = 出口參數:NTSTATUS #017 //*==================================================== #018 #019 NTSTATUS #020 DriverEntry ( #021 __in PDRIVER_OBJECT DriverObject, #022 __in PUNICODE_STRING RegistryPath #023 ) #024 { #025 DbgPrint("Hello, Windows Driver!"); #026 return STATUS_SUCCESS; #027 } #028 #029 //*==================================================== #030 //*= = 文件結束 #031 //*====================================================
本示例代碼可以從本書的\Chapter01\Hello World目錄下找到。
以上就是驅動程序最簡單的“Hello World”例子,去掉代碼注釋部分,放眼程序,全部代碼屈指可數。
由第003行注釋可知,這個例子的文件名稱是Hello World.c,說明此文件是由C語言編寫而成的。
我們知道,操作系統剛出現時是由機器語言和匯編語言編寫的,后來為了可移植性等采用了C語言。早期的一些Windows操作系統也是采用C語言編寫的,而在開發基于NT技術的Windows操作系統時,同時采用了C和C++這兩種高級語言。
那么究竟是采用C語言還是C++語言來開發驅動程序呢?其實兩者各有優缺點,讀者可以在讀本書的時候仔細體會。這里采用C語言編寫了第一個例子,因為用C語言完全可以開發出所有的驅動程序,同時你會有似曾相識的感覺。但是微軟提供了WDF驅動開發模型,這個在后面的章節中會講到,如果需要使用WDF,C++無疑是最好的選擇。
實際上,驅動程序編譯成的二進制文件是SYS類型文件,和普通的EXE類型文件一樣,也是PE格式。PE是Portable Executable File Format的簡寫,是微軟Windows平臺環境下主流的可執行程序標準格式,DLL也是常見的PE格式。所以,使用什么編程語言并不嚴格限定,如果你喜歡,即使用匯編或Delphi也可以開發驅動程序。比如在VxD驅動編程模型盛行時,很多人還是使用匯編開發設備驅動程序,并提供了相應的開發環境;更有甚者提供了EXE類型程序到SYS類型程序的轉換工具,這些工具雖然大多沒有流行起來,但是卻佐證了這種方法的可行性。
不過,微軟提供的內核編程接口和示例只有C/C++的,為了方便起見,我們約定本書使用C/C++語言來開發驅動程序。
開發驅動程序大致和開發普通應用程序一樣,幾乎擁有同樣的流程:分析需求、設計、編碼、調試、測試、發布、維護這幾個主要環節。但是在后幾個環節上,驅動程序的開發和普通應用程序的開發又有著很大的差別。
下面,我們來簡單分析一下代碼。
第010行包含了一個頭文件NTDDK.h,這個頭文件是NT驅動必須包含的一個頭文件,WDM驅動則需要換成WDM.h。
第019~027行,整段代碼只有一個DriverEntry函數。這個函數是所有驅動程序的入口函數,類似于Win32編程下的WinMain函數或C語言的main函數。
第021~022行,函數的兩個參數,分別代表驅動對象的指針和注冊表子鍵的字符串指針。這個暫且不作詳細論述,我們將在下面章節中具體說明。其中,__in是一個宏,代表這個參數是入口參數,常見的還有__out,代表出口參數。
第025~026行,再看函數里面,我們看到只有兩個語句,是不是很熟悉呢?
DbgPrint是一個函數,類似于C語言的printf函數,打印一串字符串,打印的內容為“Hello, Windows Driver!”。隨后函數返回一個值STATUS_SUCCESS,這個值是個宏,從字面意思可知它代表成功,語句類似于main函數的“return 0;”。這里需要注意的是,打印的內容無法通過控制臺查看,需要借助其他工具才能看到。
那么如何對這個程序進行編譯呢?
我們需要一個開發環境,這個開發環境名為WDK,微軟已經為我們提供了。關于開發環境的詳細介紹,請參閱本書第2章。
一切都那么自然!下面對這個例子進行簡單的擴充。
1.1.1 HelloDRIVER
這里是一個關于Hello World驅動程序示例代碼的簡單擴充。如果完全看不懂,那就直接跳到1.1.2節看代碼解釋吧!
代碼示例1-2驅動程序HelloDRIVER聲明文件
#001 /* #002 ***************************************************************** #003 *= = 文件名稱:HelloDRIVER.h #004 *= = 文件描述:關于HelloDRIVER的頭文件 #005 *= = 作 者:竹林蹊徑 #006 *= = 編寫時間:2009-04-23 21:16:00 #007 ***************************************************************** #008 */ #009 #010 #ifndef __HELLODRIVER_H__ #011 #define __HELLODRIVER_H__ #012 #013 //*============================================================== #014 //*= = 頭文件聲明 #015 //*============================================================== #016 #017 #include <NTDDK.h> #018 #019 //*============================================================== #020 //*= = 宏與結構體 #021 //*============================================================== #022 #023 typedef struct _DEVICE_EXTENSION { #024 #025 PDEVICE_OBJECT DeviceObject; // 指回設備對象的指針 #026 UNICODE_STRING DeviceName; // 設備名稱 #027 UNICODE_STRING SymbolicLink; // 符號鏈接名 #028 #029 }DEVICE_EXTENSION, *PDEVICE_EXTENSION; #030 #031 //*============================================================== #032 //*= = 函數聲明 #033 //*============================================================== #034 #035 NTSTATUS #036 DriverEntry( #037 __in PDRIVER_OBJECT DriverObject, #038 __in PUNICODE_STRING RegistryPath #039 ); #040 #041 VOID #042 DriverUnload( #043 __in PDRIVER_OBJECT DriverObject #044 ); #045 #046 NTSTATUS #047 DefaultDispatch( #048 __in PDEVICE_OBJECT DeviceObject, #049 __in PIRP Irp #050 ); #051 #052 #endif // End of __HELLODRIVER_H__ #053 #054 //*============================================================== #055 //*= = 文件結束 #056 //*==============================================================
代碼示例1-3驅動程序HelloDRIVER定義文件
#001 /* #002 ***************************************************************** #003 *= = 文件名稱:HelloDRIVER.c #004 *= = 文件描述:驅動程序HelloDRIVER例子 #005 *= = 作 者:竹林蹊徑 #006 *= = 編寫時間:2009-04-23 21:16:00 #007 ***************************************************************** #008 */ #009 #010 #include "HelloDRIVER.h" #011 #012 //*============================================================== #013 //*= = 預處理定義 #014 //*============================================================== #015 #016 #pragma alloc_text(INIT, DriverEntry) #017 #pragma alloc_text(PAGE, DefaultDispatch) #018 #pragma alloc_text(PAGE, DriverUnload) #019 #020 //*============================================================== #021 //*= = 函數名稱:DriverEntry #022 //*= = 功能描述:驅動程序入口函數 #023 //*= = 入口參數:PDRIVER_OBJECT, PUNICODE_STRING #024 //*= = 出口參數:NTSTATUS #025 //*============================================================== #026 #027 NTSTATUS #028 DriverEntry ( #029 __in PDRIVER_OBJECT DriverObject, #030 __in PUNICODE_STRING RegistryPath #031 ) #032 { #033 NTSTATUS status; #034 PDEVICE_OBJECT deviceObject; #035 PDEVICE_EXTENSION deviceExtension; #036 UNICODE_STRING symbolicLink; #037 UNICODE_STRING deviceName; #038 ULONG i; #039 KdPrint(("Enter HelloDRIVER DriverEntry!\n")); #040 #041 UNREFERENCED_PARAMETER(RegistryPath); #042 #043 RtlInitUnicodeString(&deviceName, L"\\Device\\HelloDRIVER"); #044 #045 // 處理派遣例程 #046 for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++) #047 { #048 DriverObject->MajorFunction[i] = DefaultDispatch; #049 } #050 #051 DriverObject->DriverUnload = DriverUnload; #052 DriverObject->MajorFunction[IRP_MJ_CREATE] = DefaultDispatch; #053 DriverObject->MajorFunction[IRP_MJ_CLOSE] = DefaultDispatch; #054 DriverObject->MajorFunction[IRP_MJ_READ] = DefaultDispatch; #055 DriverObject->MajorFunction[IRP_MJ_WRITE] = DefaultDispatch; #056 #057 // 創建設備 #058 status = IoCreateDevice( DriverObject, #059 sizeof(DEVICE_EXTENSION), #060 &deviceName, #061 FILE_DEVICE_UNKNOWN, #062 0, #063 TRUE, #064 &deviceObject); #065 if(!NT_SUCCESS(status)) #066 { #067 return status; #068 } #069 #070 deviceObject->Flags = DO_BUFFERED_IO; #071 deviceExtension = (PDEVICE_EXTENSION)deviceObject->DeviceExtension; #072 deviceExtension->DeviceObject = deviceObject; #073 deviceExtension->DeviceName = deviceName; #074 #075 RtlInitUnicodeString(&symbolicLink, L"\\??\\HelloDRIVER"); #076 deviceExtension->SymbolicLink = symbolicLink; #077 #078 // 創建符號鏈接 #079 status = IoCreateSymbolicLink(&symbolicLink, &deviceName); #080 #081 if(!NT_SUCCESS(status)) #082 { #083 IoDeleteDevice(deviceObject); #084 return status; #085 } #086 #087 KdPrint(("End of HelloDRIVER DriverEntry!\n")); #088 return status; #089 } #090 #091 //*============================================================== #092 //*= = 函數名稱:DriverUnload #093 //*= = 功能描述:驅動程序卸載函數 #094 //*= = 入口參數:PDRIVER_OBJECT #095 //*= = 出口參數:VOID #096 //*============================================================== #097 #098 VOID #099 DriverUnload( #100 __in PDRIVER_OBJECT DriverObject #101 ) #102 { #103 PDEVICE_OBJECT deviceObject; #104 UNICODE_STRING linkName; #105 KdPrint(("Enter HelloDRIVER DriverUnload!\n")); #106 #107 deviceObject = DriverObject->DeviceObject; #108 #109 while(NULL != deviceObject) #110 { #111 PDEVICE_EXTENSION deviceExtesion = \ #112 (PDEVICE_EXTENSION)deviceObject->DeviceExtension; #113 #114 // 刪除符號鏈接與設備 #115 linkName = deviceExtesion->SymbolicLink; #116 IoDeleteSymbolicLink(&linkName); #117 deviceObject = deviceObject->NextDevice; #118 IoDeleteDevice(deviceExtesion->DeviceObject); #119 } #120 #121 KdPrint(("End of HelloDRIVER DriverUnload!\n"));; #122 } #123 #124 //*============================================================== #125 //*= = 函數名稱:DefaultDispatch #126 //*= = 功能描述:驅動程序默認派遣例程 #127 //*= = 入口參數:PDEVICE_OBJECT, PIRP #128 //*= = 出口參數:NTSTATUS #129 //*============================================================== #130 #131 NTSTATUS #132 DefaultDispatch( #133 __in PDEVICE_OBJECT DeviceObject, #134 __in PIRP Irp #135 ) #136 { #137 NTSTATUS status; #138 KdPrint(("Enter HelloDRIVER DefaultDispatch!\n")); #139 #140 UNREFERENCED_PARAMETER(DeviceObject); #141 status = STATUS_SUCCESS; #142 #143 // 完成IRP請求 #144 Irp->IoStatus.Status = status; #145 Irp->IoStatus.Information = 0; #146 IoCompleteRequest(Irp, IO_NO_INCREMENT); #147 #148 KdPrint(("End of HelloDRIVER DefaultDispatch!\n")); #149 return status; #150 } #151 #152 //*============================================================== #153 //*= = 文件結束 #154 //*==============================================================
本示例代碼可以從本書的\Chapter01\HelloDRIVER目錄下找到。
1.1.2 代碼解釋
或許你驚訝于這個簡單的擴充如此之長,其實如果細心看一下,它并不像你想象的那么多。
細心的讀者一定發現了,目前所用的兩個例子都攜帶了完整的程序注釋。注釋對于編程風格來說是很重要的一點,很多程序員對此不以為然,這是很不好的一個習慣。不同的編程人員習慣于不同的編程風格,希望讀者能對此加以重視并形成自己的編程風格。良好的編程風格不僅有利于程序的可讀性,對于程序的質量也起著不容忽視的作用。
希望對編碼質量進一步提高的讀者可以閱讀《編程匠藝——編寫卓越代碼》或者《代碼大全》等書。在之后的代碼示例中,出于編排考慮,將不再貼出完整示例及注釋。
接下來我們仔細看一下示例代碼,并對此加以詳細解釋。
代碼示例1-2是HelloDRIVER驅動程序的聲明文件。這之后,我們對注釋部分不再單獨加以解釋,請讀者自行閱讀。
第010、011、052行,這是C語言中常見的預處理,用來避免頭文件重復包含導致編譯錯誤。另外,也可以使用#pragma once來避免此錯誤,其作用是防止頭文件多次被包含,保證頭文件只被編譯一次,比上個方法的可移植性稍差。
第017行,包含驅動所需的頭文件。
第023~029行,這是一個結構體定義,用以描述驅動程序的設備擴展。它保存了我們自定義所需的一些信息,有助于更加方便地編程。
第35~50行,這是相關的函數聲明。這些函數的具體實現存在于定義文件中,我們在下面加以詳細介紹。
代碼示例1-3是HelloDRIVER驅動程序的定義文件。
第010行,包含指定的聲明文件。為每個定義文件寫一個聲明文件是一個不錯的習慣。
第016~018行,這是一些預處理。在驅動開發中,需要為每一個函數指定其是分頁內存還是非分頁內存。INIT標識是指此函數在驅動加載時使用,是初始化相關的函數,驅動成功加載以后可以從內存卸載。PAGE標識是指此函數在驅動運行時可以被交換到磁盤上。如果不指定,編譯器默認為非分頁內存。
一般情況下,我們不需要考慮這些問題,但是有些特殊情況,代碼是不允許被交換到磁盤上的,否則將導致操作系統藍屏或者自動重啟。這里需要注意一點,那就是函數聲明必須在這些指定內存分配的預處理之前,否則無法通過編譯。
從第027行開始,是DriverEntry函數的具體實現。1.1節我們說過,DriverEntry是驅動程序的入口函數,它由操作系統內核中的I/O管理器調用。
第033~038行,這是函數相關的變量定義。在C語言中,變量不允許被定義在函數流程處理中,也就是說,必須定義在函數體的開始處,否則出現編譯錯誤。在C++語言中則沒有這種限制。
第039行,可以看出KdPrint也是一個字符串打印函數。其實這個函數和上面的DbgPrint是同一個函數,是它的宏定義方式,用以打印調試信息。將其定義為宏的好處在于調試版本打印出具體信息供開發者參考,而在發行版本編譯時完全被移除了,這樣可以減小驅動文件大小并有助于提高程序的運行效率。
這里提一下調試版本和發行版本。在應用程序中調試版本和發行版本分別被稱為Debug版本和Release版本,而在驅動程序中則被稱為Check版本和Free版本。前后兩者除了名字不一樣外,沒什么實質性的差別。而調試版本和發行版本的不同則在于前者包含了大量的調試信息,沒有經過優化,方便開發者尋找程序缺陷和漏洞。
第041行,UNREFERENCED_PARAMETER是一個宏,經常被用來指定參數未被引用,可以避免不必要的警告。
說到警告,很多應用程序員大都不以為然。但是我們希望你在驅動開發中能改掉這個習慣,當然沒有更好。因為驅動程序的崩潰會導致操作系統的崩潰,直接造成死機或藍屏。除非你十分確定警告不會對驅動程序帶來不穩定的因素,否則請修正它,因為做到沒有警告是使驅動更加趨于穩定的一個基礎。
第043行,對一個Unicode字符串進行初始化。Windows內核中大量使用Unicode字符串,其具體操作有一系列函數(詳情請參看MSDN文檔),這一系列函數屬于Rtl系列,也就是微軟推薦使用的運行時函數。
第046~049行,一個循環體。宏IRP_MJ_MAXIMUM_FUNCTION代表驅動程序最大的派遣函數指針數。這里使用一個默認的派遣函數來初始化它們,然后緊跟著在下面修改我們不打算使用默認的派遣函數指針。
這些派遣函數又可以稱為回調函數,由定義實現,提供給操作系統調用。回調函數的意義和應用程序中的沒有差別,只不過在驅動程序中,這些派遣函數是我們的主要工作重點。學習本書的主要任務也會建立在它們的基礎之上。
對于普通的驅動程序,可以不考慮對所有的派遣函數指針進行初始化,但是如果想要實現一個過濾驅動程序,那么請參照以上方式初始化。具體實現方式,請參閱本書關于過濾驅動程序的章節。如果沒有進行全部初始化,編譯器會對未處理的派遣函數指針進行默認處理。
第051行,卸載函數。這個派遣函數必須單獨提供,并且在操作系統版本不同的情況下,這個函數可能需要注意一些不同的東西。如果不打算對驅動程序進行卸載,這個函數可以不用提供。
第052~055行,提供給操作系統的創建、關閉以及讀寫的派遣函數。當然,還有更多的派遣函數需要提供,這里為了簡單,我們使用DefaultDispatch來代替。
第058行,使用IoCreateDevice函數宏創建一個設備對象,其名稱為“HelloDRIVER”。HelloDRIVER的設備類型為“FILE_DEVICE_UNKNOWN”,是一種獨占設備,在運行時只能被一個應用程序所使用。
第065~068行,判斷設備是否創建成功,并進行必要的失敗處理。驅動程序中這樣的處理對于驅動程序的健壯性起著不容忽視的作用。
第070行,設置設備的標識。有BUFFERED_IO和DO_DIRECT_IO兩種,代表了兩種不同的緩沖區處理方式。
第071~076行,這里初始化了一個Unicode字符串,同時也初始化了聲明文件中定義過的設備擴展結構體。設備擴展中保存了我們自定義所需的一些信息。
第079~085行,使用IoCreateSymbolicLink函數宏創建了設備符號鏈接,并對創建結果判斷以進行必要的失敗處理。這個符號鏈接名主要用來與應用程序進行通信。如果創建失敗,則刪除已經創建的設備對象。
驅動程序的設備名稱對應用程序是透明的,所以只能用于內核程序。這也是為什么要創建設備符號鏈接的原因。
從第098行開始,是DriverUnload函數的具體實現,它的功能是刪除設備對象和設備符號鏈接。如果在DriverEntry函數中分配了資源,也要在這里釋放。
第107行,由驅動對象指針參數得到設備對象指針。
第109~119行,遍歷已經創建的所有設備符號鏈接和設備對象,并將其刪除。
從第131行開始,是DefaultDispatch函數的具體實現,它的功能是直接完成了IRP (Input/Output Request Package,輸入輸出請求包)。
第144行,設置IRP的狀態為成功。
第145行,因為打算直接完成IRP,所以操作信息的長度為空,這里將字節處理長度信息設置為0。
第146行,使用IoCompleteRequest函數直接完成IRP。
為了遵循一部分讀者開發程序的習慣,這個示例只使用了一個文件。在編譯程序時,聲明文件是默認被包含到定義文件中的,所以暫時忽略不計。
1.1.3 驅動程序的編譯和安裝
編譯驅動程序需要驅動開發環境。我們還沒有介紹WDK的下載與安裝,所以讀者可以先看看這里的編譯、安裝效果,等閱讀完第2章以后,再回過頭來自己動手實踐,相信你一定可以非常輕松地完成這個任務。
驅動程序的編譯不像開發應用程序一樣,可以簡單地單擊某個菜單或命令按鈕就能通過IDE集成環境完全實現,而是需要使用一種叫做nmake的工具。
打開Hello World目錄,可以看到有一個Makefile文件。文件內容如下:
!INCLUDE $(NTMAKEENV)\makefile.def
這個文件幾乎千篇一律,讀者可以隨便找一個Windows驅動程序的示例拷貝一個。微軟建議不要去修改這個文件,我們最好遵循這個建議。
另外,讀者還能發現文件夾里有一個Sources文件,具體內容如下:
TARGETNAME = Hello World TARGETTYPE = DRIVER TARGETPATH = OBJ SOURCES = Hello World.c
這是一個簡單的Sources文件例子。第1行用來指定驅動編譯后的驅動程序文件名稱;第2行用來指定生成的程序為驅動程序;第3行用來指定編譯后生成文件的存放路徑;最后1行用來指定要編譯的源碼文件。
大部分驅動程序開發人員都知道Sources文件的這種用法,卻很少用它編譯EXE或者DLL。具體論述參閱本書第2章。
這里一定不要寫聲明文件,否則將出現編譯錯誤,錯誤信息提示為“編譯目錄出現錯誤”,而沒有指明具體是什么錯誤。如果犯下這樣的錯誤,一時間很難想到錯誤原因。
如果想要查看編譯輸出的錯誤信息和警告信息,可以從工程目錄里尋找ERR類型文件與WRN類型文件。
打開WDK編譯環境,這個環境是以命令行形式提供的,按如下操作進行即可。
如圖1-1所示,單擊“開始”→“程序”→“Windows Driver Kits”→“WDK6001. 10082”→“Build Environments”→“Windows XP”→“Launch Windows XP x86 Checked Build Environment”,打開WDK編譯環境的命令行,通過DOS命令切換到工程文件的存放目錄,輸入“build”,然后按回車鍵,編譯結果如圖1-2所示。

圖1-1 開始菜單中的WDK

圖1-2 Hello World的編譯結果
編譯鏈接成功的SYS類型文件存放于當前目錄的\objfre_wxp_x86\i386下。
同理,我們用同樣的辦法編譯HelloDRIVER驅動程序。不過這里采用Check版本,選擇“Launch Windows XP x86 Checked Build Environment”。文件生成的目錄會稍微有一點差異,在當前目錄的\objchk_wxp_x86\i386下。
接下來的問題是如何加載驅動程序。文件夾中的HelloDRIVER.sys驅動程序是無法直接加載的,也就是說,無法像運行EXE程序一樣,直接雙擊運行。NT驅動需要有專門的程序來加載,這里使用DriverStudio軟件里的Monitor工具。
打開Monitor軟件,單擊“File”→“Open Driver”,選擇“HelloDRIVER.sys”,然后依次運行Start Driver和Stop Driver,可以看到如圖1-3所示的信息。

圖1-3 HelloDRIVER驅動的加載信息
同樣,我們也可以用這個工具加載Hello World驅動程序。如果使用Monitor工具,可能需要安裝DriverStudio軟件,或者將其單獨取出來。如果讀者嫌麻煩,可以從網上找自己喜歡的驅動加載工具。
這里的驅動程序不需要進行調試和測試,因為我們給出的是已經經過測試與調試之后運行正常的代碼。關于具體調試和測試的知識,我們會在以后的章節中詳細論述。
1.1.4 查看我們的驅動
在前面我們提到,DbgPrint函數打印的字符串無法通過控制臺查看,需要借助于其他的工具。
通過圖1-3可以知道,使用Monitor工具可以查看驅動程序中的打印信息。這里簡單介紹一下另外一個工具——DebugView軟件,它也可以用來查看在驅動程序中打印的調試信息。
打開DebugView軟件,然后利用Monitor加載Hello World程序,可以看到DebugView中顯示了如圖1-4所示的信息。

圖1-4 使用DebugView查看打印信息
程序的“Hello, Windows Driver!”赫然在目!
如果想查看自己的設備驅動,可以按照上面的步驟再次加載HelloDRIVER驅動程序,然后重啟計算機。
右鍵單擊“我的電腦”→選擇“屬性”→“硬件”選項卡→“設備管理器”,打開“設備管理器”窗口,選擇“查看”菜單下的“顯示隱藏的設備”命令,可以看到已經加載成功的HelloDRIVER驅動。
筆者的計算機中顯示的信息如圖1-5所示。

圖1-5 查看HelloDRIVER設備
這是通過操作系統自身看到的,當然,也可以通過其他軟件來查看,比如WinObj工具,如圖1-6所示。注意:本書里提到的工具,在第2章都會進行介紹。

圖1-6 用WinObj查看HelloDRIVER設備
1.2 虛擬環境
1.2.1 使用虛擬環境進行驅動開發
因為內核調試會凍結它所運行的操作系統,并存在各種形式的系統不穩定等弊端,在實際的驅動開發工作中,大多數開發人員只是在最終階段才會在實際的機器上進行開發調試,其余的大部分時間是使用虛擬環境進行驅動開發的。
使用虛擬環境進行開發,還可以節省很多資源,例如,對于沒有能力購買另外一臺PC的人,或者對于頻繁出差需要在便攜環境下進行內核調試的人來說,都提供了不錯的解決方案。
這里通常使用的兩類虛擬環境為VMware虛擬機和Virtual PC虛擬機。
1.2.2 使用VMware虛擬機
VMware Workstation是威睿公司推出的一款商業軟件產品,是桌面虛擬化解決方案的軟件代表,如圖1-7所示。全球不同規模的客戶都依賴它降低成本和運營費用,確保業務持續性以及加強安全性等。

圖1-7 WMware 7.0.1版本下的Windows XP操作系統
VMware虛擬機在7.0版本之后的版本中增加了對Windows 7的支持。所以這里的設置以VMware 7.0以及之后的版本為準,但是配置卻與之前的版本完全一樣。
新建虛擬機之后,單擊菜單項“VM”→“Settings”,會彈出如圖1-8所示的對話框。單擊“Add”按鈕,選擇“Serial Port”,再選擇“Use named pipe”,在第一個編輯框中輸入“\\.\pipe\com_1”,第二項選擇“The end is the server.”,第三項選擇“The other end is a virtual machine.”,然后單擊“OK”按鈕完成操作。

圖1-8 VMware虛擬機的配置對話框
注意:一定要選中“Device status”下的復選框“Connect at power on”。
1.2.3 目標機設置
虛擬機里安裝了想要調試的操作系統版本之后,還需要設置目標機為可調試的。在Windows Vista操作系統之后,目標機的可調試性設置與之前略有不同,下面一一介紹。
(1)Windows XP/Windows 2003的環境設置
打開系統盤,顯示受保護的操作系統文件并且顯示所有的文件和文件夾,找到boot.ini文件,去掉只讀屬性等,打開文件可以看到如下內容:
[boot loader] timeout=30 default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS [operating systems] multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional" /noexecute=optin /fastdetect
復制最后一行文字,拷貝到下一行,并在最后添加“/debug /debug /debugport=com1/baudrate=115200”,顯示如下:
[boot loader] timeout=30 default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS [operating systems] multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional" /noexecute=optin /fastdetect /noguiboot multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional" /noexecute=optin /fastdetect /debug /debug /debugport=com1 /baudrate=115200
保存之后再次啟動,即可發現引導菜單中多了一項“調試引導的系統”。
(2)Windows Vista/ Windows 7的環境設置
Windows Vista版本之前,Windows操作系統使用boot.ini進行引導,所以可以通過修改此文件進行系統的可調試性設置,但是Windows Vista之后的操作系統就無法再用法進行可調試性設置了。
在已經出版的《天書夜讀——從匯編語言到Windows內核編程》和《寒江獨釣——Windows內核安全編程》兩本書中,關于Windows Vista的可調試性設置一直是使用bcdedit命令。這種方式比較煩瑣,但是在系統啟動時可以有選擇性地進入,或者為普通啟動,或者為調試啟動,無須再重新進行選項設置。
對于這種方式的設置,讀者可以參考前兩本書,這里不再復述。這里講述一種較為簡單的設置方式,但是如果需要普通啟動,則需要重新進行選項設置。
單擊“開始”菜單→“所有程序”→“附件”,打開“命令提示符”,輸入“msconfig”命令,打開“系統配置”對話框;或者使用“WIN+R”組合鍵打開“運行”對話框,輸入“msconfig”命令,打開“系統配置”對話框。選擇“引導”選項卡,單擊“高級選項”按鈕,如圖1-9所示。

圖1-9 Windows Vista/Windows 7的可調試性設置對話框
選中“調試”復選框,然后選擇“調試端口”等。這里的設置與虛擬機的設置以及調試器的設置一定要一致,只有三者的設置一致,才能正常連接調試。這里采用的選項與之前的設置一樣:
-b -k com:port=\\.\pipe\com_1,baud=115200,pipe
將WinDBG連接至虛擬機進行調試,如圖1-10所示。
運行VMware虛擬機里設置成功的調試操作系統,然后打開我們創建的WinDBG快捷方式進行連接。WinDBG調試器的快捷鍵以及菜單命令與Visual Studio系列軟件一致,包括設置斷點按F9鍵、運行按F5鍵等,讀者可以看一下調試菜單中的菜單項。
圖1-10是本書第一個例子Hello World驅動的調試截圖,其中斷在打印輸出日志的語句上。
1.2.4 Virtual PC虛擬機
Virtual PC是微軟為了爭奪虛擬化市場而推出的一款軟件產品,和VMware一樣也十分優秀。它也可以像VMware一樣,被用來進行驅動的雙機調試。由于這里已經詳細介紹了VMware虛擬機的使用,所以Virtual PC就不做過多的介紹了,它的設置與VMware虛擬機一樣,并不復雜,讀者可以自己嘗試一下。
1.3 小結
本章以一個簡單的Hello World驅動程序為例,講解了驅動編程中最基本的一些要素。本章內容較為基礎,對于初學者是不錯的入門課程;而對于熟練的讀者,算是讓大家在學習更豐富的內容之前,熱熱身吧。
- pcDuino開發實戰
- Modern Web Testing with TestCafe
- Linux Mint Essentials
- Mastering Distributed Tracing
- 嵌入式Linux驅動程序和系統開發實例精講
- Linux運維最佳實踐
- Linux內核觀測技術BPF
- Linux服務器配置與管理
- Kali Linux高級滲透測試
- Linux軟件管理平臺設計與實現
- Heroku Cloud Application Development
- Learning Continuous Integration with Jenkins(Second Edition)
- Linux內核修煉之道
- Mastering Azure Serverless Computing
- 樹莓派+傳感器:創建智能交互項目的實用方法、工具及最佳實踐