程式扎記: [ Windows DDP ] 派遣函式 : IRP 與派遣函式

標籤

2011年1月10日 星期一

[ Windows DDP ] 派遣函式 : IRP 與派遣函式


前言 :
IRP 的處理機制類似 Windows 應用程式中的 "訊息處理" 機制, 驅動程式接受到不同類型的 IRP 後, 會進入不同的派遣函式, 在派遣函式中 IRP 得到處理.

IRP :
在 Windows 內核中, 有一種資料結構叫做 IRP (I/O Request Package), 即輸入輸出請求封包. 它是與輸入輸出相關的重要資料結構. 上層應用程式與底層驅動通訊時, 應用程式會發出 I/O 請求. 作業系統將 I/O 請求轉化為對應 IRP 資料, 不同類型的 IRP 會根據類型傳遞到不同的派遣函式內.
IRP 是一個很複雜的資料結構, 後面會陸續介紹. 在這裡我們先了解 IRP 的兩個基本屬性, 一個是 MajorFunction, 另一個是 MinorFunction, 分別記錄 IRP 的主類型和子類型. 作業系統根據 MajorFunction 將 IRP "派遣" 到不同的派遣函式中, 在派遣函式中還可以繼續判斷這個 IRP 屬於哪種 MinorFunction.
前面介紹的 HelloDDK 和 HelloWDM 驅動程式, 都是在入口函式 DriverEntry 裡登陸了 IRP 的派遣函式. 一般來說 NT 式驅動程式和 WDM 驅動程式都是在 DriverEntry 函示裡登錄派遣函式. 以下是 HelloDDK 中的 DriverEntry 程式碼片段 :
- 範例代碼 : DDK 的 DriverEntry
  1. #pragma INITCODE    
  2. extern "C" NTSTATUS DriverEntry (    
  3.             IN PDRIVER_OBJECT pDriverObject,    
  4.             IN PUNICODE_STRING pRegistryPath    )     
  5. {    
  6.     NTSTATUS status;    
  7.     KdPrint(("Enter DriverEntry\n"));    
  8.     
  9.     //登錄其他驅動呼叫函式入口    
  10.     pDriverObject->DriverUnload = HelloDDKUnload;    
  11.     pDriverObject->MajorFunction[IRP_MJ_CREATE] = HelloDDKDispatchRoutine;    
  12.     pDriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloDDKDispatchRoutine;    
  13.     pDriverObject->MajorFunction[IRP_MJ_WRITE] = HelloDDKDispatchRoutine;    
  14.     pDriverObject->MajorFunction[IRP_MJ_READ] = HelloDDKDispatchRoutine;    
  15.         
  16.     //新建驅動裝置物件    
  17.     status = CreateDevice(pDriverObject);    
  18.     
  19.     KdPrint(("DriverEntry end\n"));    
  20.     return status;    
  21. }  

在 DriverEntry 的驅動物件 pDriverObject 中, 有個函式指標陣列 MajorFunction. 函式指標陣列的每個元素都記錄著一個函式的位置. 透過設置這個陣列, 可以將 IRP 類型和派遣函式關聯起來. 在上面例子裡, 只對四種類型的 IRP 設置了派遣函式, 而 IRP 的類型並不只是這四種. 對於其他沒有設置的 IRP 類型, 系統預設將這些 IRP 類型與 _IopInvalidDeviceRequest 函式關聯.
在進入 DriverEntry 之前, 作業系統會將 _IoInvalidDeviceRequest 的為只填滿整個 MajorFunction 陣列.

IRP 類型 :
IRP 的概念類似於 Windows 應用程式中的 "訊息" 的概念. 在 Win32 程式設計中, 程式是由 "訊息" 驅動的. 不同的訊息會被分發到不同的訊息處理函式中. 如果沒有對應的處理函式, 他會進入系統預設的訊息處理函式.
IRP 的處理類似於這種方式. 檔案 I/O 的相關函式如 CreateFile, ReadFile, WriteFile, CloseHandle 等函式會使用作業系統產生出 IRP_MJ_CREATE, IRP_MJ_READ, IRP_MJ_WRITE, IRP_MJ_CLOSE 等不同的 IRP, 這些 IRP 會被傳送到驅動程式的派遣函式中.
另外內核中的檔案 I/O 處理函式如 ZwCreateFile, ZwReadFile, ZwWriteFile, ZwClose 等, 它們同樣會將 IRP 傳送到對應驅動的對應派遣處理函式中. 還有些 IRP 是由某個系統的元件新建的, 比如 IRP_MJ_SHUTDOWN 是在 Windows 的隨插即用元件在即將關閉系統時後發出的. 底下表格列出了 IRP 的類型, 對其做了簡單說明 :


