程式扎記: [ Windows DDP ] Windows 記憶體管理 : 注意事項

標籤

2011年1月3日 星期一

[ Windows DDP ] Windows 記憶體管理 : 注意事項

前言 : 
在開發驅動程式有些注意事項說明如下... 

資料型態 : 
用 C 語言或者 C++ 語言開發時, 字元變數, Short 整數, Long 整數都有自己的標準資料類型. DDK 對這些資列結構進行了封裝. 在驅動程式中, 既可以使用 C 語言的類型定義, 也可以使用 DDK 提供的類型定義, 下表列出了 C 語言的資料類型和 DDK 中對應的資料類型 : 
 
在 C 語言中, 整數類型有 8 位元, 16 位, 32位 三種類型, 而在 DDK 中又新添了一種 64 位元的長整數. 這種 64 位元整數只有無符號形式, 表示範圍從 0 ~ 2^63, 用 LONGLONG 類型表示. 64 位元整數不是標準 C 語言定義的, 只有微軟的編譯器才識別這種類型. 64 位元整數的常量前面是一個數字, 後面加上 i64 結尾, 如 : 
LONGLONG llValue = 100i64; 

這種 64 位元整數支援加減乘除等運算. 除了 LONGLONG 之外, DDK 還提供一種 64 位元整數的表示方法, 即 LARGE_INTEGER 資料結構. 其區別是 LONGLONG 是基本的資料, 而 LARGE_INTEGER 是資料結構, 其定義如下 : 

- Syntax :
  1. typedef union _LARGE_INTEGER {  
  2.   struct {  
  3.     DWORD LowPart;  
  4.     LONG  HighPart;  
  5.   } ;  
  6.   struct {  
  7.     DWORD LowPart;  
  8.     LONG  HighPart;  
  9.   } u;  
  10.   LONGLONG QuadPart;  
  11. } LARGE_INTEGER, *PLARGE_INTEGER;  

LARGE_INTEGER 是個聯合體, 這種設計非常巧妙. 聯合體中的三個元素可以認定是 LARGE_INTEGER 的三個定義. 其中 : 
(1) LARGE_INTEGER 可以認定是由兩個部分組成. 一個是低 32 位元的整數 LowPart , 一個是高 32 位元整數 HightPart. 在 little endian 的情況下, 低 32 位元數字在前, 高 32 位元數字在後. 如果將這個 64 位數指定為 100, 可以這樣寫 : 
  1. LARGE_INTEGER LargeValue;  
  2. LargeValue.LowPart = 100;  
  3. LargeValue.HighPart = 0;  
(2) LARGE_INTEGER 等價於 LONGLONG 數據. 在這種情況下如果想將這個資料結構指定為 100 可以這樣寫 : 
  1. LARGE_INTEGER LargeValue;  
  2. LargeValue.QuadPart = 100i64;  
返回狀態值 : 
DDK 大部分函式的返回數值型別是 NTSTATUS 類型, 查看 DDK.h 檔, 可以看到 : 
typedef LONG NTSTATUS;

NTSTATUS 的定義和 LONG 等價. 為了函式的形式統一, 所有函式返回值都是 NTSTATUS 類型. NTSTATUS 就是一個 32 位元整數, 其每位元都有著不同的含意 : 
 
在執行完內核函式後, 應該查看函式返回狀態, 如果狀態碼高位為 0, 無論其他位置是否設置, 該狀態碼代表成功. 絕對不能用狀態碼與 0 比較來判斷操作是否成功, 而應該使用NT_SUCCESS 巨集. 用法如下 : 
  1. status = Foo(...);  
  2. if(NT_SUCCESS(status))   
  3. {  
  4.     // 該函式執行成功  
  5. }  
檢查記憶體可用性 : 
在驅動程式中, 對記憶體的操作要格外小心. 如果某段記憶體是唯讀, 而驅動程式試圖去操作會導致系統的崩潰. 同樣當某段記憶體是不可讀的情況下, 而驅動程式試圖去讀, 也會造成系統的崩潰. 
DDK 提供了兩個函式, 說明程式設計師在不知道某段記憶體是否可讀寫的情況下, 試探這段記憶體可讀性. 這兩個函式分別是 ProbeForRead  ProbeForWrite. 
- ProbeForRead syntax :
  1. VOID ProbeForRead(  
  2.   __in  PVOID Address,  
  3.   __in  SIZE_T Length,  
  4.   __in  ULONG Alignment  
  5. );  

