Windows內核開發-7-IRP和派遣函數


Windows內核-7-IRP和派遣函數

IRP以及派遣函數是Windows中非常重要的概念。IRP 是I/O Request Pocket的簡稱,意思是I/O操作的請求包,Windows中所有User和Kernel之間的交流都會被封裝成一個IRP結構體,然后不同的IRP會被派遣到不同的派遣函數里面,通過派遣函數來實現I/O操作。

IRP

typedef struct _IRP {
 CSHORT                    Type;
 USHORT                    Size;
 PMDL                      MdlAddress;
 ULONG                     Flags;
 union {
   struct _IRP     *MasterIrp;
   __volatile LONG IrpCount;
   PVOID           SystemBuffer;
} AssociatedIrp;
 LIST_ENTRY                ThreadListEntry;
 IO_STATUS_BLOCK           IoStatus;
 KPROCESSOR_MODE           RequestorMode;
 BOOLEAN                   PendingReturned;
 CHAR                      StackCount;
 CHAR                      CurrentLocation;
 BOOLEAN                   Cancel;
 KIRQL                     CancelIrql;
 CCHAR                     ApcEnvironment;
 UCHAR                     AllocationFlags;
 union {
   PIO_STATUS_BLOCK UserIosb;
   PVOID            IoRingContext;
};
 PKEVENT                   UserEvent;
 union {
   struct {
     union {
       PIO_APC_ROUTINE UserApcRoutine;
       PVOID           IssuingProcess;
    };
     union {
       PVOID                 UserApcContext;
#if ...
       _IORING_OBJECT        *IoRing;
#else
       struct _IORING_OBJECT *IoRing;
#endif
    };
  } AsynchronousParameters;
   LARGE_INTEGER AllocationSize;
} Overlay;
 __volatile PDRIVER_CANCEL CancelRoutine;
 PVOID                     UserBuffer;
 union {
   struct {
     union {
       KDEVICE_QUEUE_ENTRY DeviceQueueEntry;
       struct {
         PVOID DriverContext[4];
      };
    };
     PETHREAD     Thread;
     PCHAR        AuxiliaryBuffer;
     struct {
       LIST_ENTRY ListEntry;
       union {
         struct _IO_STACK_LOCATION *CurrentStackLocation;
         ULONG                     PacketType;
      };
    };
     PFILE_OBJECT OriginalFileObject;
  } Overlay;
   KAPC  Apc;
   PVOID CompletionKey;
} Tail;
} IRP;

 

 

IRP這種機制類似於Windows的消息機制,驅動在接受到IRP之后會根據IRP的不同類型分配給不同類型的派遣函數來處理IRP。

IRP不是單獨的,只要創建了IRP就會跟着創建IRP的I/O棧,有一個棧是給內核驅動用的:

img

驅動需要調用IoGetCurrentIrpStackLocation函數來獲取內驅驅動對應的I/O棧。

例如:

auto stack = IoGetCurrentIrpStackLocation(Irp)

該API返回一個IO_STACK_LOCATION 結構體:

typedef struct _IO_STACK_LOCATION {
 UCHAR                  MajorFunction;
 UCHAR                  MinorFunction;
 UCHAR                  Flags;
 UCHAR                  Control;
 union {
...
...
...
...
...
} Parameters;
 PDEVICE_OBJECT         DeviceObject;
 PFILE_OBJECT           FileObject;
 PIO_COMPLETION_ROUTINE CompletionRoutine;
 PVOID                  Context;
} IO_STACK_LOCATION, *PIO_STACK_LOCATION;

這個結構體有兩個重要的屬性,分別是MajorFunction和MinorFunction,分別記錄了IRP的主類型和子類型,操作系統根據MajorFunction來將IRP派遣到不同的派遣函數里面去處理,而還可以繼續利用MinorFunction來判斷更多的內容,基本上MajorFunction用的比較多。

設置IRP和派遣函數

在DriverEntry驅動對象里面有一個函數指針數組MajorFunction就是來記錄派遣函數的地址,來讓派遣函數和IRP一一對應,這個數組里面采用宏定義來將每個 IRP和派遣函數通過數組的索引來一一對應:

#define IRP_MJ_CREATE                   0x00
#define IRP_MJ_CREATE_NAMED_PIPE       0x01
#define IRP_MJ_CLOSE                   0x02
#define IRP_MJ_READ                     0x03
#define IRP_MJ_WRITE                   0x04
#define IRP_MJ_QUERY_INFORMATION       0x05
#define IRP_MJ_SET_INFORMATION         0x06
#define IRP_MJ_QUERY_EA                 0x07
#define IRP_MJ_SET_EA                   0x08
#define IRP_MJ_FLUSH_BUFFERS           0x09
#define IRP_MJ_QUERY_VOLUME_INFORMATION 0x0a
#define IRP_MJ_SET_VOLUME_INFORMATION   0x0b
#define IRP_MJ_DIRECTORY_CONTROL       0x0c
#define IRP_MJ_FILE_SYSTEM_CONTROL     0x0d
#define IRP_MJ_DEVICE_CONTROL           0x0e
#define IRP_MJ_INTERNAL_DEVICE_CONTROL 0x0f
#define IRP_MJ_SHUTDOWN                 0x10
#define IRP_MJ_LOCK_CONTROL             0x11
#define IRP_MJ_CLEANUP                 0x12
#define IRP_MJ_CREATE_MAILSLOT         0x13
#define IRP_MJ_QUERY_SECURITY           0x14
#define IRP_MJ_SET_SECURITY             0x15
#define IRP_MJ_POWER                   0x16
#define IRP_MJ_SYSTEM_CONTROL           0x17
#define IRP_MJ_DEVICE_CHANGE           0x18
#define IRP_MJ_QUERY_QUOTA             0x19
#define IRP_MJ_SET_QUOTA               0x1a
#define IRP_MJ_PNP                     0x1b

這些每一個都有特定的意思,比較常用的有:

IRP_MJ_CREATE //創建 和CreateFile對應
IRP_MJ_READ //讀取 和ReadFile對應
IRP_MJ_WRITE //寫入 和WriteFile對應
IRP_MJ_CLOSE //關閉 和CloseFile對應

所有的派遣函數都有一個原型:

typedef NTSTATUS DRIVER_DISPATCH (
_In_ PDEVICE_OBJECT DeviceObject,
_Inout_ PIRP Irp);
//typedef可以省去,名字自己取

IRP傳遞流程

I/O系統是以設備對象為中心,而不是以驅動對象為中心的。IRP可以在設備對象中傳來傳去:

img

但是不管是在怎么傳遞,最后都必須把這個IRP請求結束,給它完成。

完成必備操作:

NTSTATUS SysMonRead(PDEVICE_OBJECT, PIRP Irp) {
Irp->IoStatus.Status = status;//設置狀態
Irp->IoStatus.Information = count;//統計處理的字節數
IoCompleteRequest(Irp, IO_NO_INCREMENT);//完成IO操作的必須返回函數
return status;
}

IoCompleteRequest這個API需要返回一個IRP大家應該沒問題,但是第二個參數就比較復雜了,第二個參數是返回之后的線程級,這是因為一個線程在執行IRP的時候會等待。

這里以ReadFile為例,ReadFile函數會調用ntdll中的NtReadFile函數,然后ntdll中的NtReadFile函數又調用內核的NtReadFile函數,然后內核的NtReadFile函數創建關於Read這個類型的IO_MJ_READ類型的IRP再將它發送到內核的對應IRP的派遣函數里面,然后等待IRP對應派遣函數執行完之后再返回。所以需要設置當返回后線程又重新執行了的線程級。

User操作設備對象

前面我們說了,User只能通過符號鏈接來操作設備對象進行I/O交互,所以User只需要在User的API下面把通常使用的文件路徑改成符號鏈接就好了。

比如:

CreateFile(L"\\\\.\\test", GENERIC_READ, 0, nullptr, OPEN_EXISTING, 0, nullptr)