對派遣函式的簡單處理 :
大部分的 IRP 都源在於檔案 I/O 處理 Win32 API, 如 CreateFile, ReadFile 等. 處理這些 IRP 最簡單的方式就是在對應的派遣函式中, 將 IRP 的狀態設置為成功, 然後結束 IRP 請求, 並讓派遣函式返回成功. 結束 IRP 的請求使用函式 IoCompleteRequest. 下面的程式碼演示了一種最簡單的處理 IRP 請求的派遣函式 :
  1. NTSTATUS HelloDDKDispatchRoutin(IN PDEVICE_OBJECT pDevobj,  
  2.                                                         IN PIRP pIrp)  
  3. {  
  4.     KdPrint(("Enter HelloDDKDispatchRoutin\n"));  
  5.     NTSTATUS status = STATUS_SUCCESS;  
  6.     // 設置 IRP 完成狀態  
  7.     pIrp->IoStatus.Status = status;  
  8.     // 設置 IRP 操作多少位元組  
  9.     pIrp->IoStatus.Information = 0 // bytes xfered  
  10.     // 處理 IRP  
  11.     IoCompleteRequest( pIrp, IO_NO_INCREMENT );  
  12.     KdPrint(("Leave HelloDDKDispatchRoutin\n"));  
  13.     return status;  
  14. }  
在本例中, 派遣函式設置了 IRP 的完成狀態為 STATUS_SUCCESS. 這樣發起 I/O 請求的 Win32 API (如 WriteFile) 將會返回 TRUE. 相反如果將 IRP 完成狀態設置為不成功, 這時後發起 I/O 請求的 Win32 API (如 WriteFile) 將會返回 FALSE. 這種情況時, 可以使用 GetLastError Win32 API 得到錯誤代碼. 而所得到的錯誤代碼將會和 IRP 設置的狀態一致.
除了設置 IRP 的完成狀態, 派遣函式還要設置這個 IRP 請求操作了多少位元組. 在本例中, 將操作位元組簡單地設置成 0. 如果是 ReadFile 產生的 IRP, 這個位元組數代表從裝置讀了多少位元組. 最後派遣函式將 IRP 請求結束, 這是透過 IoCompleteRequest 函式完成的, 其宣告如下 :
- Syntax :
  1. VOID IoCompleteRequest(  
  2.   __in  PIRP Irp,  
  3.   __in  CCHAR PriorityBoost  
  4. );  

參數說明 :
* Irp : 代表需要被結束的 IRP.
* PriorityBoost : 代表執行緒恢復時的優先順序.

為了解釋優先順序的概念, 需要了解一下與檔案 I/O 相關的 Win32 API 的內部操作過程. 這裡以 ReadFile 為例, ReadFile 的內部操作流程如下 :
1. ReadFile 呼叫 ntdll 中的 NtReadFile. 其中 ReadFile 函式是 Win32 API, 而 NtReadFile 函式是 Native API.
2. ntdll 中的 NTReadFile 進入到核心模式, 並呼叫系統服務中的 NtReadFile 函式.
3. 系統的服務函式 NtReadFile 新建 IRP_MJ_WRITE 類型的 IRP, 然後它將這個 IRP 發送到某個驅動程式的派遣函式. NtRedFile 接著會去等待一個事件, 這是當前執行緒進入 "睡眠" 狀態, 也可說當前執行緒被阻塞住或者執行緒處於 "Pending" 狀態.
4. 在派遣函式中一般會將 IRP 請求結束, 結束 IRP 是透過 IoCompleteRequest 函式. 在 IoCompleteRequest 函式內部會設置剛剛等待的事件, 接著 "睡眠" 的執行緒被喚醒恢復執行.

例如在讀一個很大的檔案 (或者裝置) 時, ReadFile 不會立刻返回, 而是等待一段時間. 這段時間就是當前執行緒 "睡眠" 的那段時間. IRP 請求結束並標誌這個操作執行完畢, 這時 "睡眠" 的執行緒被喚醒. 在 IoCompleteRequest 函式中第二個參數 PriorityBoost 代表一種優先順序, 指的是被阻塞的執行序要以何種優先順序恢復執行. 一般情況下, 優先順序設置為 IO_NO_INCREMENT. 對某些特殊情況, 需要將阻塞的執行緒以 "優先" 的身分恢復執行, 如鍵盤, 滑鼠等輸入裝置, 因為它們需要更快地反應.