參數說明 : 
* Address : 需要被檢查的記憶體位址.
* Length : 需要被檢查的記憶體長度, 單位是位元組.
* Alignment : 描述該段記憶體是以多少位元組對齊的.

- ProbeForWrite syntax :
  1. VOID ProbeForWrite(  
  2.   __inout  PVOID Address,  
  3.   __in     SIZE_T Length,  
  4.   __in     ULONG Alignment  
  5. );  

參數說明 : 
* Address : 需要被檢查的記憶體位址.
* Length : 需要被檢查的記憶體長度, 單位是位元組.
* Alignment : 描述該段記憶體是以多少位元組對齊的.

這兩個函式不是返回該段記憶體是否可讀寫, 而是當不可讀寫的時候引發一個異常 (Exception). 這個異常需要用到微軟編譯器提供的 "結構化異常" 處理辦法. 

結構化異常處理 (try-except 區塊) : 
結構化異常 是微軟編譯器提供的獨特處理機制, 這種處理方式在一定程度上在出現錯誤的情況下, 免於程式崩潰. 為了說明結構化異常, 有兩個概念要說明. 
(1) 異常 : 異常的概念類似於插斷的概念, 當程式中某種錯誤觸發一個異常, 作業系統會尋找處理這個異常的處理函式. 如果程式提供錯易處理函式, 則進入錯誤處理函式, 如果沒有提供處理函式, 則由作業系統的預設處理函式處理. 在核心模式下, 作業系統預設處理錯誤的辦法往往很簡單, 直接讓系統 blue screen, 並在藍色畫面上簡單描述錯誤資訊, 之後系統就進入當機狀態. 這當然不是程式設計師所希望的, 程式設計師希望自己設置異常處理函式.
(2) 回捲 : 程式碼執行到某個地方出現異常錯誤時, 系統會尋找出錯點是否處於一個 try{} 區塊中, 並進入 try{} 區塊提供的異常處理程式碼. 如果當前 try 區塊沒有提供異常處理, 則會向更外一層的 try 區塊尋找異常處理程式碼. 直到最外層 try{} 區塊也沒有提供異常處理程式碼則交由作業系統處理.

這種向更外一層尋找異常處理的機制, 被稱為回捲. 一般異常處理是透過 try-except 區塊來處理 : 
  1. __try  
  2. {  
  3. }  
  4. __except(filter_value)  
  5. {  
  6. }  
在被 __try{} 包圍的區塊中, 如果出現異常, 會根據 filter_value 的數值判斷是否需要在 __except{} 區塊中處理. filter_value 的數值可能有以下三種可能 : 
(1) EXCEPTION_EXECUTE_HANDLER , 該數值為 1. 進入到 __except 進行錯誤處理, 處理完後不在回到 __try 區塊中, 轉而繼續執行.
(2) EXCEPTION_CONTINUE_SEARCH, 該數值為 0. 不使用 __except 區塊中的異常處理, 轉而向上一層回捲. 如果已經是外層, 則向作業系統請求異常處理函式.
(3) EXCEPTION_CONTINUE_EXECUTION, 該數值為 -1. 重複先前錯誤指令, 這個在驅動程式中很少用到.

ProbeForRead 和 ProbeForWrite 函式可以和 try-except 區塊配合, 用來檢查某段記憶體是否可讀寫. 下面列出一段範例, 這段程式探測空指標的位址是否可以寫. 這會引發一個異常, 程式碼用 try-except 處理異常 : 
- Driver.cpp (ProbeTest 函式) :
  1. #pragma INITCODE  
  2. VOID ProbeTest()  
  3. {  
  4.     PVOID badPointer = NULL;  
  5.   
  6.     KdPrint(("Enter ProbeTest\n"));  
  7.   
  8.     __try  
  9.     {  
  10.         KdPrint(("Enter __try block\n"));  
  11.   
  12.         //判斷空指標是否可讀,顯然會導致異常  
  13.         ProbeForWrite(badPointer,100,4);  
  14.   
  15.         //由於在上面引發異常,所以以後語句不會被執行!  
  16.         KdPrint(("Leave __try block\n"));  
  17.     }  
  18.     __except(EXCEPTION_EXECUTE_HANDLER)  
  19.     {  
  20.         KdPrint(("Catch the exception\n"));  
  21.         KdPrint(("The program will keep going\n"));  
  22.     }  
  23.   
  24.     //該語句會被執行  
  25.     KdPrint(("Leave ProbeTest\n"));  
  26. }  

