程式扎記: [ Windows DDP ] 驅動程式編譯環境配置

標籤

2010年12月22日 星期三

[ Windows DDP ] 驅動程式編譯環境配置


前言 :
這裡將一步步對驅動程式開發進行編譯, 安裝和簡單的除錯工作進行介紹. 這些步驟雖然簡單, 但往往困惑著初次接觸驅動開發者. 另外也介紹了 C 語言開發的注意細節.

呼叫約定 :
呼叫約定指的是函式被呼叫時候, 會按照不同的規則, 解釋成不同的組合語言程式碼. 為了瞭解這個概念, 首先瞭解一下呼叫堆疊的概念. 當一個函式被呼叫時, 首先會先將返回位址壓入堆疊, 緊接著會將函式的參數依序放入堆疊. 當函式退出時會以相反順序依次退出堆疊. 因此函式在被呼叫前和呼叫後的堆疊保持平衡 :

不同的呼叫約定, 會指明不同的參數進入堆疊的順序, 還會指明不同的清理堆疊的方法. 用 C 語言或者 C++ 語言編譯器編譯的時候, 會按照四種不同的呼叫約定去編譯函式. 其分別是C 語言呼叫約定, 函式由 __cdecl 修飾 ; 標準呼叫約定, 函式由 __stdcall 修飾 ; 快速呼叫約定, 函式由 __fastcall 修飾 ; C++ 類別成員函式的呼叫約定, 函式由 thiscall 修飾. 不同的呼叫約定編譯後, 會產生不同的組合語言程式碼. 下面對 C 語言呼叫約定和標準呼叫約定進行簡單介紹.
- C 語言呼叫約定
C 語言呼叫約定要求在宣告函式時用 __cdecl 對函式進行修飾, 例如 :
void __cdecl Foo(int a, int b);

C 語言呼叫會在目標 (Object) 檔中產生一個符號來代表這個函式, 此符號形式為底線+函式名, 且函式體以 ret 形式返回, 例如 :
Foo(0x12345678, 0x11223344);

展成組合語言程式碼如下 :
  1. push  11223344h  
  2. push  12345678h  
  3. call     _Foo  
  4. add    esp,8  
即從右至左, 將參數放進堆疊, 執行結束後, 以 ret 返回. 此時的堆疊和呼叫前的堆疊不一致. 需要 "呼叫者" 恢復堆疊, 用 add 指令將堆疊恢復平衡.

- 標準呼叫約定
標準呼叫約定, 要求在宣告函式時用 __stdcall 對函式進行修飾, 例如 :
void __stdcall Foo(int a, int b);

C 語言呼叫會在目的檔案中產生一個符號來代表這個函式, 此符號形式為 底線+函式名+X . 其中 X 代表清理堆疊實需要的數字, 函式以 ret X 形式返回. 例如 :
Foo(0x12345678, 0x11223344);

展成組合語言程式碼如下 :
  1. push  11223344h  
  2. push  12345678h  
  3. call     _Foo@8  
即從右至左將參數放進堆疊, 當函式呼叫完, 函式以 ret 8 返回函式. Foo 函式負責恢復堆疊. 而"呼叫者" 不負責恢復堆疊, 這是 C 語言呼叫約定和標準呼叫約定重要差別之一.
一般程式中, 很少見到用關鍵字指定函式的呼叫約定, 編譯器會選擇呼叫約定進行編譯, 在 VC 編譯器中, 預設使用 C 語言的呼叫約定. 而在 Windows 驅動程式的編寫中, 需要使用標準呼叫約定, 尤其是入口函式. 系統會尋找 _DriverEntry@8 作為驅動程式的入口點. 如果用 C 語言呼叫約定, 會將 DriverEntry 編譯成 _DriverEntry, 而不是 _DriverEntry@8, 那麼會導致連結錯誤. 因此在編譯驅動時, 需要改變預設的編譯呼叫約定.
而如果使用 DDK 編譯環境編譯驅動程式, 預設採用標準呼叫約定, 所以可以忽略此項.

函式的匯出名 :
同一個函式, 用在 C 語言編譯器和 C++ 語言編譯器編譯出來的符號名是不同的, 這點尤其需要注意. 因為在連結的時候, 連結器不知道來源程式的函式名, 而只會去目標檔 (Object) 中尋找對應的函式符號表. VC 或者 DDK 提供的編譯器 cl.exe, 既可以編譯成 C 語言, 又可以編譯成 C++ 語言. 預設情況下, 編譯器會根據原始檔案的副檔名, 來判斷使用哪種方式編譯. 當檔案副檔名是 .cpp 時後, 編譯器會用 C++ 方式編譯 ; 當副檔名是 .c 時後用 C 編譯器方式編譯.
Windows 驅動程式的入口函式規定為 _DriverEntry@8, 因此使用 C++ 編譯的時候, 會導致符號連結錯誤. 解決辦法使採用 extern "C" 修飾符 (C++ 複雜的函式符號名是為了支援函式重載功能, 有興趣讀者可以參考編譯器的相關文檔), 例如 :
  1. #pragma INITCODE  
  2. extern "C" NTSTATUS DriverEntry (  
  3.                      IN PDRIVER_OBJECT pDriverObject,  
  4.                      IN PUNICODE_STRING pRegistryPath);  
另外在 C++ 程式中, 需要包含 ntddk.h 或者 wdm.h 的時候, 可能會出現錯誤. 出現錯誤的原因是 按照 C++ 編譯方式, 原始檔案包含了 ntddk.h 或者 wdm.h . 當驅動程式中用到內核函式時 (如 IoCreateSymbolicLink) 連結器會尋找符號 _?IoCreateSymbolicLink@@YGJPAU_UNICODE_STRING@@0@Z , 而不會尋找符號 _IoCreateSymbolicLink@8. 因此該錯誤是一個連結錯誤, 而不是編譯錯誤. 些改的辦法很簡單, 包含標頭檔時, 用 extern "C" 去修飾, 程式碼如下 :
  1. #ifdef __cplusplus  
  2. extern "C"  
  3. {  
  4. #endif  
  5. #include   
  6. #ifdef __cplusplus  
  7. }  
  8. #endif  
#ifdef _cplusplus 判斷是否使用了 C++ 的編譯方式. 將包含語句用大括號括住, 並用 extern "C" 修飾, 這樣宣告時, 就按照 C 方式編譯了.

執行時函式的呼叫 :
Windows 驅動程式雖然和普通 Win32 應用程式一樣, 都是使用 C 語言或是 C++ 編寫, 但是比起普通應用程式, 增加了很多嚴格的限制. 很多 C 語言和 C++ 語言的使用技巧, 要慎重使用. 例如在 Windows 驅動程式中, 不能使用編譯器執行時函式 (Run Time Function), 甚至 C 語言中的 malloc 函式和 C++ 語言中預設的 new 運算子都不能使用 (如果使用 new, 必須重載 new 運算子), 這涉及很多 Windows 作業系統底層的知識還有編譯器的認知.
編譯廠商一般在發佈編譯器同時, 會同時將其執行時函式一起發佈給使用者. 執行時期函式是一個程式執行時必不可或缺的函式. 它由編譯器提供, 針對不同的作業系統也有所不同, 但是介面上基本上是標準的. 例如 malloc 函式就是典型的執行實其函式, 所有編譯器廠商都必須提供這個函式, 不同廠商在不同作業系統實作方法是不同的.
對於讀過 VC 執行時函式原始代碼的人大概已經明白為什麼在 Windows 驅動程式裡不能使用編譯器提供的執行時函式了. 原因很簡單, 大部分的執行時函式是透過 Win32 API 實現的. 而 API 是針對 Ring3 層 (使用者模式) 的程式, 但 Windows 驅動執行在 Ring0 層 (核心模式). 核心模式下的程式是無法呼叫使用者模式提供的 API 函式的. C 語言的 malloc 函式和 C++ 語言提供的 new 運算子, 無一例外最後都會呼叫了 Windows 的 API 函式 故無法在內核態使用.
Windows 為使用者提供了內核態的執行函式, 它可以替代應用程式的執行時函式. 在內核態的執行時函式一般形如 RtlXXXX, 這些函式會在後續章節介紹.
有一些執行時函式, 如 strcpy 等, 它們的實現不依賴於 API. 讀者完全可以將其用在 Windows 驅動的編寫中, 但是如果不清楚某個函式的實現方式, 最好用 DDK 提供的執行時函式.

