程式扎記: [ Windows DDP ] 派遣函式 : 緩衝區方式讀寫操作

標籤

2011年1月13日 星期四

[ Windows DDP ] 派遣函式 : 緩衝區方式讀寫操作


前言 :
驅動程式所建立的裝置一般會有三種讀寫裝置, 一種是緩衝區方式, 一種是直接方式, 一種是其他方式. 這裡介紹緩衝區方式的讀寫.

緩衝區裝置 :
在驅動程式新建裝置物件的時候, 需要考慮好該裝置是採用何種讀寫方式. 當 IoCreateDevice 新建完裝置後, 需要對裝置物件的 Flags 子欄位進行設置. 設置不同的 Flags 會導致不同的方式操作裝置. 在以前的 DDK 中是這樣設置的 :
- 建立裝置範例代碼 :
  1. //新建設備名稱  
  2. UNICODE_STRING devName;  
  3. RtlInitUnicodeString(&devName,L"\\Device\\MyDDKDevice");  
  4.   
  5. //新建設備  
  6. status = IoCreateDevice( pDriverObject,  
  7.                     sizeof(DEVICE_EXTENSION),  
  8.                     &(UNICODE_STRING)devName,  
  9.                     FILE_DEVICE_UNKNOWN,  
  10.                     0, TRUE,  
  11.                     &pDevObj );  
  12. if (!NT_SUCCESS(status))  
  13.     return status;  
  14.   
  15. pDevObj->Flags |= DO_BUFFERED_IO;  

裝置物件一共可以有三種讀寫方式, 分別是緩衝區方式讀寫, 直接方式讀寫, 其它方式讀寫. 這三種方式的 Flags 分別對應 DO_BUFFERED_IO, DO_DIRECT_IO 和 0. 緩衝區方式讀寫相對簡單.
讀寫操作一般是由 ReadFile 或者 WriteFile 函式引起, 這裡先以 WriteFile 函式為例進行介紹. WriteFile 要求使用者提供一段緩衝區, 並說明緩衝區大小, 然後 WriteFile 將這段記憶體的數據傳入到驅動程式中. 這段緩衝記憶體是使用者模式的記憶體位址, 驅動程式如果直接引用這段記憶體是十分危險的. 因為 Windows 作業系統是多工的, 它可能隨時切換到別的處理程序. 如果驅動程式需要存取這段記憶體, 這時作業系統可能已經切換到另一個處理程序, 如果這樣驅動程式存取的記憶體內容必定是錯誤的, 而這會引起系統崩潰.
有很多方法可以解決這個問題, 其中的一個方法是使用緩衝區方式讀寫. 對於這種方法, 作業系統將應用程式提供緩衝區的資料複製到核心模式下的位址中, 這樣無論作業系統如何切換處理程序, 核心模式位址都不會改變. IRP 的派遣函式將會對核心模式下的緩衝區操作, 而不是操作使用者模式位址的緩衝區.
這樣做的優點是比較簡單, 缺點是需要在使用者模式和核心模式間複製資料, 而影響了執行效率. 在少量記憶體操作時, 可以採用這種辦法.

