2011年1月17日 星期一

[ Windows DDP ] 派遣函式 : IO 裝置控制操作

前言 : 
除了用 ReadFile 和 WriteFile 以外, 應用程式還可以透過另一個 Win32 API DeviceIoControl 操作裝置. DeviceIoControl 內部會使作業系統新建一個 IRP_MJ_DEVICE_CONTROL 類型的 IRP, 然後作業系統會將這個 IRP 轉發到派遣函式中. 
程式設計師可以用 DeviceIoControl 定義除讀寫之外的操作, 它可以讓應用程式和驅動程式進行通訊. 例如要對一個裝置進行初始化操作, 程式設計師自訂一種 I/O 控制碼, 然後用 DeviceIoControl 將這個控制碼和請求一起傳遞給驅動程式. 在派遣函式中, 分別對不同的 I/O 控制碼進行處理. 

DeviceIoControl 與驅動互動 : 
DeviceIoControl 的宣告方式如下 : 

- Syntax :
  1. BOOL WINAPI DeviceIoControl(  
  2.   __in         HANDLE hDevice,  
  3.   __in         DWORD dwIoControlCode,  
  4.   __in_opt     LPVOID lpInBuffer,  
  5.   __in         DWORD nInBufferSize,  
  6.   __out_opt    LPVOID lpOutBuffer,  
  7.   __in         DWORD nOutBufferSize,  
  8.   __out_opt    LPDWORD lpBytesReturned,  
  9.   __inout_opt  LPOVERLAPPED lpOverlapped  
  10. );  

其中 IpBytesReturned 對應到派遣函式中的 IRP 結構中的 pIrp->IoStatus.Information. DeviceIoControl 的第二個參數是 I/O 控制碼, 也稱 IOCTL 值, 是一個 32 位元的無符號整數型. IOCTL 需要符合 DDK 的規定, 如下圖所示 : 
 
DDK 提供了一個巨集 CTL_CODE, 其定義如下 : 

- Syntax :
  1. CTL_CODE(  
  2.   DeviceType,  
  3.   Function,  
  4.   Method,  
  5.   Access  
  6. );  

參數說明 : 

* DeviceType : 裝置物件的類型, 這個類型應該和新建裝置 (IoCreateDevice) 時的類型相匹配. 一般是 FILE_DEVICE_XX 長相的巨集.
* Function : 這是驅動程式定義的 IOCTL 碼. 其中 :
- 0x0000 到 0x7FFF > 微軟保留
- 0x8000 到 0xFFFF > 程式設計師自己定義

* Method : 這示操作模式, 可已是下列四種模式之一
- METHOD_BUFFERED : 使用緩衝區方式操作
- METHOD_IN_DIRECT : 使用直接寫方式操作
- METHOD_OUT_DIRECT : 使用直接讀方式操作
- METHOD_NEITHER : 使用其他方式操作

* Access : 存取權限, 如果沒有特殊要求, 一般使用 FILE_ANY_ACCESS

緩衝記憶體模式 IOCTL : 
這一節介紹緩衝區模式的 IOCTL. 在用 CTL_CODE 巨集定義這種 IOCTL 時, 應該指定 Method 參數為 METHOD_BUFFERED. 前面介紹曾經多次提到, 在驅動中最好不要用直接存取使用者模式下的記憶體位址, 緩衝區方式可以避免程式設計師存取使用者模式下的記憶體位址. 
在 Win32 API DeviceIoControl 的內部, 使用者提供的輸入緩衝區內容被複製到 IRP 中的 pIrp->AssociatedIrp.SystemBuffer 記憶體位址, 複製的位元組數是由 DeviceIoControl 指定的輸入位元組數. 接著派遣函式讀取 pIrp->Associated.SystemBuffer 的記憶體位址, 從而獲得應用程式提供的緩衝區資料. 另外派遣函式還可以寫入 pIrp->Associated.SystemBuffer 的記憶體位址, 當作裝置的輸出資料. 作業系統會將這個位址的資料再次複製到 DeviceIoControl 提供的輸出緩衝區. 複製的位元組數由 pIrp->InStatus.Information 指定. DeviceIoControl 也可以透過它的第七個參數得到操作的位元組數. 
派遣函式首先透過 IoGetCurrentIrpStackLocation 函式得到當前的 I/O 堆疊 (IO_STACK_LOCATON). 派遣函式透過 stack->Parameters.DeviceIoControl.InputBufferLength 得到輸入緩衝區大小, 透過 stack->Parameters.DeviceIoControl.OutputBufferLength 得到輸出緩衝區大小, 最後透過 stack->Parameters.DeviceIoControl.IoControlCode 得到 IOCTL. 在派遣函式透過 C 語言中的 switch 語句分別處理不同的 IOCTL. 下面例子示範緩衝區方式處理 IOCTL 的派遣函式, 首先用 CTL_CODE 巨集定義定義 IOCTL 碼 : 

- ioctls.h :
  1. #define IOCTL_TEST1 CTL_CODE(\  
  2.             FILE_DEVICE_UNKNOWN, \  
  3.             0x800, \  
  4.             METHOD_BUFFERED, \  
  5.             FILE_ANY_ACCESS)  

在驅動程式中使用 CTL_CODE 需要包含 NTDDK.h 標頭檔, 而在應用程式中使用 CTL_CODE 需要包含 winioctl.h 標頭檔. 下面是 IOCTL 的派遣函式 : 

- Driver.cpp (HelloDDKDeviceIOControl 函式) :
  1. #pragma PAGEDCODE  
  2. NTSTATUS HelloDDKDeviceIOControl(IN PDEVICE_OBJECT pDevObj,  
  3.                                  IN PIRP pIrp)  
  4. {  
  5.     NTSTATUS status = STATUS_SUCCESS;  
  6.     KdPrint(("Enter HelloDDKDeviceIOControl\n"));  
  7.   
  8.     //得到當前堆疊  
  9.     PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);  
  10.     //得到輸入緩衝區大小  
  11.     ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength;  
  12.     //得到輸出緩衝區大小  
  13.     ULONG cbout = stack->Parameters.DeviceIoControl.OutputBufferLength;  
  14.     //得到IOCTL碼  
  15.     ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;  
  16.   
  17.     ULONG info = 0;  
  18.   
  19.     switch (code)  
  20.     {                       // process request  
  21.         case IOCTL_TEST1:  
  22.         {  
  23.             KdPrint(("IOCTL_TEST1\n"));  
  24.             //緩衝區方式IOCTL  
  25.             //顯示輸入緩衝區資料  
  26.             UCHAR* InputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;  
  27.             for (ULONG i=0;i
  28.             {  
  29.                 KdPrint(("%X\n",InputBuffer[i]));  
  30.             }  
  31.   
  32.             //操作輸出緩衝區  
  33.             UCHAR* OutputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;  
  34.             memset(OutputBuffer,0xAA,cbout);  
  35.             //設置實際操作輸出緩衝區長度  
  36.             info = cbout;  
  37.             break;  
  38.         }  
  39.   
  40.         default:  
  41.             status = STATUS_INVALID_VARIANT;  
  42.     }  
  43.   
  44.     // 完成IRP  
  45.     pIrp->IoStatus.Status = status;  
  46.     pIrp->IoStatus.Information = info;   // bytes xfered  
  47.     IoCompleteRequest( pIrp, IO_NO_INCREMENT );  
  48.   
  49.     KdPrint(("Leave HelloDDKDeviceIOControl\n"));  
  50.   
  51.     return status;  
  52. }  

底下是應用程式呼叫 DeviceIoControl 對裝置進行操作的範例代碼 : 