透過裝置連結打開裝置 :
這裡將介紹利用檔 I/O 相關的 Win32 API 對裝置進行 "打開" 與 "關閉" 操作. 要打開裝置必須透過裝置的名字才能得到該裝置的控制碼. 前面介紹過每個裝置都有裝置名稱, 如 HelloDDK 驅動程式的裝置名稱為 "\Device\MyDDKDevice". 但是裝置名無法被使用者模式下的應用程式查詢到, 裝置名稱只能被核心模式下的程式查詢到.
在應用程式中, 裝置可以透過符號連結進行存取. 驅動程式透過 IoCreateSymbolicLink 函式新建符號連結. HelloDDK 驅動程式的裝置所對應的符號連結是 "\??\HelloDDK". 在編寫程式時, 符號連結的寫法需要稍微改一下, 將前面的 "\??\" 改為 "\\.\". 因此符號連結 "\??\HelloDDK" 就變成 "\\.\HelloDDK".
下面程式碼演是如何透過 CreateFile 來打開裝置控制碼, 以及如何利用 CloseHandle 關閉裝置控制碼. 在打開和關閉裝置控制碼的時候, 作業系統內部會新建 IRP, 並將 IRP 發送到對應的派遣函式中 :
- 範例代碼 : 利用符號連結開啟與關閉 HelloDDK
  1. #include   
  2. #include   
  3.   
  4. int main()  
  5. {  
  6.     // 打開裝置控制碼, 會觸發 IRP_MJ_CREATE  
  7.     HANDLE hDevice =   
  8.         CreateFile("\\\\.\\HelloDDK",  
  9.                     GENERIC_READ | GENERIC_WRITE,  
  10.                     0,      // share mode none  
  11.                     NULL,   // no security  
  12.                     OPEN_EXISTING,  
  13.                     FILE_ATTRIBUTE_NORMAL,  
  14.                     NULL );     // no template  
  15.     // 判斷裝置是否成功打開  
  16.     if (hDevice == INVALID_HANDLE_VALUE)  
  17.     {  
  18.         printf("Failed to obtain file handle to device: "  
  19.             "%s with Win32 error code: %d\n",  
  20.             "MyWDMDevice", GetLastError() );  
  21.         return 1;  
  22.     }  
  23.     // 關閉裝置, 會觸發 IRP_MJ_CLEANUP 和 IRP_MJ_CLOSE  
  24.     CloseHandle(hDevice);  
  25.     return 0;  
  26. }  

編寫一個更通用的派遣函式 :
前面介紹的派遣函式處理過於簡單, 下面將對派遣常式一步步進行擴充. 首先介紹一個重要資列結構 ---- IO_STACK_LOCATION, 即 I/O 堆疊, 這個資料結構和 IRP 緊密相連.
在前面文章中曾介紹過驅動程式的層次結構. 驅動物件會新建一個個的裝置物件, 並將這些裝置 "疊" 成一個垂直結構. 這種垂直結構很像堆疊, 因此被稱為 "'裝置堆疊".
IRP 會被作業系統發送到裝置堆疊的頂層, 如果頂層的裝置物件的派遣函式結束了 IRP 請求, 則這次 I/O 請求結束. 如果沒有將 IRP 的請求結束, 那麼作業系統會將 IRP 轉發到裝置堆疊的下一層裝置處理. 如果這個裝置的派遣函式依然不能結束 IRP 請求, 則會繼續向下層裝置轉發.
因此一個 IRP 可能會被轉發多次, 為了記錄 IRP 在每層裝置中做的操作, IRP 會有一個 IO_STACK_LOCATION 陣列. 陣列元素應該大於 IRP 穿越過的裝置數. 每個 IO_STACK_LOCATION 元素記錄著對應裝置中做的操作. 對於本層裝置對應的 IO_STACK_LOCATION 可以透過 IoGetCurrentIrpStackLocation 函式得到, 如 :
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);