注意C語言是有轉義字符的,所以這個第一個參數看起來怪怪的。

讀寫方式

IRP和派遣函數是用來進行I/O操作的,I/O操作就是讀寫,所以很自然的有了這個讀寫方式的環境。

一般讀寫方式有三種:緩沖I/O,直接I/O,和其它方式。三種方式對應的Flags分別是DO_BUFFERED_IO、DO_DIRECT_IO和0。都在DeviceObject設備對象里面添加。

例如:

DeviceObject->Flags |= 0;

注意:並不是直接賦值

緩沖I/O

讀寫操作通常是WriteFile和ReadFile這類API,這類API會要求添加緩沖區指針和緩沖區大小作為函數參數,然后WriteFile/ReadFile將這段內存的數據傳遞給驅動程序。由於這段緩沖區是用戶模式的內存地址,所以直接使用會非常危險,因為Windows是多進程操作系統,有可能在你用的時候就被別的進程改了,所以直接使用非常危險。

而緩沖I/O的原理就是,在內核模式下開辟一個緩沖空間來存儲該緩沖區內容,然后讀取的時候先放到內核緩沖區里面,再來進行復制和賦值操作。

比如:當調用ReadFile/WriteFile時,操作系統提供內核模式下的一段地址來存放User在WriteFile里面配置的Buffer,然后當IRP請求結束,內核區域的Buffer就會被拷貝到User/Kernel里面。

1 I/O 管理器從非分頁池中分配一個與用戶緩沖區大小相同的緩沖區。它將指向這個新緩沖區的指針存儲在 IRP 的 AssociatedIrp->SystemBuffer 成員中。 (緩沖區大小可以在當前 I/O 堆棧位置的 Parameters.Read.Length 或 Parameters.Write.Length 中找到。)

2 對於寫請求,I/O 管理器將用戶的緩沖區復制到系統緩沖區。

3 現在才調用驅動程序的調度例程。驅動程序可以直接使用系統緩沖區指針而無需任何檢查,因為緩沖區在系統空間中(它的地址是絕對的 - 從任何進程上下文中都一樣),並且在任何 IRQL 中,因為緩沖區是從非分頁池分配的,所以它不能被調出。

4 一旦驅動完成IRP(IoCompleteRequest),I/O管理器(對於讀請求)將系統緩沖區復制回用戶的緩沖區(復制的大小由IRP中設置的IoStatus.Information字段決定)司機)。

5 最后,I/O 管理器釋放系統緩沖區。

 

圖文講解一下ReadFile的整體流程:(WriteFile以此類推)

 

 

這里是User下一個Buffer緩沖區:

img

操作系統知道是緩沖I/O后開辟了一個內核緩沖區,然后由WriteFile/ReadFile創建的Irp->AssociatedIrp.SystemBuffer來存儲記錄

img

然后驅動訪問系統空間往里面寫東西

 

img

操作系統將系統空間的內容拷貝到User緩沖區里。

img

拷貝完了,釋放系統空間內存。

img

 

派遣函數中可以通過Irp的Parameters.Read.Length來知道User請求多少字節,也可以通過Parameters.Write.Length來知道要寫入多少字節,但是真正執行了多少是通過Irp的IoStatus.Information字段來返回,所以WriteFile/ReadFile函數中各個參數是意義就可以解釋通透了。

 

但是這樣的缺點是會造成內存空間開銷,因為內核空間是公有的大家都得用,而且由於大量數據的復制也會影響效率,所以針對比較小的內容采用緩沖I/O可以采取這種辦法,別的還是用直接I/O吧。

//緩沖I/O的例子:
NTSTATUS SysMonRead(PDEVICE_OBJECT, PIRP Irp) {
auto status = STATUS_SUCCESS;
auto stack = IoGetCurrentIrpStackLocation(Irp);//得到當前的IRP棧
auto ulReadLength = stack->Parameters.Read.Length;//得到要讀取的字節數


//完成IRP操作
Irp->IoStatus.Status = status;//設置IRP完成狀態
Irp->IoStatus.Information = ulReadLength;//設置實際完成的IRP字節數
memset(Irp->AssociatedIrp.SystemBuffer, 0xAA, ulReadLength);//拷貝內容到系統地址空間
IoCompleteRequest(Irp, IO_NO_INCREMENT);//完成IRP操作
return status;
}

User下的就不用寫了吧,hh,偷個懶。

直接I/O

直接I/O用了另一個辦法來規避風險。

1 I/O 管理器確保用戶的緩沖區有效。

img

2 將其映射到物理內存中,然后將緩沖區鎖定在內存中,因此在另行通知之前無法將其調出。這解決了緩沖區訪問的問題之一——不會發生頁面錯誤,因此在任何 IRQL 中訪問緩沖區都是安全的。

img

3 I/O 管理器構建內存描述符列表 (MDL),這是一種知道緩沖區如何映射到 RAM 的數據結構。該數據結構的地址存儲在 IRP 的 MdlAddress 字段中。

img

4 此時,驅動程序調用派遣函數。因為用戶的緩沖區被鎖定在 RAM 中,不能從任意線程訪問。所以當驅動程序需要訪問緩沖區時,它必須調用將同一用戶緩沖區映射到系統地址的函數。由於該地址被鎖進了系統中所以,該地址在任何進程上下文中都是有效的。所以本質上,我們得到了到同一個緩沖區的兩個映射。一個來自原始地址(僅在請求者進程的上下文中有效),另一個來自系統空間,始終有效。要調用的 API 是 MmGetSystemAddressForMdlSafe,傳遞由 I/O 管理器構建的 MDL。返回值是系統地址。

img

5 User或Kernel進行緩沖區修改:

img

 

6結束: 一旦驅動程序完成請求,I/O 管理器刪除第二個映射(到系統空間),釋放 MDL 並解鎖用戶緩沖區,因此它可以像任何其他用戶模式內存一樣正常分頁。

img

 

這里全程沒有用到拷貝,完完全全就是通過地址映射User->內存,然后Kernel修改內存等同於直接修改User的緩沖區。

直接I/O在用戶態下:

直接I/O操作系統會在User下用MDL這個數據結構來存儲相關信息:

 

 

大小保存在MDL->ByteCount里,然后這段虛擬內存的首地址在MDL->StartVa里,實際緩沖區的首地址相當於起始地址的偏移地址在ByteOffset里,DDK里面封裝了幾個宏來方便用

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

直接I/O在Kernel下

Kernel下比較簡單粗暴直接用了一個宏來得到MDL在內核模式下的地址映射

MmGetSystemAddressForMdlSafe();

然后通過前面緩沖I/O用到的一些Read的length或者Write的Length直接通過這個地址拷貝就行了。

直接I/O例子

 

NTSTATUS TestRead(IN PDEVICE_OBJECT pDevObj,
IN PIRP pIrp)
{
KdPrint(("Enter HelloDDKRead\n"));

PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
NTSTATUS status = STATUS_SUCCESS;

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);

ULONG ulReadLength = stack->Parameters.Read.Length;
KdPrint(("ulReadLength:%d\n",ulReadLength));

ULONG mdl_length = MmGetMdlByteCount(pIrp->MdlAddress);
PVOID mdl_address = MmGetMdlVirtualAddress(pIrp->MdlAddress);
ULONG mdl_offset = MmGetMdlByteOffset(pIrp->MdlAddress);

KdPrint(("mdl_address:0X%08X\n",mdl_address));
KdPrint(("mdl_length:%d\n",mdl_length));
KdPrint(("mdl_offset:%d\n",mdl_offset));

if (mdl_length!=ulReadLength)
{
pIrp->IoStatus.Information = 0;
status = STATUS_UNSUCCESSFUL;
}else
{
//ÓÃMmGetSystemAddressForMdlSafe
PVOID kernel_address = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority);
KdPrint(("kernel_address:0X%08X\n",kernel_address));
memset(kernel_address,0XAA,ulReadLength);
pIrp->IoStatus.Information = ulReadLength; // bytes xfered
}