緩衝區裝置讀寫 :
以緩衝區方式寫入裝置時, 作業系統將 WriteFile 提供的使用者模式的緩衝區複製到核心模式地址下. 這個位址由 WriteFile 新建的 IRP 的 AssociatedIrp.SystemBuffer 子欄位記錄. 以 "緩衝區" 方式讀取裝置時, 作業系統會分配一段核心模式下的記憶體. 這段記憶體大小等於 ReadFile 或者 WriteFile 指定的位元組數, 並且 ReadFile 或者 WriteFile 新建 IRP 的 AssociatedIrp.SystemBuffer 子欄位會記錄這段記憶體位址. 當 IRP 請求結束 (一般由 IoCompleteRequest 函式結束 IRP) , 這段記憶體位址會被複製到 ReadFile 提供的緩衝區中.
以緩衝方式無論是 "讀" 還是 "寫" 裝置, 都會發生使用者模式位址與核心模式位址的資料複製. 複製的過程由作業系統負責. 使用者模式位址由 ReadFile 或者 WriteFile 提供, 核心模式位址由作業系統負責分配和回收. 另外在派遣函式中, 也可以透過 IO_STACK_LOCATION 中的 Parameters.Read.Length 子欄位知道 ReadFile 請求多少位元組. 透過 IO_STACK_LOCATION 中的 Parameters.Write.Length 子欄位知道 WriteFile 請求多少位元組.
然而 WriteFile 和 ReadFile 指定對裝置操作多少位元組, 並不真的意味著操作了這麼多位元組. 在派遣函式中, 應設置 IRP 的子欄位 IoStatus.Information. 這個子欄位記錄裝置實際操作了多少位元組. 下面程式碼演示了如何利用 "緩衝區" 方式讀取裝置. 本例的驅動程式返回給應用程式的資料都是 0xAA, 因此應用程式會從裝置中讀到一連串的 0xAA :
- Driver.cpp : HelloDDKRead 派遣函式
  1. NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj,  
  2.                                  IN PIRP pIrp)   
  3. {  
  4.     KdPrint(("Enter HelloDDKRead\n"));  
  5.   
  6.     //對一般IRP的簡單操作,後面會介紹對IRP更複雜的操作  
  7.     NTSTATUS status = STATUS_SUCCESS;  
  8.   
  9.     PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);  
  10.     ULONG ulReadLength = stack->Parameters.Read.Length;  
  11.       
  12.     // 完成IRP  
  13.     //設置IRP完成狀態  
  14.     pIrp->IoStatus.Status = status;  
  15.   
  16.     //設置IRP操作了多少位元組  
  17.     pIrp->IoStatus.Information = ulReadLength;   // bytes xfered  
  18.   
  19.     memset(pIrp->AssociatedIrp.SystemBuffer,0xAA,ulReadLength);  
  20.   
  21.     //處理IRP  
  22.     IoCompleteRequest( pIrp, IO_NO_INCREMENT );  
  23.   
  24.     KdPrint(("Leave HelloDDKRead\n"));  
  25.   
  26.     return status;  
  27. }  

可以在應用程式使用 ReadFile 對裝置進行讀寫 :
- 範例程式 : 對裝置進行讀的動作
  1. #include   
  2. #include   
  3.   
  4. int main()  
  5. {  
  6.     HANDLE hDevice =   
  7.         CreateFile("\\\\.\\HelloDDK",  
  8.                     GENERIC_READ | GENERIC_WRITE,  
  9.                     0,      // share mode none  
  10.                     NULL,   // no security  
  11.                     OPEN_EXISTING,  
  12.                     FILE_ATTRIBUTE_NORMAL,  
  13.                     NULL );     // no template  
  14.   
  15.     if (hDevice == INVALID_HANDLE_VALUE)  
  16.     {  
  17.         printf("Failed to obtain file handle to device: "  
  18.             "%s with Win32 error code: %d\n",  
  19.             "MyWDMDevice", GetLastError() );  
  20.         return 1;  
  21.     }  
  22.   
  23.     UCHAR buffer[10];  
  24.     ULONG ulRead;  
  25.     BOOL bRet = ReadFile(hDevice,buffer,10,&ulRead,NULL);  
  26.     if (bRet)  
  27.     {  
  28.         printf("Read %d bytes:",ulRead);  
  29.         for (int i=0;i<(int)ulRead;i++)  
  30.         {  
  31.             printf("%02X ",buffer[i]);  
  32.         }  
  33.   
  34.         printf("\n");  
  35.     }  
  36.   
  37.     CloseHandle(hDevice);  
  38.     return 0;  
  39. }  



緩衝區裝置模擬檔案讀寫 :
到目前為止對緩衝區的讀寫將被用來編寫一個虛擬裝置. 這個裝置用來模擬一個檔案, 可以將這個裝置想像成一個普通的檔案對此進行讀寫操作. 另外每次寫入這個檔, 檔案的長度會增加, 可以利用 GetFileSize 函式 得到該檔的長度.
為了實現這個目的, 需要編寫三個 IRP 的派遣函式. 它們分別對應著寫入, 讀取與獲取檔案長度操作. 下面進行說明 :
(1) 寫入操作 : 在應用程式中, 透過 WriteFile 函式對裝置進行寫入操作. 下面程式片段是應用程式分配 10 位元組的緩衝區, 然後將緩衝區填寫 0xBB, 然後將這 10 位元組寫入裝置 :
- 範例程式 : 建立檔案 Handle, 必進行寫入
  1. HANDLE hDevice =   
  2.     CreateFile("\\\\.\\HelloDDK",  
  3.                 GENERIC_READ | GENERIC_WRITE,  
  4.                 0,      // share mode none  
  5.                 NULL,   // no security  
  6.                 OPEN_EXISTING,  
  7.                 FILE_ATTRIBUTE_NORMAL,  
  8.                 NULL );     // no template  
  9.   
  10. if (hDevice == INVALID_HANDLE_VALUE)  
  11. {  
  12.     printf("Failed to obtain file handle to device: "  
  13.         "%s with Win32 error code: %d\n",  
  14.         "MyWDMDevice", GetLastError() );  
  15.     return 1;  
  16. }  
  17.   
  18. UCHAR buffer[10];  
  19. memset(buffer,0xBB,10);  
  20. ULONG ulRead;  
  21. ULONG ulWrite;  
  22. BOOL bRet;  
  23. bRet = WriteFile(hDevice,buffer,10,&ulWrite,NULL);  
  24. if (bRet)  
  25. {  
  26.     printf("Write %d bytes\n",ulWrite);  
  27. }  

WriteFile 內部會新建產生 IRP_MJ_WRITE 類型的 IRP, 作業系統會將這個 IRP 傳遞給驅動程式. IRP_MJ_WRITE 的派遣函式需要將傳進來的資料保存起來, 以便讀取該裝置的時候讀出. 在本例中, 這個資料將儲存在一個緩衝區中, 緩衝區的位址記錄在裝置擴充中. 在裝置啟動的時候, 驅動程式負責分配這個緩衝區, 在裝置被卸載的時候, 驅動程式回收該緩衝區.
對於 IRP_MJ_WRITE 的派遣函式, 主要任務是將寫入的資料儲存在這段緩衝區中. 如果寫入的位元組數過大 (超過緩衝區), 派遣函式將 IRP 的狀態設置成錯誤狀態. 另外在裝置擴充中有一個變數記錄著這個虛擬裝置的檔案長度. 對裝置的寫入操作會更改這個變數 :
- Driver.cpp (HelloDDWrite 函式) : 對寫入處理的派遣函式
  1. NTSTATUS HelloDDWrite(IN PDEVICE_OBJECT pDevObj,  
  2.                                  IN PIRP pIrp)   
  3. {  
  4.     KdPrint(("Enter HelloDDWrite\n"));  
  5.   
  6.     NTSTATUS status = STATUS_SUCCESS;  
  7.   
  8.     PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;  
  9.   
  10.     PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);  
  11.     //獲取存儲的長度  
  12.     ULONG ulWriteLength = stack->Parameters.Write.Length;  
  13.     //獲取存儲的偏移量  
  14.     ULONG ulWriteOffset = (ULONG)stack->Parameters.Write.ByteOffset.QuadPart;  
  15.       
  16.     if (ulWriteOffset+ulWriteLength>MAX_FILE_LENGTH)  
  17.     {  
  18.         //如果存儲長度+偏移量大於緩衝區長度,則返回無效  
  19.         status = STATUS_FILE_INVALID;  
  20.         ulWriteLength = 0;  
  21.     }else  
  22.     {  
  23.         //將寫入的資料,存儲在緩衝區內  
  24.         memcpy(pDevExt->buffer+ulWriteOffset,pIrp->AssociatedIrp.SystemBuffer,ulWriteLength);  
  25.         status = STATUS_SUCCESS;  
  26.         //設置新的檔長度  
  27.         if (ulWriteLength+ulWriteOffset>pDevExt->file_length)  
  28.         {  
  29.             pDevExt->file_length = ulWriteLength+ulWriteOffset;  
  30.         }  
  31.     }  
  32.   
  33.     pIrp->IoStatus.Status = status;  
  34.     pIrp->IoStatus.Information = ulWriteLength;  // bytes xfered  
  35.     IoCompleteRequest( pIrp, IO_NO_INCREMENT );  
  36.     KdPrint(("Leave HelloDDWrite\n"));  
  37.     return status;  
  38. }  

(2) 讀取操作 : 在應用程式中, 透過 ReadFile 從裝置讀取資料, 程式碼如下 :
- main.cpp : 讀取裝置內容
  1. bRet = ReadFile(hDevice,buffer,10,&ulRead,NULL);  
  2. if (bRet)  
  3. {  
  4.     printf("Read %d bytes:",ulRead);  
  5.     for (int i=0;i<(int)ulRead;i++)  
  6.     {  
  7.         printf("%02X ",buffer[i]);  
  8.     }  
  9.   
  10.     printf("\n");  
  11. }  