IO_STACK_LOCATION 結構中會記錄 IRP 的類型, 即 IO_STACK_LOCATION 中的 MajorFunction 子欄位. 下面程式碼增加了派遣函式的複雜度, 演示了派遣函式如何獲得當前 IO_STACK_LOCATION 以及如何獲得 IRP 的類型. 在 DriverEntry 中將所有 IRP 類型都和一個派遣函式HelloDDKDispatchRoutin相關聯, 然後在派遣函式中分辨出 IRP 的類型並打出對應的 log 資訊 :
- Driver.cpp (HelloDDKDispatchRoutin 派遣函式) :
  1. #pragma PAGEDCODE  
  2. NTSTATUS HelloDDKDispatchRoutin(IN PDEVICE_OBJECT pDevObj,  
  3.                                  IN PIRP pIrp)   
  4. {  
  5.     KdPrint(("Enter HelloDDKDispatchRoutin\n"));  
  6.   
  7.     PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);  
  8.     //建立一個字串陣列與IRP類型對應起來  
  9.     static char* irpname[] =   
  10.     {  
  11.         "IRP_MJ_CREATE",  
  12.         "IRP_MJ_CREATE_NAMED_PIPE",  
  13.         "IRP_MJ_CLOSE",  
  14.         "IRP_MJ_READ",  
  15.         "IRP_MJ_WRITE",  
  16.         "IRP_MJ_QUERY_INFORMATION",  
  17.         "IRP_MJ_SET_INFORMATION",  
  18.         "IRP_MJ_QUERY_EA",  
  19.         "IRP_MJ_SET_EA",  
  20.         "IRP_MJ_FLUSH_BUFFERS",  
  21.         "IRP_MJ_QUERY_VOLUME_INFORMATION",  
  22.         "IRP_MJ_SET_VOLUME_INFORMATION",  
  23.         "IRP_MJ_DIRECTORY_CONTROL",  
  24.         "IRP_MJ_FILE_SYSTEM_CONTROL",  
  25.         "IRP_MJ_DEVICE_CONTROL",  
  26.         "IRP_MJ_INTERNAL_DEVICE_CONTROL",  
  27.         "IRP_MJ_SHUTDOWN",  
  28.         "IRP_MJ_LOCK_CONTROL",  
  29.         "IRP_MJ_CLEANUP",  
  30.         "IRP_MJ_CREATE_MAILSLOT",  
  31.         "IRP_MJ_QUERY_SECURITY",  
  32.         "IRP_MJ_SET_SECURITY",  
  33.         "IRP_MJ_POWER",  
  34.         "IRP_MJ_SYSTEM_CONTROL",  
  35.         "IRP_MJ_DEVICE_CHANGE",  
  36.         "IRP_MJ_QUERY_QUOTA",  
  37.         "IRP_MJ_SET_QUOTA",  
  38.         "IRP_MJ_PNP",  
  39.     };  
  40.   
  41.     UCHAR type = stack->MajorFunction;  
  42.     if (type >= arraysize(irpname))  
  43.         KdPrint((" - Unknown IRP, major type %X\n", type));  
  44.     else  
  45.         KdPrint(("\t%s\n", irpname[type]));  
  46.   
  47.   
  48.     //對一般IRP的簡單操作,後面會介紹對IRP更複雜的操作  
  49.     NTSTATUS status = STATUS_SUCCESS;  
  50.     // 完成IRP  
  51.     pIrp->IoStatus.Status = status;  
  52.     pIrp->IoStatus.Information = 0;  // bytes xfered  
  53.     IoCompleteRequest( pIrp, IO_NO_INCREMENT );  
  54.   
  55.     KdPrint(("Leave HelloDDKDispatchRoutin\n"));  
  56.   
  57.     return status;  
  58. }  

將驅動程式成功載入後, 執行應用程式(上一節介紹的應用程式, 用來打開/關閉裝置), 隨後用 DbgView 查看驅動程式輸出的 log 資訊. 可以發現依序進入派遣函式的是 IRP_MJ_CREATE, IRP_MJ_CLEANUP 和 IRP_MJ_CLOSE. 下圖為執行結果 :


補充說明 :
[ Windows DDP ] 驅動程式的基本結構 : NT 式驅動的基本結構
[ Windows DDP ] 驅動程式的基本結構 : WDM 式驅動的基本結構
[ Windows DDP ] 驅動程式的基本結構 : 實驗 - 從 Log 檢視 驅動物件與裝置物件
Tools : IRPTrace
IrpTrace is a tool that watches I/O request packages (IRP) sent to kernel-mode driver(s) of Windows NT 4.0, Windows 2000 or Windows XP. Information about IRP requests can be sent to remote debugger and/or saved to a file. The collected information is available for instantaneous or deferred analysis, which makes this tool indispensable for debugging and support of device drivers.
This message was edited 14 times. Last update was at 24/12/2010 19:20:29

1 則留言:

  1. Hi, 作者 因為考慮到你有留版權的問題
    所以我將該留言刪除了
    謝謝你的網址 對我幫助很大:D

    回覆刪除

網誌存檔

關於我自己

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