派遣函數是Windows驅動程序中的重要概念。驅動程序的主要功能是負責處理I/O請求,其中大部分I/O請
求是在派遣函數中處理的。程序員
用戶模式下全部對驅動程序的I/O請求,所有由操做系統轉換爲一個叫作IRP數據結構,不一樣的IRP會被「
派遣」到不一樣的派遣函數中。windows
IRP與派遣函數數組
IRP的處理機制相似於Windows應用程序中的「消息處理」,驅動程序接收到不一樣的IRP後,會進入不一樣的
派遣函數,在派遣函數中IRP獲得處理。數據結構
1.IRP函數
在Windows內核中,有一種數據結構叫作IRP(I/O Request Package),即輸入輸出請求包。上層應用程
序與底層驅動程序通訊時,應用程序會發出I/O請求。操做系統將I/O請求轉化爲相應的IRP數據,不一樣類
型的IRP會被傳遞到不一樣的派遣函數中。spa
IRP有兩個基本的重要屬性,一個是MajorFunction,另外一個MinorFunction,分別記錄IRP的主類型和子
類型,操做系統根據MajorFunction將IRP「派遣」到不一樣的派遣函數中,在派遣函數中還能夠繼續判斷
這個IRP屬於哪一種MinorFunction。操作系統
下面是HelloDDK的DriverEntry中關於派遣函數的註冊:code
[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;
} orm
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;
}
其餘方式的讀寫操做:
這裏暫時不討論此種方法。