2010年12月26日 星期日

[ Windows DDP ] 驅動程式的基本結構 : WDM 式驅動的基本結構

前言 : 
在 Windows 2000 以後, 微軟公司加入了新的驅動程式模型, 這就是 WDM. WDM 模式是建立在 NT 式驅動程式模型基礎上的. 因此有了前面對 NT 式驅動的理解, 對於 WDM 驅動的基本架構應該可以很容易上手. 

實體裝置物件與功能裝置物件 : 
在 WDM 模型中, 完成一個裝置的操作, 至少有兩個裝置物件共同完成. 其中一個是實體裝置物件 (Physical Device Object, 簡稱 PDO). 另一個是功能裝置物件 (Function Device Object, 簡稱 FDO). 其關係是 "附加" 與 "被附加" 的關係. 
當 PC 插入某個裝置的時候, PDO 會自動建立. 確切的說是由匯流排驅動新建的. PDO 不能單獨操作裝置, 需要配合 FDO 一起使用. 系統會提示檢測到新裝置. 要求安裝驅動程式. 需要安裝的驅動程式指的就是 WDM 程式, 此驅動程式負責新建 FDO, 並且附加到 PDO 之上. 
當一個 FDO 附加到 PDO 上的時候, PDO 裝置物件的子欄位 AttachedDevice 會記錄 FDO 的位置. PDO 被稱作底層驅動或者下層驅動, 而 FDO 被稱作高層驅動或者上層驅動. 這裡上層是指接近發出 I/O 請求的地方, 而 "下層" 指的是靠近實體裝置的地方. PDO 和 FDO 的關係可以從下圖得到更好理解 : 
 

這是一個簡單的一種情況, 事實上要比這個複雜一些. 在 FDO 和 PDO 之間還會存在篩選驅動, 如下圖所示. 在 FDO 上面的篩選驅動被稱為上層篩選驅動. 在 FDO 的下層驅動被稱為下層篩選驅動. 另外每個裝置中, 有個 StackSize 子欄位表明操作這個裝置需要幾層才能達到最下層的實體裝置. 最上層篩選裝置物件的 StackSize 為 4, 也就是需要 4 個裝置才能到達最底層的實體裝置 : 
 
篩選驅動可以巢狀嵌套, 也就是可以有多個高層篩選驅動, 也可以有多個底層篩選驅動. 篩選驅動不是必須存在. 在 WDM 模型中 PDO 和 FDO 是必須的. 可以看出 NT 式驅動和 WDM 驅動在設計思維上有所不同. NT 裝置是被動被裝入的, 例如當有裝置插入 PC 後, 系統不會提示, 使用者必須自己指定載入何種驅動. 而 WDM 驅動則會在插入裝置後, 系統自動新建出 PDO, 並且提示用戶安裝 FDO. 
WDM 提示使用者載入 FDO, 如果該裝置已經由微軟提供, 則會自動進行安裝. 例如當 USB 滑鼠插入 PC 後, 系統就會找到對應驅動並載入. 這種設計思維導致了 WDM 模型支援隨插即用功能. 當然為了支持隨插即用功能, 這些還遠遠不夠. 匯流排驅動新建的 PDO 為程式設計員提供了很多隨插即用的服務, 這在以後的章節會接著介紹. 

WDM 驅動的入口程式 : 
和 NT 驅動一樣, WDM 驅動的入口程式也是 DriverEntry, 但是初始化作用被分散到其它常式中. 例如新建裝置物件的責任就不在 DriverEntry 中, 而被放在了 AddDrvice 常式中. 同時在 DriverEntry 中, 需要設置對 IRP_MJ_PNP 處理的派遣常式. 下面是 HelloWDM 的 DriverEntry 函式 : 

