程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 派遣函數IRP

派遣函數IRP

編輯:C++入門知識

派遣函數是Windows驅動程序中的重要概念。驅動程序的主要功能是負責處理I/O請求,其中大部分I/O請
求是在派遣函數中處理的。

用戶模式下所有對驅動程序的I/O請求,全部由操作系統轉換為一個叫做IRP數據結構,不同的IRP會被“
派遣”到不同的派遣函數中。


IRP與派遣函數

IRP的處理機制類似於Windows應用程序中的“消息處理”,驅動程序接收到不同的IRP後,會進入不同的
派遣函數,在派遣函數中IRP得到處理。


1.IRP

在Windows內核中,有一種數據結構叫做IRP(I/O Request Package),即輸入輸出請求包。上層應用程
序與底層驅動程序通信時,應用程序會發出I/O請求。操作系統將I/O請求轉化為相應的IRP數據,不同類
型的IRP會被傳遞到不同的派遣函數中。

IRP有兩個基本的重要屬性,一個是MajorFunction,另一個MinorFunction,分別記錄IRP的主類型和子
類型,操作系統根據MajorFunction將IRP“派遣”到不同的派遣函數中,在派遣函數中還可以繼續判斷
這個IRP屬於哪種MinorFunction。

下面是HelloDDK的DriverEntry中關於派遣函數的注冊:

[cpp] view plaincopy
#pragma INITCODE
extern "C" NTSTATUS DriverEntry(
IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegisterPath
)
{
NTSTATUS status;
KdPrint(("Enter DriverEntry\n"));

//設置卸載函數
pDriverObject->DriverUnload = HelloDDKUnload;

//設置派遣函數
pDriverObject->MajorFunction[IRP_MJ_CREATE] = HelloDDKDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloDDKDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = HelloDDKDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_READ] = HelloDDKDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_CLEANUP] = HelloDDKDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_SET_INFORMATION] = HelloDDKDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = HelloDDKDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_SHUTDOWN] = HelloDDKDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = HelloDDKDispatchRoutine;

//創建驅動設備對象
status = CreateDevice(pDriverObject);

KdPrint(("Leave DriverEntry\n"));
return status;

}

2.IRP的類型
文件I/O的相關函數,如CreateFile,ReadFile,WriteFile,CloseHandle等函數會使操作系統產生出I
RP_MJ_CREATE,IRP_MJ_READ,IRP_MJ_WRITE,IRP_MJ_CLOSE等不同的IRP。另外,內核中的文件I/O處理
函數,如ZwCreateFile,ZwReadFile,ZwWriteFile,ZwClose,他們同樣會產以上IRP。

一下列出了IRP的類型,並對其產生的來源做了說明


IRP類型 來源

------------------------------------------------------------------------------------------
-----------------------------------------------------

IRP_MJ_CREATE 創建設備,CreateFile會產生此IRP

------------------------------------------------------------------------------------------
-----------------------------------------------------

IRP_MJ_CLOSE 關閉設備,CloseHandle會產生此IR
P

------------------------------------------------------------------------------------------
-----------------------------------------------------
IRP_MJ_CLEANUP 清除工作,CloseHandle會產生此IRP

------------------------------------------------------------------------------------------
-----------------------------------------------------
IRP_MJ_DEVICE_CONTROL DeviceControl函數會產生此IRP

------------------------------------------------------------------------------------------
-----------------------------------------------------
IRP_MJ_PNP 即插即用消息,NT驅動不支持次
IRP,WDM驅動才支持次IRP

------------------------------------------------------------------------------------------
-----------------------------------------------------
IRP_MJ_POWER 在操作系統處理電源消息時,產生次
IRP

------------------------------------------------------------------------------------------
-----------------------------------------------------
IRP_MJ_QUERY_INFORMATION 獲取文件長度,GetFileSize會產生IRP

------------------------------------------------------------------------------------------
-----------------------------------------------------
IRP_MJ_READ 讀取設備內容,ReadFile會產生此
IRP

------------------------------------------------------------------------------------------
-----------------------------------------------------
IRP_MJ_SET_INFORMATION 設置文件長度,GetFileSize會產生IRP