除了讀寫記憶體外, try-except 區塊還可以處理一些異常. DDK 提供了一些函式觸發異常, 可以根據需求使用這些函式 : 
ExRaiseStatus : 用指定狀態碼觸發異常.
ExRaiseAccessViolation : 觸發 STATUS_ACCESS_VIOLATION 異常
ExRaiseDatatypeMisalignment : 觸發 STATUS_DATATYPE_MISALIGNMENT 異常

結構化異常處理 (try-finally 區塊) : 
結構化異常還有另一種使用方法, 就是利用 try-finally 區塊, 強迫函式在退出前執行一段程式碼 : 
- 範例代碼 : Test Finally block
  1. NTSTATUS TryFinallyTest()  
  2. {  
  3.     NTSTATUS status = STATUS_SUCCESS;  
  4.     __try  
  5.     {  
  6.         return STATUS_SUCCESS;  
  7.     }  
  8.     __finally  
  9.     {  
  10.         KdPrint(("Enter finally block\n"));  
  11.     }  
  12. }  

上段程式碼的 __try{} 區塊中, 無論執行什麼程式碼, 在程式退出該函式前都會執行 __finally{} 區塊中的程式碼. 這樣的目的是在退出前需要執行一些資源回收的工作, 而資源回收程式碼的最佳位置就是放在這個區塊中. 

使用巨集注意的地方 : 
DDK 提供了大量的巨集, 在使用這些巨集時候, 要注意一種錯誤發生, 這就是 "側效" (Side Effect). 巨集一般由多行組成, 如下面的形式, 其中 "\" 代表換行 : 
  1. #define PRINT(msg) KdPrint(("===================\n"));\  
  2.                                  KdPrint(msg);\  
  3.                                  KdPrint(("===================\n"));  
在 C 語言中規定, for 或者是 if 語句塊中的內容如果只是一句, 可以省略 {}. 如 : 
  1. if(bRet)  
  2. {  
  3.     Foo();  
  4. }  
等價於 : 
但是如果 Foo 是巨集而非函式時, 就會發生邏輯錯誤, 如 : 
  1. if(bRet) PRINT(msg);  
等價於 : 
  1. if(bRet) KdPrint(("===================\n"));  
  2. KdPrint(msg);  
  3. KdPrint(("===================\n"));  
這顯然跟我們預期結果不一致. 產生這樣的錯誤稱為 "側效" 錯誤. 解決這個辦法一般有兩個辦法 : 
(1) 對於 if, while, for 這樣的語句不省略 {}. 這是最保險的做法.
(2) 在編寫多行巨集的時候, 在巨集的前後加上 {}. 如 :
  1. #define PRINT(msg) {\  
  2.                                  KdPrint(("===================\n"));\  
  3.                                  KdPrint(msg);\  
  4.                                  KdPrint(("===================\n"));\  
  5. }  

斷言 : 
在驅動程式開發中, 還有一個技巧, 就是使用 "斷言(ASSERT)". 在驅動程式使用 "斷言" 一般是透過使用 ASSERT 巨集, 例如 : 
  1. NTSTATUS Foo(PCHAR* str)  
  2. {  
  3.     ASSERT(str!=NULL);  // 斷言  
  4.     // 對 str 操作  
  5. }  
這段程式碼會認為輸入參數絕對不可能是空指標, 因此在函式一開頭就做一個斷言 (ASSERT). 一旦斷言失敗, 會引發一個異常. 

沒有留言:

張貼留言

網誌存檔

關於我自己

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