- HelloWDM.cpp (DriverEntry 函式) :
  1. #pragma INITCODE   
  2. extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,  
  3.                                 IN PUNICODE_STRING pRegistryPath)  
  4. {  
  5.     KdPrint(("Enter DriverEntry\n"));  
  6.   
  7.     pDriverObject->DriverExtension->AddDevice = HelloWDMAddDevice;  
  8.     pDriverObject->MajorFunction[IRP_MJ_PNP] = HelloWDMPnp;  
  9.     pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] =   
  10.     pDriverObject->MajorFunction[IRP_MJ_CREATE] =   
  11.     pDriverObject->MajorFunction[IRP_MJ_READ] =   
  12.     pDriverObject->MajorFunction[IRP_MJ_WRITE] = HelloWDMDispatchRoutine;  
  13.     pDriverObject->DriverUnload = HelloWDMUnload;  
  14.   
  15.     KdPrint(("Leave DriverEntry\n"));  
  16.     return STATUS_SUCCESS;  
  17. }  

從上面程式碼可以看出 WDM 驅動的 DriverEntry 和 NT 式驅動的 DriverEntry 有以下幾點不同 : 
* 增加了對 AddDevice 函式的設置. 這是 WDM 驅動和 NT 驅動非常重要的不同點. 因為 NT 驅動是主動載入裝置的, 也就是驅動一旦被載入就新建裝置. 而 WDM 驅動是被動載入裝置的. 作業系統必須載入 PDO 後, 呼叫驅動的 AddDevice 常式, AddDevice 常式中負責新建 FDO 並附加到 PDO 之上.
* 新建裝置物件已經不在這個含是中了, 而在 AddDevice 常式中新建.
* 必須加入 IRP_MJ_PNP 的派遣 Callback 函式. IRP_MJ_PNP 主要是負責電腦中隨插即用的處理, 在 WDM 驅動中加入了很多隨插即用的處理.

WDM 驅動的 AddDevice 常式 : 
AddDevice 常式是 WDM 驅動所獨有的, 在 NT 驅動中沒有該常式. 在 DriverEntry 中, 需要設置 AddDevice 常式的函式位址. 設置的方式是驅動物件中有個 DriverExtension 子欄位 DriverExtension 中有個 AddDevice 子欄位, 將該欄位指向 AddDevice 常式的函式位址. 
pDriverObject->DriverExtension->AddDevice = HelloWDMAddDevice; 

和 DriverEntry 不同, AddDevice 常式的名字可以任意命名, 在 HelloWDM 例子中, 使用的名字就是 HelloWDMAddDevice : 
- HelloWDM.cpp (HelloWDMAddDevice 函式) :
  1. #pragma PAGEDCODE  
  2. NTSTATUS HelloWDMAddDevice(IN PDRIVER_OBJECT DriverObject,  
  3.                            IN PDEVICE_OBJECT PhysicalDeviceObject)  
  4. {   
  5.     PAGED_CODE();  
  6.     KdPrint(("Enter HelloWDMAddDevice\n"));  
  7.   
  8.     NTSTATUS status;  
  9.     PDEVICE_OBJECT fdo;  
  10.     UNICODE_STRING devName;  
  11.     RtlInitUnicodeString(&devName,L"\\Device\\MyWDMDevice");  
  12.     // 新建裝置物件          
  13.     status = IoCreateDevice(  
  14.         DriverObject,  
  15.         sizeof(DEVICE_EXTENSION),  
  16.         &(UNICODE_STRING)devName,  
  17.         FILE_DEVICE_UNKNOWN,  
  18.         0,  
  19.         FALSE,  
  20.         &fdo);  
  21.     if( !NT_SUCCESS(status))  
  22.         return status;  
  23.     // 得到裝置擴充  
  24.     PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension;  
  25.     // 將 FDO 附加在 PDO 上  
  26.     pdx->fdo = fdo;  
  27.     pdx->NextStackDevice = IoAttachDeviceToDeviceStack(fdo, PhysicalDeviceObject);  
  28.     UNICODE_STRING symLinkName;  
  29.     RtlInitUnicodeString(&symLinkName,L"\\DosDevices\\HelloWDM");  
  30.   
  31.     // 用裝置擴充記錄裝置名和符號連結  
  32.     pdx->ustrDeviceName = devName;  
  33.     pdx->ustrSymLinkName = symLinkName;  
  34.     // 新建符號連結  
  35.     status = IoCreateSymbolicLink(&(UNICODE_STRING)symLinkName,&(UNICODE_STRING)devName);  
  36.   
  37.     if( !NT_SUCCESS(status))  
  38.     {  
  39.         IoDeleteSymbolicLink(&pdx->ustrSymLinkName);  
  40.         status = IoCreateSymbolicLink(&symLinkName,&devName);  
  41.         if( !NT_SUCCESS(status))  
  42.         {  
  43.             return status;  
  44.         }  
  45.     }  
  46.     // 設置裝置標誌  
  47.     fdo->Flags |= DO_BUFFERED_IO | DO_POWER_PAGABLE;  
  48.     fdo->Flags &= ~DO_DEVICE_INITIALIZING;  
  49.   
  50.     KdPrint(("Leave HelloWDMAddDevice\n"));  
  51.     return STATUS_SUCCESS;  
  52. }  