用 DDK 編譯環境編譯驅動程式 :
DDK 提供了一系列編譯和連結的工具, 這些工具包括編譯器 cl.exe 和連結程式 link.exe . 這些編譯器和連結器和 VC 中提供的工具完全一樣, 所以理論上驅動程式可以用 DDK 編譯, 也可以在 VC 上進行編譯 (需要微調編譯參數).
在 DDK 編譯環境下, 使用者需要編寫指令檔, 描述專案裡用到的原始檔案名, 包含目錄路徑, 程式庫目錄路徑, 編譯最佳化參數等資訊. 然後使用者在命令列執行 build.exe 命令, 編譯環境會自動解析指令檔來編譯連結驅動程式.
- 編譯版本
DDK 編譯環境為使用者提供兩種編譯版本: Checked 版本 與 Free 版本. Checked 版本和 Free 版本的關係類似 VC 中的 Debug 版本與 Release 版本.
Free 版本是最終發行版本, 要進行必要的最佳化並刪除所有的除錯符號, 編譯環境會將編譯器的最佳化參數打開. 因此此版本編譯出來的驅動體積是最小, 執行速度是最快, 但是無法進行原始程式碼的除錯.
Checked 版本和 Free 版本相反, 是一個未最佳化的除錯版本, 裡面包含了大量除錯符號, 來對應原始程式碼中具體的位置. 關閉了最佳化參數是為了便於除錯, 雖然執行速度稍慢, 但是其最大優點是可以進行原始程式碼的除錯. 可以使用 Softice 或者 WinDbg 進行除錯.

- nmake 工具
在早期程式員沒有 IDE 開發環境, 必須在一個 makefile 的檔中指定需要編譯的檔案, 並透過 nmake 工具解析 makefile 檔. 此檔按照編譯的依賴順序做出判斷, 需要先編譯哪些檔後編譯哪些檔.
例如 .exe 是依賴於 obj 檔, 也就是 obj 檔的最後修改日期晚於 exe 的最後修改日期, 那麼就認為 exe 檔是過時的, 需要重新編譯. 一般程式的依賴關係如下, 箭頭方向表示 "依賴於" :

編好的 makefile 檔, 就可以在命令列下執行 nmake 了. 在命令列下, 首先執行 VCVARS32.BAT, 這個批次檔負責一些系統環境變數, 這些環境變數一般是關於 include 目錄的位置, lib 的位置. 執行 nmake 的步驟如下 :
VCVARS32
nmake Test.mak # Test.mak 是此專案的 makefile 的名稱

執行完 nmake 後, 會編譯生成所需要的最終二進位映射.

- build 工具
DDK 提供的編譯環境, 主要是呼叫 build 工具編譯和連結程式碼. Build 的主要工作是呼叫 nmake 工具, 將呼叫的參數傳進 nmake 工具中. 同時 build 會根據不同的編譯版本, 設置不同的環境變數. 這樣在 nmake 呼叫時, 會根據不同的環境變數, 編譯出不同的結果.
build 工具會根據 checked 版本和 free 版本生成程式碼. 同時也會根據指定的作業系統如 Windows XP, Windows 2003 等生成程式碼. 而且也會根據不同平台, 如 386 系列平台 (i386), Itanium 平台, 編譯出不同的程式碼.
build 可以生成不同版本的編譯結果, 而不需要編寫多個版本的 makefile 檔. 只需要一個 makefile 就夠了. 這就是 build 工具的優點. 其呼叫關係如下所示 (cl.exe 為 編譯器, link.exe 是連結器) :