------------------------------------------------------------------------------------------
-----------------------------------------------------
IRP_MJ_SHUTDOWN 關閉系統前會產生此IRP

------------------------------------------------------------------------------------------
-----------------------------------------------------
IRP_MJ_SYSTEM_CONTROL 系統內部產生的控制信息,類似於內核調用DeviceC
ontrol函數

------------------------------------------------------------------------------------------
-----------------------------------------------------
IRP_MJ_WRITE 對設備進行WriteFile時會產生此
IRP

------------------------------------------------------------------------------------------
-----------------------------------------------------


3.對派遣函數的簡單處理

大部分的IRP都源於文件I/O處理Win32API,處理這些IRP最簡單的方法就是在相應的派遣函數中,將IRP
狀態設置為成功,然後結束IRP的請求,並讓派遣函數成功返回。結束IRP的請求使用函數IoCompleteRe
quest.。下面代碼演示了一種最簡單的處理IRP請求的派遣函數。

[plain] view plaincopy
NTSTATUS HelloDDKDispatchRoutine(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
{
KdPrint(("Enter HelloDDKDispatchRoutine\n"));
//對一般IRP的簡單操作
NTSTATUS status = STATUS_SUCCESS;
//設置IRP完成狀態
pIrp->IoStatus = status;
//設置IRP操作了多少字節
pIrp->IoStatus.Information = 0;
//處理IRP
IoCompleteRequest(pIrp,IO_NO_INCREMENT);
KdPrint(("Leave HelloDDKDispatchRputine"));
return status;
}

本例中,派遣函數設置了IRP的完成狀態為STATUS_SUCCESS。這樣,發起I/O操作請求的Win32API將會返
回TRUE。相反則會返回FALSE。這種情況時,可以使用GetLastError Win32API得到錯誤代碼,所得的錯
誤代碼會和IRP設置的狀態一致。
除了設置IRP的完成狀態,派遣函數還要設置這個IRP操作了多少字節。

派遣函數將IRP請求結束,這是通過IoCompleteRequest函數完成的。


4.通過設備鏈接打開設備

要打開設備,必須通過設備名字才能得到該設備的句柄。前面介紹過,每個設備都有設備名稱,如Hell
oDDK驅動程序的設備名稱為“\\Device\\MyDDKDevice”,但是設備名稱無法被用戶模式下的應用程序查
詢到,設備名只能被內核模式下的程序查詢到。在應用程序中需要通過符號鏈接進行訪問。

下面程序演示在用戶模式下打開驅動設備:

[cpp] view plaincopy
#include <windows.h>
#include <stdio.h>

int main()
{
HANDLE hDevice =
CreateFile("\\\\.\\HelloDDK",
GENERIC_READ | GENERIC_WRITE,
0, // share mode none
NULL, // no security
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL ); // no template

if (hDevice == INVALID_HANDLE_VALUE)
{
printf("Failed to obtain file handle to device: "
"%s with Win32 error code: %d\n",
"MyWDMDevice", GetLastError() );
return 1;
}

CloseHandle(hDevice);
return 0;
}


5.編寫一個更通用的派遣函數
在Windows驅動開發中,有一個重要的內核數據結構,IO_STACK_LOCATION,即I/O堆棧,這個數據結構和
IRP緊密相連。

驅動對象會創建一個個設備對象,並將這些設備對象“疊”成一個垂直結構,被稱為“設備棧”。IRP會
被操作系統發送到設備棧頂層,如果頂層設備結束了本次IRP的請求,則I/O請求結束,如果不讓I/O請求
結束,可以將IRP繼續轉發到下一層設備。因此,一個IRP可能會被轉發多次。為了記錄IRP在每層設備中
的操作,IRP會有一個IO_STACK_LOCATION數組,每個IO_STACK_LOCATION元素記錄著對應設備中做的操作
。對於本層的IO_STACK_LOCATION,可以通過IoGetCurrentIrpStackLocation函數得到。IO_STACK_LOCA
TION結構中會記錄IRP的類型,即IO_STACK_LOCATION中的MajorFuncation子域。

下面代碼增加了派遣函數的難度:

[plain] view plaincopy
#pragma PAGEDCODE
NTSTATUS HelloDDKDispatchRoutine(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
{
KdPrint(("Enter HelloDDKDispatchRoutine\n"));

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
//建立一個字符串數組與IRP類型對應起來
static char * irpname[] =
{
"IRP_MJ_CREATE",
"IRP_MJ_CREATE_NAMED_PIPE",
"IRP_MJ_CLOSE",
"IRP_MJ_READ",
"IRP_MJ_WRITE",
"IRP_MJ_QUERY_INFORMATION",
"IRP_MJ_SET_INFORMATION",
"IRP_MJ_QUERY_EA",
"IRP_MJ_SET_EA",
"IRP_MJ_FLUSH_BUFFERS",
"IRP_MJ_QUERY_VOLUME_INFORMATION",
"IRP_MJ_SET_VOLUME_INFORMATION",
"IRP_MJ_DIRECTORY_CONTROL",
"IRP_MJ_FILE_SYSTEM_CONTROL",
"IRP_MJ_DEVICE_CONTROL",
"IRP_MJ_INTERNAL_DEVICE_CONTROL",
"IRP_MJ_SHUTDOWN",
"IRP_MJ_LOCK_CONTROL",
"IRP_MJ_CLEANUP",
"IRP_MJ_CREATE_MAILSLOT",
"IRP_MJ_QUERY_SECURITY",
"IRP_MJ_SET_SECURITY",
"IRP_MJ_POWER",
"IRP_MJ_SYSTEM_CONTROL",
"IRP_MJ_DEVICE_CHANGE",
"IRP_MJ_QUERY_QUOTA",
"IRP_MJ_SET_QUOTA",
"IRP_MJ_PNP",
};
UCHAR type = stack->MajorFunction;
if (type >= arraysize(irpname))
{
KdPrint(("-Unknow IRP ,major type %X\n",type));
}
else
{
KdPrint(("\t%s\n",irpname[type]));
}
//對一般IRP的簡單操作,後面會介紹對IRP更復雜的操作
NTSTATUS status = STATUS_SUCCESS;
//完成IRP
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = 0;
IoCompleteRequest(pIrp,IO_NO_INCREMENT);
KdPrint(("Leave HelloDDKDispatchRoutine\n"));

return status;

}


緩沖區方式讀寫操作

驅動程序所創建的設備一般會有三種讀寫方式,一種是緩沖區方式,一種是直接方式,一種是其他方式


1.緩沖區方式

IOCreateDevice創建完設備後,需要對設備對象的Flags子域進行設置,設置不同的Flags會導致以不同
的方式操作設備。

設備對象一共可以有三種讀寫方式,這三種方式的Flags分別對應為DO_BUFFERED_ID,DO_DIRECT_IO和0
,緩沖區方式讀寫相對簡單。

讀寫操作一般是由ReadFile或者WriteFile函數引起的,這裡以WriteFile函數為例進行介紹。WriteFil
e要求用戶提供一段緩沖區,並且說明緩沖區的大小,然後WriteFile將這段內存的數據傳入到驅動程序
中。

這段緩沖區內存是用戶模式的內存地址,驅動程序如果直接引用這段內存是十分危險的。如果以緩沖
區方式讀寫,操作系統會將應該用程序提供緩沖區的數據復制到內核模式下的地址中,這樣無論操作系
統如何切換進程,內核模式的地址都不回改變。IRP派遣函數真正操作的是內核模式下的緩沖區地址,而
不是用戶模式下的緩沖區地址。但是這樣做會有一定的效率影響。


2.緩沖區設備讀寫

以緩沖區方式寫設備時,操作系統將WriteFile提供的用戶模式的緩沖區復制到內核模式地址下,這個地
址由WriteFile創建的IRP的AssociateIrp.SystemBuffer子域記錄。

另外,在派遣函數中也可以通過IO_STACK_LOCATION中的Parameters.Read.Length子域知道ReadFile請求
多少字節。通過IO_STACK_LOCATION中的Parameters.Write.Length子域知道WriteFile請求多少字節。

然後,WriteFile和ReadFile指定對設備操作多少字節,並不真正意味著操作了這麼多字節。在派遣函數
中,應該設置IRP的子域IoStatus.Information.這個子域記錄設備實際操作了多少字節。

下面代碼演示了如何利用“緩沖區”方式讀設備:

[plain] view plaincopy
NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
{
KdPrint(("Enter HelloDDKRead\n"));

//對一般IRP進行處理,後面會介紹對IRP更復雜的處理
NTSTATUS status = STATUS_SUCCESS;

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
//獲得需要讀設備的字節數
ULONG ulReadLength = stack->Parameters.Read.Length;

//完成IRP
//設置IRP完成狀態
pIrp->IoStatus.Status = status;

//設置IRP操作了多少字節
pIrp->IoStatus.Information = ulReadLength;

memset(pIrp->AssociatedIrp.SystemBuffer,0XAA,ulReadLength);

//處理IRP
IoCompleteRequest(pIrp,IO_NO_INCREMENT);
KdPrint(("Leave HelloDDKRead\n"));

return status;

}

ring3下的程序來讀取數據:
[plain] view plaincopy
#include <windows.h>
#include <stdio.h>

int main()
{
HANDLE hDevice =
CreateFile("\\\\.\\HelloDDK",
GENERIC_READ | GENERIC_WRITE,
0, // share mode none
NULL, // no security
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL ); // no template

if (hDevice == INVALID_HANDLE_VALUE)
{
printf("Failed to obtain file handle to device: "
"%s with Win32 error code: %d\n",
"MyWDMDevice", GetLastError() );
return 1;
}

UCHAR buffer[10];
ULONG ulRead;
BOOL bRet = ReadFile(hDevice,buffer,10,&ulRead,NULL);
if (bRet)
{
printf("Read %d bytes:",ulRead);
for (int i=0;i<(int)ulRead;i++)
{
printf("%02X ",buffer[i]);
}

printf("\n");
}

CloseHandle(hDevice);
return 0;
}

直接讀寫方式:
1.直接讀取設備:

除了“緩沖區”方式讀寫設備外,另一種方式是直接方式讀寫設備。這種方式需要在創建完設備對象後
,在設置設備屬性的時候,設置為DO_DIRECT_IO。
和緩沖區讀寫方式不同,直接讀寫設備,操作系統會將用戶模式下的緩沖區鎖住。然後操作系統將這段
緩沖區在內核模式地址再次映射一遍。這樣,用戶模式的緩沖區和內核模式的緩沖區指向的是同一區域
的物理內存。無論操作系統如何切換進程,內核模式地址都保持不變。

操作系統先將用戶模式的地址鎖住後,操作系統用內存描述符(MDL數據結構)記錄這段內存。

MDL記錄這段虛擬內存,這段虛擬內存的大小存儲在mdl->ByteCount裡,這段虛擬內存的第一個頁地址是
mdl->StartVa,這段虛擬內存的首地址對於第一個頁地址的偏移量是mdl->ByteOffset,。因此,這段虛
擬內存的首地址應該是 mdl->StartVa + mdl->ByteOffset。

DDK提供裡幾個宏方便程序員得到這幾個數值:

[plain] view plaincopy
#define MmGetMdlByteCount(mdl) ((Mdl)->ByteCount)

#define MmGetMdlByteOffset(mdl) ((Mdl)->ByteOffset)

#define MmGetMdlVirtualAddress(mdl) ((PVOID)((PCHAR)((Mdl->StartVa) + (Mdl)->ByteOffset))

2.直接讀取設備的讀寫
下面結合代碼演示如何編寫直接方式設備的派遣函數

[plain] view plaincopy
NTSTATUS HelloDDKRead(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)
{
//MDL的長度應該和讀長度相等,否則該操作應該設為不成功
pIrp->IoStatus.Information = 0;
status = STATUS_UNSUCCESSFUL;
}else
{
//用MmGetSystemAddressForMdlSafe得到MDL在內核模式下的映射
PVOID kernel_address = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePri
ority);
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 HelloDDKRead\n"));

return status;
}


其他方式的讀寫操作:

這裡暫時不討論此種方法。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved