2011年1月17日 星期一

[ Windows DDP ] 派遣函式 : 直接方式讀寫操作

前言 : 
前面介紹了對裝置緩衝區方式讀寫, 這裡將介紹對裝置的直接讀寫方式. 

直接讀取原理 : 
除了 "緩衝區" 方是讀寫裝置外, 另一種方法是直接對裝置進行讀寫. 這種方式需要新建完裝置物件後, 在設置物件屬性時設置為 DO_DIRECT_IO, 而不是設置 DO_BUFFERED_IO. 

  1. //新建裝置  
  2. status = IoCreateDevice( pDriverObject,  
  3.                     sizeof(DEVICE_EXTENSION),  
  4.                     &(UNICODE_STRING)devName,  
  5.                     FILE_DEVICE_UNKNOWN,  
  6.                     0, TRUE,  
  7.                     &pDevObj );  
  8. if (!NT_SUCCESS(status))  
  9.     return status;  
  10.   
  11. pDevObj->Flags |= DO_DIRECT_IO; // 使用直接讀寫操作  
在應用程式進行讀寫的時候, 例如使用 WriteFile 寫入裝置時, 會要求使用者提供一段緩衝區, 這段緩衝區裡面是要寫入裝置的資料. 作業系統在新建 IRP_MJ_WRITE 的時候, 將需要寫入的資料複製到 IRP 資料結構中的 AssociatedIrp.SystemBuffer 中, 對於讀取裝置的操作也是類似的. 
和緩衝區方式讀寫裝置的不同, 直接讀寫方式時, 作業系統會將使用者模式下的緩衝區鎖住. 然後作業系統將這段緩衝區在核心模式位址再做一次映射. 這樣使用者模式的緩衝區和核心模式的緩衝區指向的就是同一區域的實體記憶體. 無論作業系統如何切換處理程序, 核心模式位址都保持不變. 
作業系統先將使用者模式的位址鎖定後, 作業系統用記憶體描述表 (MDL 資料結構) 記錄這段記憶體. 使用者模式的緩衝區在虛擬記易體上是連續的, 但是在實體記憶體上可能是離散的. MDL 記錄這段虛擬記憶體, 這段虛擬記憶體的大小儲存在 mdl->ByteCount 裡, 這段虛擬記憶體的第一個頁位址是 mdl->StartVa, 這段虛擬記憶體的首位址對於第一個頁位址的偏移量是 mdl->ByteOffset. 因此這段虛擬記憶體的首位址應該是 mdl->StartVal + mdl->ByteOffset. DDK 提供了幾個巨集方便程式設計師得到這幾個值 : 

#define MmGetMdlByteCount(Mdl) ((Mdl)->ByteCount)
#define MmGetMdlByteOffset(Mdl) ((Mdl)->ByteOffset)
#define MmGetMdlVirtualAddress(Mdl) ((PVOID)((PCHAR)((Mdl)->StartVa) + (Mdl)->ByteOffset))

底下是 MDL 示意圖 : 
 
裝置的直接讀寫 : 
前面介紹了直接寫入裝置的原理, 下面結合程式碼演式示如何編寫直接讀寫裝置的派遣函式. 這理示範 "IRP_MJ_READ" 的派遣函式, 其他的如寫入操作可以參考它完成. 應用程式呼叫 Win32 API ReadFile, 作業系統將 IRP_MJ_READ 轉發到對應的派遣函式中. 派遣函式透過讀取 I/O 堆疊的 stack->Parameters.Read.Length 來獲取這次讀取的長度. 這個長度實際上就是 ReadFile 函式的第三個參數 nNumberOfBytesToRead. 
派遣函式設置 pIrp->IoStatus.Information 告訴 ReadFile 實際讀取了多少位元組. 這個數值對應著 ReadFile 的第四個參數 : 

- Syntax :
  1. BOOL WINAPI ReadFile(  
  2.   __in         HANDLE hFile,  
  3.   __out        LPVOID lpBuffer,  
  4.   __in         DWORD nNumberOfBytesToRead,  
  5.   __out_opt    LPDWORD lpNumberOfBytesRead,  
  6.   __inout_opt  LPOVERLAPPED lpOverlapped  
  7. );  

透過 IRP 的 pIrp->MdlAddress 得到 MDL 資料結構, 這個結構描述了被鎖定的緩衝區記憶體. 透過 DDK 的三個巨集 MmGetMdlByteCount, MmGetMdlVirtualAddress 與 MmGetMdlByteOffset 可以得到鎖定緩衝區的長度, 虛擬記憶體位址與偏移量. 如果派遣函式的返回值是 STATUS_SUCCESS 則 ReadFile 返回 TRUE, 表明操作成功. 如果派遣函式返回不是 STATUS_SUCCESS, 則 ReadFile 返回 False, 表明操作失敗. 如果失敗可以透過 Win32 API GetLastError 得到錯誤程式碼. 
透過上述介紹, 可以了解直接讀寫裝置的原理, 下面給出這種派遣函式的一段範例代碼 : 

- 範例代碼 : MDL 直接讀取操作
  1. NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj,  
  2.                                  IN PIRP pIrp)   
  3. {  
  4.     KdPrint(("Enter HelloDDKRead\n"));  
  5.   
  6.     PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;  
  7.     NTSTATUS status = STATUS_SUCCESS;  
  8.   
  9.     PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);  
  10.   
  11.     ULONG ulReadLength = stack->Parameters.Read.Length;  
  12.     KdPrint(("ulReadLength:%d\n",ulReadLength));  
  13.   
  14.     ULONG mdl_length = MmGetMdlByteCount(pIrp->MdlAddress);  
  15.     PVOID mdl_address = MmGetMdlVirtualAddress(pIrp->MdlAddress);  
  16.     ULONG mdl_offset = MmGetMdlByteOffset(pIrp->MdlAddress);  
  17.       
  18.     KdPrint(("mdl_address:0X%08X\n",mdl_address));  
  19.     KdPrint(("mdl_length:%d\n",mdl_length));  
  20.     KdPrint(("mdl_offset:%d\n",mdl_offset));  
  21.   
  22.     if (mdl_length!=ulReadLength)  
  23.     {  
  24.         //MDL的長度應該和讀長度相等,否則該操作應該設為不成功  
  25.         pIrp->IoStatus.Information = 0;  
  26.         status = STATUS_UNSUCCESSFUL;  
  27.     }else  
  28.     {  
  29.         //用MmGetSystemAddressForMdlSafe得到MDL在核心模式下的映射  
  30.         PVOID kernel_address = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority);  
  31.         KdPrint(("kernel_address:0X%08X\n",kernel_address));  
  32.         memset(kernel_address,0XAA,ulReadLength);  
  33.         pIrp->IoStatus.Information = ulReadLength;   // bytes xfered  
  34.     }  
  35.       
  36.     pIrp->IoStatus.Status = status;  
  37.       
  38.     IoCompleteRequest( pIrp, IO_NO_INCREMENT );  
  39.     KdPrint(("Leave HelloDDKRead\n"));  
  40.   
  41.     return status;  
  42. }  

底下是應用程式執行結果, 可以知道應用程式提供的緩衝位址為 0x0012FE74 : 
 
從執行結果可以看出, 應用程式提供的緩衝區 0x0012FE74. 接著從派遣函式印出來的虛擬位址也是 0x0012FE74. 呼叫 MmGetSystemAddressForMdlSafe 後 , 0x0012FE74 位址被重新映射為位址 0xF7CB8E74. 映射以後驅動程式讀寫 0xF7CB8E74 就相當於讀寫 0x0012FE74 地址了. 地下是執行過程 : 

沒有留言:

張貼留言

[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...