DDK 提供的 build 工具比 nmake 工具編譯更加便利. build 工具將所有要生成的編譯版本互相隔絕, 使之互不影響. 例如透過 build 工具, 可以指定在編譯 32 位元驅動程式時, 用一系列的包含檔, 程式庫和一些編譯開關. 而編譯 64位元 驅動程式時, 指定另外一系列的包含檔案, 程式庫和編譯開關, 而從不用修改原始檔案, makefile 就可以達到同時生成多個平台上的驅動.

- makefile 檔案
makefile 檔案指定原始檔之間的依賴關係, 確定專案中哪些檔案需要重新編譯. 在 DDK 程式進行編譯時, build 工具會呼叫 nmake 工具去解析 makefile 檔案進行編譯, 關於 makefile 更加詳細的資料請參見 MSDN 文檔.
makefile 只需要列出一系列依賴關係, 如果從頭開始寫 makefile 是會很煩人的事情, 大多數情況下, 使用者只需要寫如下一個 makefile 檔 :
!INCLUDE $(NTMAKEENV)\makefile.def

這個 makefile 除了前面的註釋外, 主要是包含了 DDK 目錄中的 makefile.def. DDK 文檔不建議使用者修改該 makefile 檔. 請遵從 DDK 文檔的這個建議.

- dirs 檔案
build 工具可以遞迴地指定需要編譯的檔和目錄. dirs 檔中描述需要編譯的子目錄. 如果目前的目錄中存在一個 dirs 檔, build 會分析此檔, 依次進入這些子目錄編譯. 這些子目錄可以含有其他 dirs 檔或者 sources 檔. 下面是 DDK 根目錄下 src 中的 dirs 檔. 如果執行 build 命令後, 這些子目錄將依次進行編譯 :
- dirs :
  1. DIRS= \  
  2.           general \  
  3.           ime \  
  4.           input \  
  5.           kernel \  
  6.           mmedia \  
  7.           network \  
  8.           print \  
  9.           setup \  
  10.           smartcrd \  
  11.           storage \  
  12.           vdd \  
  13.           video \  
  14.           wdm  

- sources 檔案
build 工具會尋找目前目錄下的 source 檔或者 dirs 檔中指定子目錄下的 sources 檔. 此檔記錄了需要編譯的原始檔案檔名, 包含目錄路徑, 程式庫目錄等資訊. 一般情況下, 這些所指定的資訊全部都是些改 build 工具提供的變數. build 會根據這些變數通知 nmake 工具進行編譯. 下面列出幾個常用的 build 變數 :
* TARGETNAME : 描述目標驅動的名稱.
* TARGETTYPE : 描述目標程式碼生成的類別. TARGETTYPE = DRIVER 意謂著是生成驅動. 如果 TARGETTYPE=PROGRAM, 則編譯成 Win32 程式.
* DDKROOT : 設置 DDK 的根目錄.
* C_DEFINES : 指示 C 預編譯定義參數, 其作用相當於在 C 檔中用 #define 宣告的定義.
* TARGETPATH : 指示目標程式碼生成路徑
* INCLUDES : 設定包含目錄路徑.
* TARGETLIBS : 設置目標程式所需要的程式庫
* MSC_WARNING_LEVEL : 指明編譯警告級別. 一般情況下為 W3, 及第三級的警告.
* SOURCES : 指定此專案所有的原始檔案, 注意只指定 C 檔 或者 C++ 檔, 而不需要指定 h 檔.

下面是一個標準的 SOURCES 檔, 使用者可以根據此檔修改成自己需要的 SOURCES 檔 :
- 範例 SOURCES 檔 :
  1. TARGETNAME=bulkusb  
  2. TARGETTYPE=DRIVER  
  3. DDKROOT=$(_NTDRIVE)$(_NTROOT)  
  4.   
  5. C_DEFINES= $(C_DEFINES) -DWMI_SUPPORT -DUSB2  
  6.   
  7. TARGETPATH=obj  
  8.   
  9. INCLUDES=$(DDKROOT) \private \ntos \inc;  \  
  10.                   ..\..\inc  
  11.   
  12. NTTARGETFILE0=mofcomp  
  13.   
  14. USE_MAPSYM=1  
  15.   
  16. TARGETLIBS=$(DDK_LIB_PATH)\hidclass.lib \  
  17.                      $(DDK_LIB_PATH)\usbd.lib \  
  18.                      $(DDK_LIB_PATH)\ntoskrnl.lib  
  19.   
  20. MSC_WARNING_LEVEL=/W3 /WX  
  21.   
  22. SOURCES=bulkusb.c \  
  23.                  bulkpnp.c \  
  24.                  bulkpwr.c \  
  25.                  bulkdev.c \  
  26.                  bulkwmi.c \  
  27.                  bulkrwr.c \  
  28.                  bulkusb.rc  

- makefile.inc 檔案
makefile.inc 是可選的, 它也是一個 makefile 檔. 如果目前的目錄存在這個檔. build 工具會分析此檔. 並按照此檔按進行 C 語言編譯之外的動作.
- makefile.inc 範例 :
  1. mofcomp: bulkusb.bmf  
  2.   
  3. bulkusb.bmf: bulkusb.mof  
  4.     mofcomp -B:bulkusb.bmf bulkusb.mof  
  5.     wmimofck bulkusb.bmf  

這是一個標準的 makefile 檔, mofcomp 依賴於 bulkusb.bmf 檔. 如果 mofcomp 的比 bulkusb.bmf 的日期 "過時". 則執行一個行為, 但此行為沒有指定, 及空行為. 後面指定 bulkusb.bmf 依賴於 bulkusb.mof . 如果 bulkusb.bmf 比 bulkusb.mof "過時", 則執行 mofcomp -B:bulkusb.bmf bulkusb.mof.

- build 工具的環境變數
build 工具的環境變數指的是在上述這些檔案中出現的環境變數. build 會初始化這些變數, 使用者可以根據自己的需求更改這些變數, 這裡介紹一些常用的環境變數.
* BASEDIR : 此變數指定驅動目錄的基準, 預設為 DDK 的根目錄.
* BUILD_DEFAULT : build 命令會呼叫 nmake 命令, 此環境變數設置預設呼叫參數, 例如 :
C:\> build -eswM -nmake -i
等價於 :
C:\> set BUILD_DEFAULT=-eswm -nmake -i
C:\> build

* BUILD_DEFAULT_TARGETS : 此環境變數設置預設的編譯目標平台. 假設在 Itanium 平台下, 可以編譯出 x86 的程式碼. 一般來說這個變數設置就是使用者所使用的那個平台. 例如 :
C:\> build -386
等價於 :
C:\> set BUILD_DEFAULT_TARGET=-386
C:\> build

* BUILD_MAKE_PROGRAM : 此環境變數設置 makefile 的檔案名稱, 預設情況下此變數就是當前目錄下的 makefile. 一般情況下不要修改此變數.
* C_DEFINES : 此環境變數可以設置預編譯的定義, 這如同在原始檔案中指定 #define 預定義. 例如想通知編譯器定義 DEFAULT_BUILD , 可以像下面程式碼一樣修改這個變數 :
C_DEFINES = /DDEEBUG_BUILD

如果在 SOURCES 檔中為了增加新的預定義, 而不會將已定義預定義覆蓋掉, 可以做如下定義 :
C_DEFINES=$(C_DEFINES) /DUNICODE

* CRT_INC_PATH : 此變數指定 C 執行時的 include 路徑. 預設是 %ddkroot%\inc\crt.
* CRT_LIB_PATH : 此變數指定 C 執行時的程式庫目錄檔路徑. 預設是 %ddkroot%\lib\*
* DDK_INC_PATH : 此變數指定 DDK 的 include 路徑. 預設是 %ddkroot%\inc\ddk\wxp
* DDK_LIB_DEST : 此變數指定 DDK 編譯程式庫的路徑. 預設是 %ddkroot%\lib
* DDK_LIB_PATH : 此變數指定 DDK 的導入程式庫目錄路徑. 預設是 %ddkroot%\lib\*
* WDM_INC_PATH : 此變數指定 WDM 標頭檔的目錄路徑. 預設是 %ddkroot%\inc\ddk\wdm\wxp

更多有關 build 的內容可以參考 這裡 (The WDK build environment)
This message was edited 16 times. Last update was at 15/12/2010 14:06:55

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!