從上述程式碼可以看出, AddDevice 常是類似於 NT 驅動中的 DriverEntry 新建裝置物件的相關操作, 但是略有不同. AddDevice 函式有兩個輸入參數, 一個是 DriverObject, 一個是裝置物件 PhysicalDeviceObject. 驅動物件是 I/O 管理器新建的驅動物件. 裝置物件 PhysicalDeviceObject 就是底層匯流排驅動新建的 PDO 裝置物件. 傳進該參數的目的就是將 FDO 附加在 PDO 之上. 在 AddDevice 可以分為下面幾個步驟 : 
1. 在 AddDevice 透過 IoCreateDevice 等函式, 新建了裝置物件, 該裝置物件就是 FDO, 即功能驅動裝置物件. 和 NT 驅動一樣, 可以設置驅動物件名稱, 也可以不設置.
2. 新建完 FDO 後, 需要將 FDO 的位址保存起來, 以便日後使用. 保存的位置是在裝置擴充中, 前面介紹過在驅動程式中應該盡量避免使用全域變數, 而使用裝置擴充. 如果該電腦中存在多個同類裝置, 例如插入兩個相同型號網卡, 作業系統會呼叫兩次 AddDevice 常式. 每個 AddDevice 常式會新建各自的 FDO 並記錄在各字裝置擴充中.
3. 驅動程式將新建的 FDO 附加在 PDO 上, 附加的這個動作是依靠 IoAttachDeviceToDeviceStack 函式 實現的.
4. 設置 FDO 的 Flags 子欄位. DO_BUFFERED_IO 是定義裝置為 "緩衝記憶體裝置". 另外 ~DO_DEVICE_INITIALIZING 是將 Flag 上的 DO_DEVICE_INITIALIZING 為清零. 保證裝置初始化完畢, 這一步是必需的.

前面介紹過當 FDO 附加到 PDO 上面時, PDO 會透過 AttachedDevice 子欄位知道它上面的裝置是 FDO (或是篩選裝置) . 但是 FDO 卻不知道自己的下層是什麼裝置. 解決辦法就是透過裝置擴充記錄 FDO 下層的裝置. 下面是 HelloWDM 的裝置擴充定義 : 
  1. typedef struct _DEVICE_EXTENSION  
  2. {  
  3.     PDEVICE_OBJECT fdo;             // FDO  
  4.     PDEVICE_OBJECT NextStackDevice;     //下層驅動裝置  
  5.     UNICODE_STRING ustrDeviceName;  // 裝置名  
  6.     UNICODE_STRING ustrSymLinkName; // 符號連結名  
  7. } DEVICE_EXTENSION, *PDEVICE_EXTENSION;  
程式員可以根據自己需求訂制自己的裝置擴充. 子欄位 FDO 是為了保存 FDO 的位址, 以備日後使用. 子欄位 NextStackDevice 是為了定位裝置的下一層位置. 在附加操作完成後, 需要設定符號連結, 以便使用者應用程式可以存取該裝置. 

DriverUnload 常式 : 
在 NT 式驅動中, DriverUnload 常式主要負責作刪除裝置和取消符號連結. 而在 WDM 驅動中, 這部分的操作被 IRP_MN_REMOVE_DEVICE 的 IRP 處理函式所負責, 而 DriverUnload 常式顯得相對簡單. 如果在 DriverEntry 中有申請記憶體的操作, 可以在 DriverUnload 常式中回收. 在 HelloWDK 程式中, DriverUnload 常式除了列印兩行 log 資訊, 什麼也沒做. 