- main.cpp : 對緩衝區模式 DeviceIoControl 操作範例
  1. #include   
  2. #include   
  3. //使用CTL_CODE必須加入winioctl.h  
  4. #include   
  5. #include "..\NT_Driver\Ioctls.h"  
  6.   
  7. int main()  
  8. {  
  9.     HANDLE hDevice =   
  10.         CreateFile("\\\\.\\HelloDDK",  
  11.                     GENERIC_READ | GENERIC_WRITE,  
  12.                     0,      // share mode none  
  13.                     NULL,   // no security  
  14.                     OPEN_EXISTING,  
  15.                     FILE_ATTRIBUTE_NORMAL,  
  16.                     NULL );     // no template  
  17.   
  18.     if (hDevice == INVALID_HANDLE_VALUE)  
  19.     {  
  20.         printf("Failed to obtain file handle to device: "  
  21.             "%s with Win32 error code: %d\n",  
  22.             "MyWDMDevice", GetLastError() );  
  23.         return 1;  
  24.     }  
  25.   
  26.     UCHAR InputBuffer[10];  
  27.     UCHAR OutputBuffer[10];  
  28.     //將輸入緩衝區全部置成0XBB  
  29.     memset(InputBuffer,0xBB,10);  
  30.     DWORD dwOutput;  
  31.     //輸入緩衝區作為輸入,輸出緩衝區作為輸出  
  32.   
  33.     BOOL bRet;  
  34.     bRet = DeviceIoControl(hDevice, IOCTL_TEST1, InputBuffer, 10, &OutputBuffer, 10, &dwOutput, NULL);  
  35.     if (bRet)  
  36.     {  
  37.         printf("Output buffer:%d bytes\n",dwOutput);  
  38.         for (int i=0;i<(int)dwOutput;i++)  
  39.         {  
  40.             printf("%02X ",OutputBuffer[i]);  
  41.         }  
  42.         printf("\n");  
  43.     }  
  44.     CloseHandle(hDevice);  
  45.     return 0;  
  46. }  

底下是執行結果 : 
 

直接記憶體模式 IOCTL : 
這節介紹直接方式的 IOCTL. 在用 CTL_CODE 巨集定義這種 IOCTL 時, 應該指定 Method 參數為 METHOD_OUT_DIRECT 或者 METHOD_IN_DIRECT. 直接模式的 IOCTL 同樣可以避免驅動程式存取使用者模式的記憶體位址. 
在呼叫 DeviceIoControl 時, 輸入緩衝區的內容被複製到 IRP 中的 pIrp->AssociatedIrp.SystemBuffer 記憶體位址, 複製的位元組數是按照 DeviceIoControl 指定輸入位元組數. 這個步驟和緩衝區模式 IOCTL 的處理是一樣的. 但是對於 DeviceIoControl 指定的輸出緩衝區的處理, 直接模式的 IOCTL 和緩衝區模式的 IOCTL 卻是以不同方式處理. 作業系統會將 DeviceIoControl 指定的輸出緩衝區鎖定, 然後在核心模式位址重新映射一段位址. 派遣函式中的 IRP 結構中的 pIrp->MdlAddress 記錄 DeviceIoControl 指定的輸出緩衝區. 派遣函式應該使用 MmGetSystemAddressForMdlSafe 將這段記憶體映射到核心模式下的記憶體位址. 
另外在派遣函式中, 需要先透過 IoGetCurrentIrpStackLocation 函式得到當前 I/O 堆疊. 然後透過 stack->Parameters.DeviceIoControl.InputBufferLength 得到輸入緩衝區大小, 透過 stack->Parameters.DeviceIoControl.OutputBufferLength 得到輸出緩衝區大小. 最後透過 stack->Parameters.DeviceIoControl.IoControlCode 得到 IOCTL. 在派遣函式中透過 C 語言中的 switch 語句分別處理不同的 IOCTL. 
首先用 CTL_CODE 巨集定義 IOCTL 碼 : 

- Ioctls.h (IOCTL_TEST2) : 定義 CTL_CODE 提供函式 DeviceIoControl 參數使用
  1. #define IOCTL_TEST2 CTL_CODE(\  
  2.             FILE_DEVICE_UNKNOWN, \  
  3.             0x801, \  
  4.             METHOD_IN_DIRECT, \  
  5.             FILE_ANY_ACCESS)  

在驅動程式中使用 CTL_CODE 需要包含 NTDDK.h 標頭檔, 而在應用程式中使用 CTL_CODE 需要包含 winioctl.h 標頭檔. 下面是 IOCTL 的派遣函式 : 