ReadFile 內部會新建 IRP_MJ_READ 類型的 IRP, 作業系統會將這個 IRP 傳遞給驅動程式中 IRP_MJ_READ 的派遣函式. 而該派遣函式主要任務是把記錄的資料複製到 AssociatedIrp.SystemBuffer 中 :
- Driver.cpp (HelloDDKRead 函式) : 處理讀取操作的派遣函式
  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.     ULONG ulReadLength = stack->Parameters.Read.Length;  
  11.     ULONG ulReadOffset = (ULONG)stack->Parameters.Read.ByteOffset.QuadPart;  
  12.   
  13.     if (ulReadOffset+ulReadLength>MAX_FILE_LENGTH)  
  14.     {  
  15.         status = STATUS_FILE_INVALID;  
  16.         ulReadLength = 0;  
  17.     }else  
  18.     {  
  19.         //將資料存儲在AssociatedIrp.SystemBuffer,以便應用程式使用  
  20.         memcpy(pIrp->AssociatedIrp.SystemBuffer,pDevExt->buffer+ulReadOffset,ulReadLength);  
  21.         status = STATUS_SUCCESS;  
  22.     }  
  23.   
  24.     pIrp->IoStatus.Status = status;  
  25.     pIrp->IoStatus.Information = ulReadLength;   // bytes xfered  
  26.     IoCompleteRequest( pIrp, IO_NO_INCREMENT );  
  27.     KdPrint(("Leave HelloDDKRead\n"));  
  28.   
  29.     return status;  
  30. }  

(3) 讀取檔案長度 : 讀取裝置長度依靠 GetFileSize Win32 API 獲得. GetFileSize 內部會發出 IRP_MJ_QUERY_INFORMATION 類型的 IRP. 這個 IRP 請求的作用是向裝置查詢一些資訊, 這包括查詢檔長度, 裝置新建的時間, 裝置屬性等. 在本例中 IRP_MJ_QUERY_INFORMATION 派遣函式的主要任務是告訴應用程式這個裝置的長度.
IRP_MJ_QUERY_INFORMATION 可以會獲取不同的資訊, 每種資訊對應一種類別號. 該類別號是一個 FILE_INFORMATION_CLASS 類型的列舉變數, 可以讀取 I/O 堆疊的 Parameters.QueryFile.FileInformationClass 子欄位. 查詢裝置長度時, 也就是呼叫 Win32 API GetFileSize 時, Parameters.QueryFile.FileInformationClass 應該為 FileStandardInformtion. 這時 IRP 的緩衝區資料為 FILE_STANDARD_INFORMATION 資料結構的資料 :
- Syntax :
  1. typedef struct _FILE_STANDARD_INFORMATION {  
  2.   LARGE_INTEGER AllocationSize;  
  3.   LARGE_INTEGER EndOfFile;  
  4.   ULONG         NumberOfLinks;  
  5.   BOOLEAN       DeletePending;  
  6.   BOOLEAN       Directory;  
  7. } FILE_STANDARD_INFORMATION, *PFILE_STANDARD_INFORMATION;  

其中 EndOfFile 子欄位指明裝置長度, 些改這個子欄位會在 GetFileSize 的返回值中呈現. IRP_MJ_QUERY_INFORMATION 派遣函式如下 :
- Driver.cpp (HelloDDKQueryInfomation) : 處理檔案長度 Query 的派遣函式
  1. NTSTATUS HelloDDKQueryInfomation(IN PDEVICE_OBJECT pDevObj,  
  2.                                  IN PIRP pIrp)   
  3. {  
  4.     KdPrint(("Enter HelloDDKQueryInfomation\n"));  
  5.   
  6.     PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);  
  7.   
  8.     PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;  
  9.   
  10.     FILE_INFORMATION_CLASS info = stack->Parameters.QueryFile.FileInformationClass;  
  11.     if (info==FileStandardInformation )  
  12.     {  
  13.         KdPrint(("FileStandardInformation\n"));  
  14.         PFILE_STANDARD_INFORMATION file_info  =   
  15.             (PFILE_STANDARD_INFORMATION)pIrp->AssociatedIrp.SystemBuffer;  
  16.         file_info->EndOfFile = RtlConvertLongToLargeInteger(pDevExt->file_length);  
  17.     }  
  18.   
  19.     NTSTATUS status = STATUS_SUCCESS;  
  20.     // 完成IRP  
  21.     pIrp->IoStatus.Status = status;  
  22.     pIrp->IoStatus.Information = stack->Parameters.QueryFile.Length;  // bytes xfered  
  23.     IoCompleteRequest( pIrp, IO_NO_INCREMENT );  
  24.   
  25.     KdPrint(("Leave HelloDDKQueryInfomation\n"));  
  26.   
  27.     return status;  
  28. }  
This message was edited 8 times. Last update was at 25/12/2010 15:26:23

沒有留言:

張貼留言

網誌存檔

關於我自己

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