對 IRP_MN_REMOVE_DEVICE IRP 的處理 : 
關於 IRP 的介紹, 後面會有詳細說明. 驅動程式內部是由 IRP 所驅動. 新建 IRP 的原因有很多, IRP_MN_REMOVE_DEVICE 這個 IRP 是當裝置需要被卸載時, 由隨插即用管理器新建, 並發送到驅動程式中. IRP 一般由兩個號碼指定該 IRP 的具體意義, 一個是 IRP 號 (Major IRP), 另一個是輔 IRP 號 (Minor IRP). 每個 IRP 都由對應的派遣函式所處理, 而派遣函式是在 DriverEntry 中指定的, 例如 : 
pDriverObject->MajorFunction[IRP_MJ_PNP] = HelloWDMPnp; 

上面這行程式就是將 IRP_MJ_PNP 的派遣函式指定為 HelloWDMPnp 函式. 
當裝置需要被卸載時, 會先後發出多個 IRP_MJ_PNP. 這些 IRP 的輔 IRP 號會有所不同. 其中之一是 IRP_MN_REMOVE_DEVICE. 在 WDM 驅動程式中, 對裝置的卸載一般是對 IRP_MN_REMOVE_DEVICE 處理函式中進行卸載. 在 HelloWDM 驅動程式中, 負責處理 IRP_MN_REMOVE_DEVICE 的函式是 HandleRemoveDevice , 其程式碼如 : 
- HelloWDM.cpp (HandleRemoveDevice 函式) :
  1. #pragma PAGEDCODE  
  2. NTSTATUS HandleRemoveDevice(PDEVICE_EXTENSION pdx, PIRP Irp)  
  3. {  
  4.     PAGED_CODE();  
  5.     KdPrint(("Enter HandleRemoveDevice\n"));  
  6.   
  7.     Irp->IoStatus.Status = STATUS_SUCCESS;  
  8.     NTSTATUS status = DefaultPnpHandler(pdx, Irp);  
  9.     IoDeleteSymbolicLink(&(UNICODE_STRING)pdx->ustrSymLinkName);  
  10.   
  11.     //呼叫IoDetachDevice()把fdo從裝置堆疊中脫開:  
  12.     if (pdx->NextStackDevice)  
  13.         IoDetachDevice(pdx->NextStackDevice);  
  14.       
  15.     //刪除fdo:  
  16.     IoDeleteDevice(pdx->fdo);  
  17.     KdPrint(("Leave HandleRemoveDevice\n"));  
  18.     return status;  
  19. }  

在處理 IRP_MN_REMOVE_DEVICE 函式中, 它功能類似於 NT 驅動中的 DriverUnload 函式. 除了需要刪除裝置, 取消符號連結外, 在此函式還需要將 FDO 從 PDO 上的堆疊中 "摘除" 下來. 使用的函式是 IoDetachDevice. 
在 FDO 從裝置鏈中被刪除後, 但 PDO 還是存在. PDO 的刪除不是由程式設計師負責, 而是由作業系統負責.

4 則留言:

  1. 嗨 您好
    最近在練習windows driver常參考您的網誌
    但相關幾篇裡的圖片幾乎都死光了 不知是否可以重新補齊
    感謝~

    回覆刪除
    回覆
    1. 補充:

      您的網誌寫得相當清楚 很少看到這麼詳細的

      謝謝~

      刪除
    2. Image host 失聯了, 圖檔沒有備份...Orz 可以支持一下原著:
      http://www.books.com.tw/products/0010439379

      刪除

[Git 常見問題] error: The following untracked working tree files would be overwritten by merge

  Source From  Here 方案1: // x -----删除忽略文件已经对 git 来说不识别的文件 // d -----删除未被添加到 git 的路径中的文件 // f -----强制运行 #   git clean -d -fx 方案2: 今天在服务器上  gi...