简单说说驱动程序设计的入门
簡單說說驅動程序設計的入門,其實初級驅動設計中也能使用C++,也能使用類,但和用戶程序中的用法有一些區別,一些特殊的地方需要特別注意。從筆者的經驗來看,WDK給出的AVStream小端口驅動示例工程,就都是C++代碼,這是由于AVStream的模塊性非常強,在實現較大功能模塊時,非得用類封裝,否則難以表述清楚。
很少有專題講內核中的C++編程,中文資料恐怕更是罕見。由于C++的普及性、與C的親密關系,以及大部分情況下程序員都使用C++編譯器編譯C程序的事實,當初學者聽說內核中“不容易”(筆者也聽說過“無法”二字)用C++進行編程時,會大吃一驚。不管是說者無意,還是聽者有心,Windows內核的現狀,決定了C語言是內核編程的首選。
本章專門講述如何在內核中編寫C++驅動程序。筆者先寫一個簡單的例子,顯示類的一些基本特性,并由此交代出幾項關鍵點;然后改造《WDF USB設備驅動開發》一章中的WDFCY001驅動的例子,將它全部改造成一個驅動類,并最終實現C++的最大優點:多態。
一個簡單的例子
首先我們嘗試把用戶程序中最簡單的類拷貝到內核中,編譯鏈接,看看行不行。下面就是筆者定義的整數類,它封裝一個整數,對象能夠被當成整數使用。
以下是代碼片段: class clsInt{ Public: clsInt(){m_nValue = 0;} clsInt(int nValue){m_nValue = nValue;} void print(){KdPrint((“m_nValue:%d/n”, m_nValue));} operator int(){return m_nValue;} private: int m_nValue; };
上例是一個非常簡單的類定義,我們將在DriverEntry函數中使用它,分別定義一個局部變量和動態創建一個對象。我們通過Debug信息來觀察對象行蹤,希望能夠得到正確的輸出。入口函數中的定義如下:
以下是代碼片段: extern "C" NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath ) { // 創建兩個對象,一個是局部變量,一個是動態創建的 clsInt obj1(1); clsInt* obj2 = new(NonPagedPool, "abcd") clsInt(2); // 打印Log信息 obj1.print(); obj2->print(); delete obj2; // 讓模塊加載失敗 return STATUS_UNSUCCESSFUL; }
上面代碼中先后創建了兩個clsInt對象,一個是在棧中創建的,初始變量為1;一個是動態創建的,初始變量為2。后者由于是動態創建的,必須手動調用delete函數釋放內存,所以其析構函數比前者先調用。我們必須從Log信息中得到類似的脈絡,以證明其正確性。代碼請參看simClass工程。圖6-1是Log信息的截圖,我們如愿以償地得到了想要的結果。
?
?
圖6-1 對象Log信息
new/delete
查看上面的代碼,會發現一個不同于以往的new操作符。這是怎么回事呢?我們這一節就講講它。在用戶程序中,創建和釋放一個對象使用 new/delete方法,其底層乃是調用HeapAllocate/HeapFree 堆API從線程堆棧中申請空間。但問題是,內核CRT沒有提供new/delete操作符,所以需要自己定義。自定義的new/delete操作符,自然也是能夠從堆棧中分配內存的,內核中有RtlAllocateHeap/RtlFreeHeap堆棧服務函數。但在內核中,我們一般使用內存池來獲取內存,實際上內存池和堆棧使用了同一套實現機制。使用ExAllocatePool/ExFreePool函數對從內存池申請/釋放內存,下面是一個例子。
下面是使用new進行內存申請的一個例子。
以下是代碼片段: // 定義一個32位的TAG值 #define TAG "abcd" // 外部已經定義了一個clsName類 extern class clsName; // 為clsName申請對象空間 clsName* objName = NULL; objName = new(NonPagedPool, TAG)clsName();
上面的new操作和用戶程序中的new操作具有同樣的功效,但需要注意第一個參數size_t是必須外置的,編譯器會自動用sizeof(clsName)求取長度并作為第一個參數。一般地說,對于類似下面的語句:
className objName = new(…) className(…)
其執行過程是,首先由new操作符為新對象動態分配內存,并返回指針;然后再對此新創建的對象,選擇與className(…) 相符的構造函數進行初始化。
再來看看delete操作符的重載。
以下是代碼片段: __forceinline void __cdecl operator delete(void* pointer) { ASSERT(NULL != pointer); if (NULL != pointer) ExFreePool(pointer); }
刪除對象數組,即delete[]操作符重載。
以下是代碼片段: __forceinline void __cdecl operator delete[](void* pointer) { ASSERT(NULL != pointer); if (NULL != pointer) ExFreePool(pointer); }
上面兩個函數最終都會將指定地址的內存釋放,但在釋放之前,前者會調用指定對象的析構函數,后者會對數組中每個成員調用析構函數。示例如下:
以下是代碼片段: extern clsName *objName; extern clsName *objArray[]; delete objName; delete[] objArray;
extern "C"
對extern "C"編譯指令,大家不會感到陌生。它一般這樣用:
以下是代碼片段: extern "C"{ //…內容 }
既然是編譯指令,就一定是作用于編譯時刻的。它告訴編譯器,對于作用范圍內的代碼,以C編譯器方式編譯。一般是針對C++/Java等程序而用的。如果括號內僅有一項,那么括號可以省略。
最早讓我們見識到它的作用的是在入口函數DriverEntry中。現在必須這樣聲明它:
以下是代碼片段: extern "C" NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath );
初學者未必知道這一點,如果“忘記”做上述改動,將得到如下錯誤:
以下是代碼片段: error LNK2019: unresolved external symbol _DriverEntry@8 referenced in function _GsDriverEntry@8 error LNK1120: 1 unresolved externals
很奇怪,這是一個鏈接錯誤,說明編譯過程是通過的。怎么回事呢?認真看一下錯誤內容,原來是系統在鏈接時找不到入口函數_DriverEntry@8。這個奇怪的函數名,很顯然是C編譯器對DriverEntry進行編譯后的結果,前綴“_”是C編譯器特有的,后綴“@8”是所有參數的長度。原來我們現在使用的是C++編譯器,一定是它把DriverEntry編譯成了系統無法認識的另一副模樣了(實際上,C++編譯器會把它編譯成以“?DriverEntry@@”開頭的一串很長的符號)。
一旦加上extern "C"修飾符,上述問題即立刻消失了。extern "C"提醒編譯器要使用C編譯格式編譯DriverEntry函數,這樣編譯生成的函數名稱為“_DriverEntry@8”,鏈接器即可正確地識別出符號了。
全局/靜態變量
首先列出規則如下:
不能定義類的全局或者靜態對象,除非這個類沒有構造函數;否則全局對象將因初始化過程中含有無法解決的符號,而導致鏈接失敗。
讀者可能難以理解這個規定,所以要用實例進行更深的挖掘才行。以simClass的clsInt類為例,如果定義如下全局變量:
clsInt gA;
對項目進行編譯,會毫不留情地得到如下錯誤(也是鏈接錯誤):
errors in directory c:/trunk/simclass
c:/trunk/simclass/main.obj : error LNK2019: unresolved external symbol _atexit referenced in function "void __cdecl "dynamic initializer for "gA""(void)" (__EgA@@YAXXZ)
上面的鏈接錯誤,是由于函數__EgA@@YAXXZ中找不到符號_atexit。這兩個名字都怪得不得了!理解它們要從C++標準說起,C++標準規定對于全局對象的處理,編譯器要保證全局對象在main()函數運行之前已經被初始化,并且保證main()函數在退出前被刪除(析構)。變量的初始化與刪除,需要編譯器專門為它們各自創建一個函數,并在合適的時機進行調用。函數名稱根據不同的編譯器會有所不同,在這里看到,用于對gA進行初始化的是函數__EgA@@YAXXZ,筆者通過IAD反匯編后看到,用于刪除(析構)的是函數__FgA@@YAXXZ。后者一點問題都沒有,但前者遇到了問題,無法解析_atexit符號。筆者將其匯編代碼拷貝如下:
以下是代碼片段: // 函數名,注釋很明白地告訴我們,此函數是gA的初始化函數 __EgA@@YAXXZ: ; DATA XREF: .CRT$XCU:_gA$initializer$o 0000031E mov edi, edi 00000320 push ebp 00000321 mov ebp, esp // 下面首先會調用clsInt的默認構造函數 // 第一句是將m_nValue賦值為0 00000323 mov ds:clsInt gA, 0 // 下面是DbgPrint調用 0000032D mov eax, ds:clsInt gA 00000332 push eax 00000333 push offset clsInt gA 00000338 push offset PrintString 0000033D call _DbgPrint 0000033D 00000342 add esp, 0Ch // 初始化已經完畢了,問題出在這里 //初始化完畢后,把地址作為參數,調用_atexit以注冊終止函數 00000345 push offset 0000034A call _atexit 0000034A // 恢復堆棧 0000034F add esp, 4 00000352 pop ebp 00000353 retn 00000353 00000353 _text$yc ends
上面的匯編代碼,大部分都是正確的,只是到了最后調用_atexit函數時才出了錯(_atexit是導入符號,實際函數名應去掉前面的“_”,即atexit)。atexit是一個C標準函數,其作用是向系統注冊終止函數,即主程序在終止之前需調用的處理函數。上面我們看到,atexit將作為參數進行了調用以析構gA。在邏輯上是沒有問題的,但atexit函數在內核中未實現。實際上,它有下面的一行調用:
atexit();
現在的問題就歸結為:內核中沒有C運行時函數atexit。請問:它可以有嗎?它難道不可以有嗎?
上面筆者也說過,內核代碼和用戶程序是非常不一樣的。用戶程序的生命周期由main()調用開始,main()調用結束,整個程序也即完結。而驅動程序卻不一樣,雖然我們有時候把DriverEntry比作main(),但二者在本質上不同,DriverEntry的生命周期非常短,其作用僅是將內核文件鏡像加載到系統中時進行驅動初始化,調用結束后驅動程序的其他部分依舊存在,并不隨它而終止。所以我們一般可把DriverEntry稱為“入口函數”,而不可稱為“主函數”。因此作為初級驅動設計來說,它沒有一個明確的退出點,這應該是atexit無法在內核中實現的原因吧。
從圖6-2我們看到,用戶程序是一個獨立運行單位,main()函數是主線程,它的生命周期也就是程序的生命周期。而初級驅動設計呢?它的生命周期其實只是鏡像文件的生命周期,即加載與卸載,并沒有固定的主線程與之匹配甚至支配其生命周期;相反,驅動代碼可以出現在任何線程環境中,被任何線程調用。
話說回來,其實驅動程序也是有明顯的生命周期的,即從DriverEntry開始到DriverUnload結束的鏡像文件的生命周期,如圖6-3所示。這也并非不可利用,筆者覺得,如果在DriverEntry調用前執行全局對象的初始化函數,而同時把終止函數注冊到DriverUnload中,或許能夠解決問題,但前提是要求系統要做相應的改動了。因為DriverUnload是可選的,所以若采用這種方法,應采取措施為未提供DriverUnload函數的驅動設置默認的卸載函數。但隨著微軟對這方面研究的深入,筆者相信,這個問題一定是他們的問題列表中必須解決的一項。
內核中使用C++還有一點需要注意,就是C++編譯器會在不提醒的情況下,使用堆棧生成臨時變量若干,而內核堆棧是非常有限的,所以常常需要對此保持一份警惕。
總結
以上是生活随笔為你收集整理的简单说说驱动程序设计的入门的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 分享自己的C#开发类库
- 下一篇: 飞鸽传书2010绿色版