pIrp->IoStatus.Status = status;

IoCompleteRequest( pIrp, IO_NO_INCREMENT );
KdPrint(("Leave TestRead\n"));

return status;
}

其它

其它的I/O讀寫方式用得非常少,而且很麻煩,這里就不介紹了。

 

IO設備控制操作

除了常用的ReadFile,WriteFile,CreateFile,CloseFile這類操作外,還可以通過另一個API DeviceIoControl來操作設備。DeviceIoControl會創建一個IRP_MJ_DEVICE_CONTROL類型的IRP,別的和其它的IRP以及派遣函數是一樣的。

DeviceIoControl和驅動交互

DeviceIoControl除了可以被用來讀寫還可以用在其它操作上。

BOOL DeviceIoControl(
 HANDLE       hDevice, //設備對象句柄
 DWORD        dwIoControlCode, //控制碼
 LPVOID       lpInBuffer, //輸入緩沖區
 DWORD        nInBufferSize, //輸入緩沖區大小
 LPVOID       lpOutBuffer, //輸出緩沖區
 DWORD        nOutBufferSize, //輸出緩沖區大小
 LPDWORD      lpBytesReturned, //實際返回字節數 這個對應這Irp->IoStatus.Information
 LPOVERLAPPED lpOverlapped //是否OVERLAP操作
);

dwIoControlCode是I/O控制碼,也叫IOCTL值。