- Driver.cpp : 參考 case IOCTL_TEST2
  1. #pragma PAGEDCODE  
  2. NTSTATUS HelloDDKDeviceIOControl(IN PDEVICE_OBJECT pDevObj,  
  3.                                  IN PIRP pIrp)  
  4. {  
  5.     NTSTATUS status = STATUS_SUCCESS;  
  6.     KdPrint(("Enter HelloDDKDeviceIOControl\n"));  
  7.   
  8.     //得到當前堆疊  
  9.     PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);  
  10.     //得到輸入緩衝區大小  
  11.     ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength;  
  12.     //得到輸出緩衝區大小  
  13.     ULONG cbout = stack->Parameters.DeviceIoControl.OutputBufferLength;  
  14.     //得到IOCTL碼  
  15.     ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;  
  16.   
  17.     ULONG info = 0;  
  18.   
  19.     switch (code)  
  20.     {                       // process request  
  21.         case IOCTL_TEST1:  
  22.         {  
  23.             KdPrint(("IOCTL_TEST1\n"));  
  24.             //緩衝區方式IOCTL  
  25.             //顯示輸入緩衝區資料  
  26.             UCHAR* InputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;  
  27.             for (ULONG i=0;i
  28.             {  
  29.                 KdPrint(("%X\n",InputBuffer[i]));  
  30.             }  
  31.   
  32.             //操作輸出緩衝區  
  33.             UCHAR* OutputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;  
  34.             memset(OutputBuffer,0xAA,cbout);  
  35.             //設置實際操作輸出緩衝區長度  
  36.             info = cbout;  
  37.             break;  
  38.         }  
  39.         case IOCTL_TEST2:  
  40.         {  
  41.             KdPrint(("IOCTL_TEST2\n"));  
  42.             //緩衝區方式IOCTL  
  43.             //顯示輸入緩衝區資料  
  44.   
  45.             //緩衝區方式IOCTL  
  46.             //顯示輸入緩衝區資料  
  47.             UCHAR* InputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;  
  48.             for (ULONG i=0;i
  49.             {  
  50.                 KdPrint(("%X\n",InputBuffer[i]));  
  51.             }  
  52.   
  53.             //pIrp->MdlAddress為DeviceIoControl輸出緩衝區位址相同  
  54.             KdPrint(("User Address:0X%08X\n",MmGetMdlVirtualAddress(pIrp->MdlAddress)));  
  55.   
  56.             UCHAR* OutputBuffer = (UCHAR*)MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority);  
  57.             //InputBuffer被映射到核心模式下的記憶體位址,必定在0X80000000-0XFFFFFFFF之間  
  58.             memset(OutputBuffer,0xAA,cbout);  
  59.             //設置實際操作輸出緩衝區長度  
  60.             info = cbout;  
  61.             break;  
  62.         }  
  63.         default:  
  64.             status = STATUS_INVALID_VARIANT;  
  65.     }  
  66.   
  67.     // 完成IRP  
  68.     pIrp->IoStatus.Status = status;  
  69.     pIrp->IoStatus.Information = info;   // bytes xfered  
  70.     IoCompleteRequest( pIrp, IO_NO_INCREMENT );  
  71.   
  72.     KdPrint(("Leave HelloDDKDeviceIOControl\n"));  
  73.   
  74.     return status;  
  75. }  

在應用程式部分可以參考 緩衝記憶體模式 IOCTL 的, 只是把在呼叫 DeviceIoControl 的參數由 IOCTL_TEST1 換成 IOCTL_TEST2. 底下為執行結果 : 

4 則留言:

  1. 不好意思 問個問題
    IOCTL的直接記憶體模式有分兩種
    METHOD_IN_DIRECT 使用直接寫方式操作
    METHOD_OUT_DIRECT 使用直接讀方式操作
    看了相關範例後發現這兩種的寫法好像相同
    那這兩種到底差在哪?

    回覆刪除
    回覆
    1. Document is your best teacher:
      https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/buffer-descriptions-for-i-o-control-codes

      刪除
  2. 作者已經移除這則留言。

    回覆刪除

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