Windows有一些內置的I/O控制碼可以選用:(在官方文檔上可以獲取:DeviceIoControl function (ioapiset.h) - Win32 apps | Microsoft Docs

 

同樣的我們也可以自己定義,Windows提供了控制碼的定義規定:

 

 

 

在ddk頭文件里面有一個宏定義方便我們使用:

#define CTL_CODE( DeviceType, Function, Method, Access ) (((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))

DeviceType(31-16):設備對象的類型,這個和IoCreateDevice時設置的設備對象類型一樣。

Access(15-14):訪問權限,如果沒有特殊要求,一般采用FILE_ANY_ACCESS

Funciton:0X000~0X7FF由微軟保留,0x800~0xFFF由程序員自己定義

Method:操作模式,以下四種之一:

1 METHOD_BUFFERED: 使用緩沖區方式操作

2 METHOD_IN_DIRECT: 使用直接寫方式操作

3 METHOD_OUT_DIRECT: 使用直接讀方式操作

4 METHOD_NEITHER: 使用其它方式操作

 

緩沖內存模式IOCTL

DeviceIoControl的緩沖讀取和前面的緩沖I/O有一點不一樣,前面的流程都一樣,都是復制到系統進程的緩沖區里面,然后這個緩沖區地址可以由Irp->AssociatedIrp.SystemBuffer來獲取。不一樣的是DeviceIoControl會傳入輸入和輸出兩個緩沖區,但是兩個緩沖區對應的是一個地址,因為如果是輸入就是輸出到Kernel里,Kernel可以進行操作后,再把這個緩沖區修改了然后作為輸出,輸出到User里。

首先定義一個自己的IOCTL碼

#define IOCTL_TEST CTL_CODE(FILE_DEVICE_UNKNOWN,0X800,METHOD_BUFFERED,FILE_ANY_ACCESS)

在內核狀態下要使用IOCTL需要添加ntddk頭文件,在User下需要添加winioctl.h頭文件

NTSTATUS DeviceIoControl(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
auto stack = IoGetCurrentIrpStackLocation(pIrp);

auto status = STATUS_SUCCESS;

//得到輸入緩沖區大小
auto cbIn = stack->Parameters.DeviceIoControl.InputBufferLength;

//得到輸出緩沖區大小
auto cbOut = stack->Parameters.DeviceIoControl.OutputBufferLength;

//得到IOCTL控制碼
auto code = stack->Parameters.DeviceIoControl.IoControlCode;
ULONG info = 0;
switch (code)
{
case IOCTL_TEST1:
{
//處理輸入給內核的緩沖區內容
UCHAR* InputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;
for (ULONG i = 0; i < cbIn; i++)
{
KdPrint(("%x\n", InputBuffer[i]));
}

//處理輸出給User的緩沖區內容
UCHAR* OutputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;
memset(OutputBuffer, 0xAA, cbOut);
info = cbOut;
break;
}
default:
{
status = STATUS_INVALID_VARIANT;
break;
}

}

pIrp->IoStatus.Information = info;
pIrp->IoStatus.Status = status;

IoCompleteRequest(pIrp, IO_NO_INCREMENT);

}

 

直接內存模式IOCTL

直接內存模式IOCTL也和直接I/O有稍許區別,直接IOCTL中的輸入緩沖區就是前面的緩沖內存模式下的緩沖區,直接開辟一個系統緩沖區,但是輸出緩沖區就是前面的直接I/O,通過地址映射來得到的地址。所以直接內存模式的IOCTL需要分兩種來處理,一種是輸入緩沖區當緩沖I/O處理,另一種是輸出緩沖區當直接I/O來處理。

這里需要說明一下直接模式的

METHOD_IN_DIRECT: 使用直接寫方式操作

METHOD_OUT_DIRECT: 使用直接讀方式操作

這兩種方式的區別,在調用DeviceIoControl的時候是會指定打開的模式是只讀還是只寫,還是可讀可寫,就對應着這兩種,如果是只讀,那么只有METHOD_IN_DIRECT編寫的IOCTL控制代碼才會識別才會有用,以此類推。

直接內存模式IOCTL的例子:

1 先創建IOCTL

#define IOCTL_TEST1 CTL_CODE(FILE_DEVICE_UNKNOWN,0X801,METHOD_IN_DIRECT,FILE_ANY_ACCESS)

2 編寫對應IOCTL的派遣函數

NTSTATUS DeviceIoControl(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
auto stack = IoGetCurrentIrpStackLocation(pIrp);

auto status = STATUS_SUCCESS;

//得到輸入緩沖區大小
auto cbIn = stack->Parameters.DeviceIoControl.InputBufferLength;

//得到輸出緩沖區大小
auto cbOut = stack->Parameters.DeviceIoControl.OutputBufferLength;

//得到IOCTL控制碼
auto code = stack->Parameters.DeviceIoControl.IoControlCode;
ULONG info = 0;
switch (code)
{
case IOCTL_TEST1:
{
//處理輸入給內核的緩沖區內容
UCHAR* InputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;
for (ULONG i = 0; i < cbIn; i++)
{
KdPrint(("%x\n", InputBuffer[i]));
}

//處理輸出給User的緩沖區內容
UCHAR* OutputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;
memset(OutputBuffer, 0xAA, cbOut);
info = cbOut;
break;
}
case IOCTL_TEST2:
{
//當是IOCTL_TEST2的IOCTL時的代碼邏輯
auto InputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;
for (int i = 0; i < cbIn; i++)
{
KdPrint(("%X\n", InputBuffer[i]));
}

auto OutputBuffer = (UCHAR*)MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority);
memset(OutputBuffer, 0xAA, cbOut);
info = cbOut;
break;
}
default:
{
status = STATUS_INVALID_VARIANT;
break;
}

}

pIrp->IoStatus.Information = info;
pIrp->IoStatus.Status = status;

IoCompleteRequest(pIrp, IO_NO_INCREMENT);

}

 

其它內存模式IOCTL

其它模式用的很少,而且很麻煩,這里也不介紹了。

 

 

總結

不管是DeviceIoControl還是ReadFile,WriteFile其實都是用作User和Kernel交互的API,其中的IRP是自帶的數據結構體,里面保存了要交互的東西的信息。DeviceIoControl更像輸入東西給Kernel讓Kernel通過根據輸入的內容和控制碼來執行命令的一個東西,通過I/O來控制Kernel執行一些代碼流程;而ReadFille/WriteFile可能用得更多的是在單純的交互數據上面。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM