IRP IO_STACK_LOCATION 《寒江獨釣》核心學習筆記(1)

Andrew.Hann發表於2013-11-30

在學習核心過濾驅動的過程中,遇到了大量的涉及IRP操作的程式碼,這裡有必要對IRP的資料結構和與之相關的API函式做一下筆記。

 

1. 相關閱讀資料

《深入解析 windows 作業系統(第4版,中文版)》 --- 9章

《windows driver kit 幫助文件》

http://support.microsoft.com/kb/115758/zh-cn  IRP 結構中各地址欄位的含義

http://www.programlife.net/io_stack_location-irp.html    程式碼瘋子對IRP的研究

 

 

 

 

2. IRP的資料結構

IRP是一個資料結構,其中包含了用來描述一個IO請求的完整資訊。

IO管理器建立一個IRP來代表一個IO操作,並且將該IRP傳遞給正確的驅動程式,當此IO操作完成時再處理該請求包。相對的,驅動程式(上層的虛擬裝置驅動或者底層的真實裝置驅動)接收一個IRP,執行該IRP指定的操作,然後將IRP傳回給IO管理器,告訴它,該操作已經完成,或者應該傳給另一個驅動以進行進一步處理。

 

談到IRP,IRP是個總的概念,本質上IRP由IRP Header和IRP Sub-Request組成

從資料結構的角度上來說,其實資料結構 IRP 只是"I/O 請求包"IRP的頭部,在 IRP 資料結構的後面還有一個IO_STACK_LOCATION 資料結構的陣列,陣列的大小則取決於 IRP 資料結構中的StackCount(我們之後會詳細分析),其數值來自裝置堆疊中頂層裝置物件的 StackSize 欄位。

這樣,就在 IRP 中為目標裝置物件裝置堆疊中的每一層即每個模組(每個驅動)都準備好了一個 IO_STACK_LOCATION 資料結構。而CurrentLocation,則是用於該陣列的下標,說明目前是在堆疊中的哪一層,因而正在使用哪一個 IO_STACK_LOCATION 資料結構。

 

那這兩個結構是怎麼來的呢?有兩種渠道:

1. 程式設計師在程式碼中手工地建立一個IRP(廣義的IRP)

PIRP 
  IoAllocateIrp(
    IN CCHAR  StackSize,
    IN BOOLEAN  ChargeQuota
    );

任何核心模式程式在建立一個IRP時,同時還建立了一個與之關聯的 IO_STACK_LOCATION 結構陣列:陣列中的每個堆疊單元都對應一個將處理該IRP的驅動程式,堆疊單元中包含該IRP的型別程式碼和引數資訊以及完成函式的地址。

2. I/O管理器在接收到應用層的裝置讀寫請求後,將請求封裝為一個IRP請求(包括IRP頭部和IRP STACK_LOCATINO陣列)發往對應的裝置的裝置棧的最頂層的那個裝置驅動。

 

我們先從IRP的頭結構開始學起:

下面是WDK上搬下來的解釋,我們來一條一條的學習。

IRP

typedef struct _IRP 
{
  .
  .
  PMDL  MdlAddress;
  ULONG  Flags;
  union 
  {
    struct _IRP  *MasterIrp;
    .
    .
    PVOID  SystemBuffer;
  } AssociatedIrp;
  .
  .
  IO_STATUS_BLOCK  IoStatus;
  KPROCESSOR_MODE  RequestorMode;
  BOOLEAN PendingReturned;
  .
  .
  BOOLEAN  Cancel;
  KIRQL  CancelIrql;
  .
  .
  PDRIVER_CANCEL  CancelRoutine;
  PVOID UserBuffer;
  union 
  {
    struct 
    {
    .
    .
    union 
    {
      KDEVICE_QUEUE_ENTRY DeviceQueueEntry;
      struct 
      {
        PVOID  DriverContext[4];
      };
    };
    .
    .
    PETHREAD  Thread;
    .
    .
    LIST_ENTRY  ListEntry;
    .
    .
    } Overlay;
  .
  .
  } Tail;
} IRP, *PIRP;

MSDN 說IRP是一個半透明結構,開發者只能訪問其中透明的部分,所以其中的..代表是我們不能訪問的部分,所以我們在程式設計中基本上也不會用到它們,我們集中精力來觀察這些暴露出來的資料結構。

 

 

 

1. IRP中的三種緩衝區

PMDL  MdlAddress;
union {
    struct _IRP  *MasterIrp;
    .
    .
    PVOID  SystemBuffer;
  } AssociatedIrp;
PVOID UserBuffer;

IRP這個資料結構中有3個地方可以描述緩衝區。 

1) irp->AssociatedIrp.SystemBuffer
2) irp->MdlAddress
3) irp->UserBuffer    

不同的IO類別,IRP的緩衝區不同。
1) AssociatedIrp.SystemBuffer
一般用於比較簡單且不追求效率情況下的解決方案 把R3層中的記憶體中的緩衝資料拷貝到核心空間中。注意,是直接拷貝過來,有的書上會說這是"直接方式",不過我們要重點記住的是它是直接拷貝過來。

2) MdlAddress
通過構造MDL就能實現這個R3到R0的地址對映功能。MDL可以翻譯為"記憶體描述符鏈",本質上就是一個指標,從這個MDL中可以讀出一個核心空間的虛擬地址。這就彌補了UserBuffer的不足,同時比SystemBuffer的完全拷貝方法要輕量,因為這個記憶體實際上還是在老地方,沒有拷貝。

3) UserBuffer
最追求效率的解決方案 R3的緩衝區地址直接放在UserBuffer裡,在核心空間中直接訪問。在當前程式和傳送程式一致的情況下,核心訪問應用層的記憶體空間當然是沒錯的。但是一 旦核心程式已經切換,這個訪問就結束了,訪問UserBuffer當然是跳到其他程式空間去了(我們還是訪問同一個地址,但是這個時候因為程式的上下文切換了,同一個地址對應的內容自然不同了)。因為在windows中,核心空間是所有程式共享的,而應 用層空間則是各個程式隔離的。當然還有一個更簡單的做法是把應用層的地址空間對映到核心空間,這需要在頁表中增加一個對映。

在驅動在獲取這三種緩衝區的方法。

//獲得緩衝區
PUCHAR buf = NULL;
if(irp->MdlAddress != NULL)
{
    buffer = (PUCHAR)MmGetSystemAddressForMdlSafe(irp->MdlAddress, NormalPagePriority);
}
else
{
    buffer = (PUCHAR)irp->UserBuffer;
}
if(buffer = NULL)
{
    buffer = (PUCHAR)irp->AssociatedIrp.SystemBuffer;
}

看到這裡,就產生另一個問題了,這三種緩衝區是作業系統幫我們自動填好的嗎?那在什麼樣的IRP請求會使用到不同的緩衝區型別呢?

這裡就要涉及到兩種記憶體訪問方式: 直接方式DO_DIRECT_IO / 非直接方式(緩衝方式)DO_BUFFERD_IO

1) 在buffered(AssociatedIrp.SystemBuffer)方式中,I/O管理器先建立一個與使用者模式資料緩衝區大小相等的系統緩衝區。而你的驅動程式將使用這個系統緩衝區工作。I/O管理器負責在系統緩衝區和使用者模式緩衝區之間複製資料。
2) 在direct(MdlAddress)方式中,I/O管理器鎖定了包含使用者模式緩衝區的實體記憶體頁,並建立一個稱為MDL(記憶體描述符表)的輔助資料結構來描述鎖定頁。因此你的驅動程式將使用MDL工作。
3) 在neither(UserBuffer)方式中,I/O管理器僅簡單地把使用者模式的虛擬地址傳遞給你。而使用使用者模式地址的驅動程式應十分小心。

 

繼續思考我們之前的問題,在IRP中具體是使用哪種緩衝方式呢?由誰來決定?

答案是在增加裝置的時候就決定的了。即我們在新增一個裝置的時候就要決定這個裝置的緩衝區讀寫方式。

DRIVER_ADD_DEVICE AddDevice;

NTSTATUS
  AddDevice(
    __in struct _DRIVER_OBJECT  *DriverObject,
    __in struct _DEVICE_OBJECT  *PhysicalDeviceObject 
    )
  {...}

NTSTATUS 
  IoCreateDevice(
    IN PDRIVER_OBJECT  DriverObject,
    IN ULONG  DeviceExtensionSize,
    IN PUNICODE_STRING  DeviceName  OPTIONAL,
    IN DEVICE_TYPE  DeviceType,
    IN ULONG  DeviceCharacteristics,
    IN BOOLEAN  Exclusive,
    OUT PDEVICE_OBJECT  *DeviceObject
    );

typedef struct _DEVICE_OBJECT 
{
  CSHORT  Type;
  USHORT  Size;
  LONG  ReferenceCount;
  PDRIVER_OBJECT  DriverObject;
  PDEVICE_OBJECT  NextDevice;
  PDEVICE_OBJECT  AttachedDevice;
  PIRP  CurrentIrp;
  PIO_TIMER  Timer;
  ULONG  Flags;//notice
  ULONG  Characteristics;
  __volatile PVPB  Vpb;
  PVOID  DeviceExtension;
  DEVICE_TYPE  DeviceType;
  CCHAR  StackSize;
  union 
  {
    LIST_ENTRY  ListEntry;
    WAIT_CONTEXT_BLOCK  Wcb;
  } Queue;
  ULONG  AlignmentRequirement;
  KDEVICE_QUEUE  DeviceQueue;
  KDPC  Dpc;
  ULONG  ActiveThreadCount;
  PSECURITY_DESCRIPTOR  SecurityDescriptor;
  KEVENT  DeviceLock;
  USHORT  SectorSize;
  USHORT  Spare1;
  PDEVOBJ_EXTENSION  DeviceObjectExtension;
  PVOID  Reserved;
} DEVICE_OBJECT, *PDEVICE_OBJECT;
 

NTSTATUS AddDevice(DriverObject, PhysicalDeviceObject)
{ 
    PDEVICE_OBJECT fdo; 
    IoCreateDevice(..., &fdo); 
    fdo->Flags |= DO_BUFFERED_IO; 
    //或者是下面的程式碼,這三句任取其一
    fdo->Flags |= DO_DIRECT_IO; 
    //<or> 
    fdo->Flags |= 0; 
}

總結一下,在新增裝置的時候這個物理裝置的資料緩衝方式就被決定了。這之後你不能該變緩衝方式的設定,因為過濾器驅動程式將複製這個標誌設定,並且,如果你改變了設定,過濾器驅動程式沒有辦法知道這個改變。

接下來解決目前為止的最後一個疑問: 系統是如何來根據裝置物件中的緩衝型別碼來進行不同緩衝區型別的的緩衝區資料的實際填充過程的(即實際的緩衝區填充過程是什麼)?

1) Buffered方式(有一個複製過程)
當I/O管理器建立IRP_MJ_READ或IRP_MJ_WRITE請求時(讀寫請求中會用到資料緩衝區,普通的檔案屬性查詢請求或設定中最多用到一個指定的資料結構大小的記憶體空間即可),它探測裝置的緩衝標誌(在建立裝置時就決定的了)以決定如何描述新IRP中的資料緩衝區。如果DO_BUFFERED_IO(UserBuffer)標誌設定,I/O管理器將分配與使用者緩衝區大小相同的"非分頁"記憶體(不會產生缺頁中斷的記憶體空間)。
注意,它把緩衝區的地址和長度儲存到兩個十分不同的地方,下面是一段模擬程式碼。你可以假定I/O管理器執行下面程式碼(注意這並不是Windows NT的原始碼):

PVOID uva;             //  user-mode virtual buffer address
ULONG length;          //  length of user-mode buffer

PVOID sva = ExAllocatePoolWithQuota(NonPagedPoolCacheAligned, length);
if (writing)
{
    RtlCopyMemory(sva, uva, length);
} 
Irp->AssociatedIrp.SystemBuffer = sva; 

PIO_STACK_LOCATION stack = IoGetNextIrpStackLocation(Irp);
if (reading)
{
    stack->Parameters.Read.Length = length; 
} 
else
{
    stack->Parameters.Write.Length = length; 
} 
<code to send and await IRP>
if (reading)
{
    RtlCopyMemory(uva, sva, length);
} 
ExFreePool(sva);

可以看出,系統緩衝區地址被放在IRP的AssociatedIrp.SystemBuffer域中,而資料的長度被放到stack->Parameters聯合中。I/O管理器把使用者模式虛擬地址(uva變數)儲存到IRP的UserBuffer域中,這樣一來核心驅動程式碼就可以找到這個地址。

 

2) Direct方式
如果你在裝置物件中指定DO_DIRECT_IO(MDL)方式,I/O管理器將建立一個MDL用來描述包含該使用者模式資料緩衝區的鎖定記憶體頁(本質上還是一種對映,建立對映到同一記憶體位置的核心模式虛擬地址)。MDL結構的宣告如下:

typedef struct _MDL 
{
  struct _MDL *Next;
  CSHORT Size;
  CSHORT MdlFlags;
  struct _EPROCESS *Process;
  PVOID MappedSystemVa;
  PVOID StartVa;
  ULONG ByteCount;
  ULONG ByteOffset;
} MDL, *PMDL;

 

 StartVa成員給出了使用者緩衝區的虛擬地址,這個地址僅在擁有資料緩衝區的使用者模式程式上下文中才有效(即使用者模式中的虛擬地址空間)。ByteOffset是緩衝區起始位置在一個頁幀中的偏移值,ByteCount是緩衝區的位元組長度。Pages陣列沒有被正式地宣告為MDL結構的一部分,在記憶體中它跟在MDL的後面,包含使用者模式虛擬地址對映為物理頁幀的個數。

要注意的是,我們不可以直接訪問MDL的任何成員。應該使用巨集或訪問函式:

巨集或函式    描述
IoAllocateMdl    建立MDL(在檔案系統驅動的透明加密中你可能需要建立自己的臨時MDL以暫時替換系統的原始的MDL)

IoBuildPartialMdl    建立一個已存在MDL的子MDL

IoFreeMdl    銷燬MDL

MmBuildMdlForNonPagedPool    修改MDL以描述核心模式中一個非分頁記憶體區域

MmGetMdlByteCount    取緩衝區位元組大小

MmGetMdlByteOffset    取緩衝區在第一個記憶體頁中的偏移

MmGetMdlVirtualAddress    取虛擬地址

MmGetSystemAddressForMdl    建立對映到同一記憶體位置的核心模式虛擬地址

MmGetSystemAddressForMdlSafe    與MmGetSystemAddressForMdl相同,但Windows 2000首選

MmInitializeMdl    (再)初始化MDL以描述一個給定的虛擬緩衝區

MmPrepareMdlForReuse    再初始化MDL

MmProbeAndLockPages    地址有效性校驗後鎖定記憶體頁

MmSizeOfMdl    取為描述一個給定的虛擬緩衝區的MDL所佔用的記憶體大小

MmUnlockPages    為該MDL解鎖記憶體頁

對於I/O管理器執行的Direct方式的讀寫操作,其過程可以想象為下面程式碼:

KPROCESSOR_MODE mode;   //  either KernelMode or UserMode
PMDL mdl = IoAllocateMdl(uva, length, FALSE, TRUE, Irp);
MmProbeAndLockPages(mdl, mode, reading ? IoWriteAccess : IoReadAccess); 

<code to send and await IRP>

MmUnlockPages(mdl);
ExFreePool(mdl);

I/O管理器首先建立一個描述使用者緩衝區的MDL。IoAllocateMdl的第三個引數(FALSE)指出這是一個主資料緩衝區。第四個引數(TRUE)指出記憶體管理器應把該記憶體充入程式配額。最後一個引數(Irp)指定該MDL應附著的IRP。在內部,IoAllocateMdl把Irp->MdlAddress設定為新建立MDL的地址,以後你將用到這個成員,並且I/O管理器最後也使用該成員來清除MDL。

這段程式碼的關鍵地方是呼叫MmProbeAndLockPages。該函式校驗那個資料緩衝區是否有效,是否可以按適當模式訪問。如果我們向裝置寫資料,我們必須能讀緩衝區。如果我們從裝置讀資料,我們必須能寫緩衝區。另外,該函式鎖定了包含資料緩衝區的實體記憶體頁,並在MDL的後面填寫了頁號陣列。在效果上,一個鎖定的記憶體頁將成為非分頁記憶體池的一部分,直到所有對該頁記憶體加鎖的呼叫者都對其解了鎖。

在Direct方式的讀寫操作中,對MDL你最可能做的事是把它作為引數傳遞給其它函式。例如,DMA傳輸的MapTransfer步驟需要一個MDL。另外,在內部,USB讀寫操作總使用MDL。所以你應該把讀寫操作設定為DO_DIRECT_IO方式,並把結果MDL傳遞給USB匯流排驅動程式。

順便提一下,I/O管理器確實在stack->Parameters聯合中儲存了讀寫請求的長度,但驅動程式應該直接從MDL中獲得請求資料的長度

ULONG length = MmGetMdlByteCount(mdl);

 

 

3) Neither方式
如果你在裝置物件中同時忽略了DO_DIRECT_IO和DO_BUFFERED_IO標誌設定,你將得到預設的neither方式。對於這種方式,I/O管理器將簡單地把使用者模式虛擬地址和位元組計數直接交給你,其餘的工作由你去做。這種情況下程式設計師將自己去解決因為程式的切換導致的使用者模式地址失效問題。

Irp->UserBuffer = uva; 
PIO_STACK_LOCATION stack = IoGetNextIrpStackLocation(Irp);
if (reading)
  stack->Parameters.Read.Length = length; 
else
  stack->Parameters.Write.Length = length; 

<code to send and await IRP>

至此,我們目前的疑問就全部解決了,我們知道了IRP中的三種不同的緩衝區是怎麼來的(裝置新增的時候決定的),是由誰填充的(作業系統自動地把使用者模式地址空間的資料填充到IRP中的緩衝區中/或者直接給出使用者空間地址)。

接下來就可以引出IRP中(準備說是IRP頭部的另一個成員域)

 

 

 

 

2. ULONG  Flags

File system drivers use this field, which is read-only for all drivers. 
Network and, possibly, highest-level device drivers also might read this field, 
which can be set with one or more of the following system-defined masks:

在檔案系統驅動程式的程式設計中將使用到這個資料域,這對所有的驅動程式來說是隻讀的,它指示了這個IRP的操作型別。

IRP_NOCACHE
IRP_PAGING_IO
IRP_MOUNT_COMPLETION
IRP_SYNCHRONOUS_API
IRP_ASSOCIATED_IRP
IRP_BUFFERED_IO
IRP_DEALLOCATE_BUFFER
IRP_INPUT_OPERATION
IRP_SYNCHRONOUS_PAGING_IO
IRP_CREATE_OPERATION
IRP_READ_OPERATION
IRP_WRITE_OPERATION
IRP_CLOSE_OPERATION
IRP_DEFER_IO_COMPLETION

關於這個Flags欄位,我們常常要注意的是,我們如果在做過濾/繫結/捕獲型別的驅動型別的程式設計中,我們新建立的上層過濾裝置的Flags欄位的值一定要和下層的真實裝置或底層驅動的Flags保持一致。

比如我在做檔案系統卷的過濾裝置程式設計的時候就遇到這樣的程式碼:

//裝置標誌的複製
if (FlagOn( DeviceObject->Flags, DO_BUFFERED_IO )) 
{
    SetFlag( SFilterDeviceObject->Flags, DO_BUFFERED_IO );
}
if (FlagOn( DeviceObject->Flags, DO_DIRECT_IO ))
{
    SetFlag( SFilterDeviceObject->Flags, DO_DIRECT_IO );
} 

 

 

 

 

 

3.  IO_STATUS_BLOCK  IoStatus

typedef struct _IO_STATUS_BLOCK 
{
  union 
  {
    NTSTATUS  Status;
    PVOID  Pointer;
  };
  ULONG_PTR  Information;
} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;

IoStatus(IO_STATUS_BLOCK)是一個僅包含兩個域的結構,驅動程式在最終完成請求時設定這個結構。
IoStatus.Status 表示IRP完成狀態
ntdef.h
ntstatus.h
WDK的這兩個標頭檔案中定了所有的系統級的返回資訊,在驅動中,函式的返回值大多數情況下就是這樣結果狀態資訊,當然也有通過引用的方式獲得函式執行的結果的,但是函式還是返回個執行結果。

IoStatus.information的值與請求相關,如果是資料傳輸請求,則將該域設定為傳輸的位元組數(在windows程式設計中,基本上涉及到資料讀寫的函式一般都是返回這一類的結果,即操作的位元組數)。

 

 

 

 

4. KPROCESSOR_MODE  RequestorMode

RequestorMode將等於一個列舉常量UserMode或KernelMode, 指定原始I/O請求的來源。驅動程式有時需要檢視這個值來決定是否要信任某些引數。

 

 

 

5. BOOLEAN PendingReturned

PendingReturned(BOOLEAN)如果為TRUE,則表明處理該 IRP的最低階派遣例程返回了STATUS_PENDING。完成例程通過參考該域來避免自己與派遣例程間的潛在競爭。

If set to TRUE, a driver has marked the IRP pending. 
Each IoCompletion routine should check the value of this flag. 
If the flag is TRUE, and if the IoCompletion routine will not return STATUS_MORE_PROCESSING_REQUIRED, 
the routine should call IoMarkIrpPending to propagate the pending status to drivers above it in the device stack. 

這段話是什麼意思呢?這和核心驅動中的多層裝置棧有關係。為了解釋這個問題,我們先來看一段程式碼demo:

....
Kevent event;
KeInitializeEvent(&event, NotificatinoEvent, FALSE);
IoCopyCurrentIrpStackLocationToNext(Irp);
//設定完成回撥函式
IoSetCompletionRoutine(
    Irp,
    IrpComplete, //回撥函式
    &event,
    TRUE,
    TRUE,
    TRUE
);
status = IoCallDriver(DeviceObject, Irp);
if(status == STATUS_PENDING)
{
    //code to handle asynchronous response
    //非同步 
  status = KeWaitForSingleObject( &waitEvent,
                                            Executive,
                                            KernelMode,
                                            FALSE,
                                            NULL );

}
...


//這是一個IRP完成回撥函式的原型
NTSTATUS IrpComplete(
    IN PDEVICE_OBJECT DeviceObject,
    IN PIRP Irp,
    IN PVOID Context
)
{
    ...
    if(Irp->PendingReturned)
    {
        //這個函式等價於: Irp->IoStatus.Status = STATUS_PENDING 即表名這個IRP處理流程依舊沒有結束
        IoMarkIrpPending(Irp);
    }
    return Irp->IoStatus.Status;
}

請原諒我沒頭沒腦的給這出這段看起來不知所云的程式碼。這是因為IRP機制是一種基礎機制,往往是配合一些具體的過濾驅動的程式設計而使用的,如果要給出完整的例程那這篇文章的篇幅就會無窮無盡了。

所以接下來我盡我最大的能力來解釋這段程式碼的意思並給出它的利用場景。

IRP作為一個執行緒無關的呼叫棧
進行一個裝置的I/O操作通常需要呼叫這一裝置相關的不止一個驅動。每一個和這個裝置相關的驅動都會建立一個裝置物件(Device Object),並且這些裝置物件會垂直壓入(排列進)一個裝置棧(Device Stack)。IRP會在裝置棧中從上到下的一個個被傳遞進去(從頂層的裝置驅動一直往下到底層的真實裝置)。對於棧中的每一個驅動,IRP都會用一個指標標識一個棧位置(IO_STACK_LOCATION和裝置棧上的裝置驅動的一一對應關係用這個指標來繫結)。由於驅動可以非同步地處理請求,因此IRP就像是一個執行緒無關的呼叫棧一樣

仔細看這張圖,每一級驅動程式都使用下一級驅動程式的堆疊單元儲存自己完成例程指標。最底層的驅動程式不應該安裝一個完成例程,它應該總是返回一個真實的硬體操作結果。即我們在當前位置的裝置驅動中設定一個下層驅動的完成回撥函式時,本質上是在下層驅動的棧空間中佈置一個了一個回撥函式地址。


將IRP傳遞到下一級驅動程式(又被稱作轉發IRP)是指IRP等價於一個子例程呼叫。當驅動轉發一個IRP,這個驅動程式必須向IRP引數組增加下一個I/O棧位置,告知這一IRP棧的指標,然後呼叫下一驅動的分發例程(dispatch routine)。基本來說,就是驅動向下呼叫IRP棧(calling down the IRP stack)

傳遞一個IRP,驅動通常會採取以下幾種步驟
1) . 建立下一個I/O棧位置的引數。
1.1) 呼叫IoGetNextIrpStackLocation例程來得到一個指標指向下一個I/O棧位置,然後將請求引數陣列複製到那個得到的位置
1.2) 呼叫CopyCurrentIrpStackLocationToNext例程(如果驅動設定了IoCompletion例程)
或者
1.3) 呼叫IoSkipCurrentIrpStackLocation例程(沒有設定IoCompletion例程)來傳遞當前位置所使用的同樣的引數組。

2). 如果需要的話,呼叫IoSetCompletionRoutine例程,為後期處理(post-processing)設定一個IoCompletion例程。如果驅動設定了IoCompletion例程,那麼他在上一步中必須使用IoCopyCurrentIrpStackLocationToNext。

3). 通過呼叫IoCallDriver例程將請求傳遞到下一個驅動。這個例程會自動通告IRP棧指標,並且呼叫下一個驅動的分發例程。

在驅動程式將IRP傳遞給下一個驅動之後,就不再擁有這個IRP,並且不能檢視再去訪問這個IRP,否則會導致系統崩潰。那個IRP會被其他的驅動或者執行緒釋放或這直接完成。

理解這句話非常重要,這個IRP中的核心思想,也就是說,一旦你呼叫了IoCallDriver()把IRP傳遞給了下層的驅動,這個IRP就和你沒關係了。

如果驅動需要訪問一個已經在棧裡傳下去的IRP,那麼這個驅動必須實現(設定)IoCompletion例程。當I/O管理器(I/O Manager)呼叫IoCompletion例程時(當下層完成處理後,自動呼叫了回撥函式),這個驅動(之前把IRP下發的那個上層驅動)就能夠在IoCompletion例程執行期間重新獲得對這一IRP的所有權。如此,IoCompletion例程就能夠訪問IRP中的域。

設定非同步完成回撥函式的方法上面的程式碼已給出,這是一個經典的模型,即建立一個事件物件->初始化這個事件物件->上層對這個事件進行阻塞等 待->將IRP下發給下層驅動->下層驅動完成處理邏輯後設定設定這個事件(即觸發一個完成訊號,解除這個事件的互斥)->上層驅動獲 得這個事件的釋放->上層驅動繼續程式碼邏輯,並根據下層驅動的返回結構來做進一步的操作。

若是驅動的分發例程(上層驅動)也必須在IRP被後面的驅動(下層驅動)處理完成之後再處理它,這個IoCompletion例程(上層驅動設定的完成回撥函式)必須返回STATUS_MORE_PROCESSING_REQUIRED,以將IRP的所有權返回給分發例程(上層驅動)。如此依賴,I/O管理器會停止IRP的處理(這指回卷處理,之後會解釋),將最終完成IRP的任務(呼叫IoCompleteRequest來完成這個IRP)留給分發例程。分發例程能夠在之後呼叫IoCompleteRequest來完成這個IRP,或者還能將這個IRP標記為等待進一步處理,繼續回傳給它之上的驅動。


當輸入、輸出操作(I/O)完成時,完成這個I/O操作的驅動會呼叫IoCompleteRequest例程,這個例程將IRP棧指標移到指向IRP棧的前一個(更上面)的位置。
如果一個驅動在裝置棧中向下傳遞IRP時設定了IoCompletion例程,I/O管理器就會在IRP棧指標再次指向這一驅動的這個I/O棧位置的時候呼叫此例程,IoCompletion例程就表現為: 當IRP在裝置棧中傳遞時,操作IRP的那些驅動的返回地址。

當每一個驅動都完成了它對應的子請求,I/O請求就完成了。I/O管理器從Irp->IoStatus.Status域取回請求的狀態資訊,並且從Irp->IoStatus.Information域取回傳輸的位元組數。

這我們要理清一下思路: 我們知道,IRP在裝置棧中逐級向下傳遞,並根據情況可能逐級的設定回撥函式,
一直到最底層的真實裝置完成了請求。
之後,系統會有一個"回捲"操作,類似我們在C程式設計中的函式棧的呼叫迭代的模型。
逐級的"回捲"過程就是不斷呼叫上層驅動設定的回撥函式的過程。
也就是不斷觸發IoCompletion函式的過程,上層驅動通過IoCompletion來獲得對之前下發的IRP的重新控制權。

一個IoCompletion例程能夠返回兩個狀態值中的任一個:
1. STATUS_PENDING--繼續向上完成特定IRP。I/O管理器提升IRP棧指標,並且呼叫上一個驅動的IoCompletion例程(繼續回捲)
2. STATUS_MORE_PROCESSING_REQUIRED--中斷向上完成特定IRP的過程,並且把IRP棧指標留在當前位置(暫時中斷回捲)。
返回這一狀態的驅動通常會在過後呼叫IoCompleteRequest例程來重新開始向上完成特定的IRP的過程。
完成IRP時是忽略還是拷貝當前棧空間(IO_STACK_LOCATION),返回什麼狀態
值,以及完成函式中如何結束IRP,我們這裡做個總結。
1) 如果對IRP完成之後的事情無興趣,則直接忽略當前IO_STACK_LOCATION(即
呼叫核心API函式IoSkipCurrentIrpStackLocation),然後向下傳遞請求,返回IoCallDriver所返回的狀態。

2) 不但對IRP完成之後的事情無興趣,而且不打算立刻返回成功或失敗(即擋在當前這
一層的驅動這裡,不再往下傳遞了)。那麼不用忽略或者拷貝當前IO_STACK_LOCATION,填寫IRP的狀態引數後呼叫IoCompleteRequest,
並返回自己想返回的結果。

3) 如果對IRP完成之後的事情有興趣,並打算在完成函式中處理,則應該首先拷貝當
前IO_STACK_LOCATION(IoCopyCurrentIrpStackLocationToNext),然後指定完成函式,並返回IoCallDriver()所返回的狀態。在完成函式中,不需要呼叫
IoCompleteRequest,直接返回IRP的當前狀態即可

4) 如果對IRP完成之後的事情有興趣,並打算在完成函式中處理,有時候會把任務塞
進系統工作執行緒或者希望在另外的執行緒中去完成IRP,那麼完成函式中應該返回STATUS_MORE_PROCESSING_REQUIRED,此時完成IRP時應該呼叫
IoCompleteRequest。另一種類似的情況是在分發函式中等待完成函式中設定事件,那麼完成函式返回STATUS_MORE_PROCESSING_REQUIRED,分發函式
在等待結束後呼叫IoCompleteRequest。

(這段話是我們自己根據MSDN和《寒江獨釣》的研究後總結的,說心理話,不敢保證100%正確,這塊內容確實很複雜,如果看到這篇文章的牛牛知道真實的詳細細節的話,希望不吝賜教,分享一些好的思路)

至此,我們知道了PendingReturned是用來判斷下層的驅動返回的處理狀態的。那它的利用場景是什麼呢?這通常見於一些檔案系統驅動過濾的應用中: 如磁碟透明加密, NTFS透明加密的程式設計中,我們的上層過濾驅動要先捕獲到這個IRP_MJ_READ請求,然後下放這個IRP,讓它去呼叫磁碟驅動讀取資料,然後在完成回撥函式中對剛才讀取到的資料進行加解密,這是一個典型的應用。

 

 

 

 

6. IRP操作取消機制

BOOLEAN  Cancel;
KIRQL  CancelIrql;
.
.
PDRIVER_CANCEL  CancelRoutine;

這三個欄位同屬於IRP取消機制的範湊,我們一併研究。

Cancel(BOOLEAN)如果為TRUE,則表明IoCancelIrp已被呼叫,該函式用於取消這個請求。如果為FALSE,則表明沒有呼叫IoCancelIrp函式。
CancelIrql(KIRQL)是一個IRQL值,表明那個專用的取消自旋鎖是在 這個IRQL上獲取的。當你在取消例程中釋放自旋鎖時應參考這個域。
CancelRoutine(PDRIVER_CANCEL)是驅動程式取消例程的地 址。你應該使用IoSetCancelRoutine函式設定這個域而不是直接修改該域

PDRIVER_CANCEL IoSetCancelRoutine(
    IN PIRP  Irp,
    IN PDRIVER_CANCEL  CancelRoutine
    ); 

VOID IoAcquireCancelSpinLock(
    OUT PKIRQL  Irql
    );

IRP請求的最終結局無非有兩個:要麼被完成了,要麼被取消了。完成IRP請求的過程已經在前面講過了,這裡仔細講一個IRP請求的取消。

為什麼要取消IRP請求呢?一般來講,原因不外乎是本請求操作超時或裝置故障導致的。具體理解,可以考慮如下兩種情形:

情形1:驅動傳送一個請求到下級驅動,下級驅動由於忙,將它放到自己的請求隊中去,下級驅動一直忙,請求一直沒有得到處理,而這個請求又比較重要,如果一直得不到處理就會造成系統處於死鎖。於是,驅動就會給這個請求加上超時機制,若超過一定的時間還沒有得到處理結果,就通知下級驅動直接取消該請求。

情形1:驅動傳送很多請求到下級驅動去處理,下級驅動返回了一個請求的結果。可是,這個結果是個錯誤,而且是個很嚴重的錯誤,比如裝置出故障了。這時,就要將裝置進行錯誤恢復,如重啟裝置,同時,其它送下去的請求都要同時取消掉。

驅動如何被取消? 一般來講,取消的發起者一定是上層的驅動,而取消的實際執行者,則是下層的驅動。

1. 上層的驅動通過呼叫 IoCancelIrp(Irp)來取消某個請求。這個請求應該是被某個下層驅動放在緩衝佇列中等待處理或正在處理。

2. 下層驅動為了能支援驅動取消機制,一般都在收到IRP請求時就馬上註冊一個取消回撥例程 CancelRoutine,註冊的函式是 IoSetCancelRoutine(Irp, CancelRoutine)

3. IoCancelIrp 呼叫中就會去先呼叫 IoAcquireCancelSpinLock,再呼叫這個設定好的CancelRoutine

4. CancelRoutine中,驅動一般會根據當前Irp的狀態來做相應的操作。如果Irp是在請
求佇列中,就會先將它移出佇列,設定好返回狀態STATUS_CANCELLED,然後再呼叫IoCompleteRequest來直接完成這個請求,最後千萬不要忘記呼叫 
IoReleaseCancelSpinLock(Irp的CancelIrql)來釋放自旋鎖。

 

 

 

 

7. Tail(一個很大的聯合體)

union 
{
    struct 
    {
    .
    .
    union 
    {
      KDEVICE_QUEUE_ENTRY DeviceQueueEntry;
      struct 
      {
        PVOID  DriverContext[4];
      };
    };
    .
    .
    PETHREAD  Thread;
    .
    .
    LIST_ENTRY  ListEntry;
    .
    .
    } Overlay;
  .
  .
} Tail;

Tail.Overlay是Tail聯合中的一種 結構,它含有幾個對WDM驅動程式有潛在用途的成員。

在這個圖中,以水平方向從左到右是這個聯合的三個可 選成員,在垂直方向是每個結構的成員描述。

Tail.Overlay.DeviceQueueEntry(KDEVICE_QUEUE_ENTRY)

Tail.Overlay.DriverContext(PVOID[4])是Tail.Overlayare內 一個未命名聯合的兩個可選成員(只能出現一個)。

I/O管理器把DeviceQueueEntry作為裝置標準請求佇列中的連線域。當IRP還沒有進入某 個佇列時,如果你擁有這個IRP你可以使用這個域,你可以任意使用DriverContext中的四個指標。

Tail.Overlay.ListEntry(LIST_ENTRY) 僅能作為你自己實現的私有佇列的連線域。我們在做檔案系統驅動過濾的時候往往對讀寫請求進行序列化處理,這個時候就需要這個ListEntry來構建一個IO請求的佇列,以解決並行請求的序列化問題。

 

 




 

至此,我們把IRP頭部的資料結構分析完了,接下繼續學習下IRP Sub-Requst子請求部分的資料結構(即裝置棧中的每層驅動對應的IO_STACK_LOCATION結構)

我們知道,在IRP頭部後面跟有很多個相同的結構。

PIO_STACK_LOCATION 
  IoGetCurrentIrpStackLocation(
    IN PIRP  Irp
    );

使用這個函式可以獲得"本層"所對應的那個IO_STACK_LOCATION。

我們來分析一下這個IO_STACK_LOCATION的資料結構。

typedef struct _IO_STACK_LOCATION 
{
  UCHAR  MajorFunction;
  UCHAR  MinorFunction;
  UCHAR  Flags;
  UCHAR  Control;
  union 
  {
        //
        // Parameters for IRP_MJ_CREATE 
        //
        struct 
      {
            PIO_SECURITY_CONTEXT  SecurityContext;
            ULONG  Options;
            USHORT POINTER_ALIGNMENT  FileAttributes;
            USHORT  ShareAccess;
            ULONG POINTER_ALIGNMENT  EaLength;
        } Create;
        //
        // Parameters for IRP_MJ_READ 
        //
        struct 
      {
            ULONG  Length;
            ULONG POINTER_ALIGNMENT  Key;
            LARGE_INTEGER  ByteOffset;
        } Read;
        //
        // Parameters for IRP_MJ_WRITE 
        //
        struct 
      {
            ULONG  Length;
            ULONG POINTER_ALIGNMENT  Key;
            LARGE_INTEGER  ByteOffset;
        } Write;
        //
        // Parameters for IRP_MJ_QUERY_INFORMATION 
        //
        struct 
      {
            ULONG  Length;
            FILE_INFORMATION_CLASS POINTER_ALIGNMENT  FileInformationClass;
        } QueryFile;
        //
        // Parameters for IRP_MJ_SET_INFORMATION 
        //
        struct 
      {
            ULONG  Length;
            FILE_INFORMATION_CLASS POINTER_ALIGNMENT  FileInformationClass;
            PFILE_OBJECT  FileObject;
            union 
          {
                struct 
            {
                    BOOLEAN  ReplaceIfExists;
                    BOOLEAN  AdvanceOnly;
                };
                ULONG  ClusterCount;
                HANDLE  DeleteHandle;
            };
        } SetFile;
        //
        // Parameters for IRP_MJ_QUERY_VOLUME_INFORMATION 
        //
        struct 
      {
            ULONG  Length;
            FS_INFORMATION_CLASS POINTER_ALIGNMENT  FsInformationClass;
        } QueryVolume;
        //
        // Parameters for IRP_MJ_DEVICE_CONTROL and IRP_MJ_INTERNAL_DEVICE_CONTROL 
        //
        struct 
      {
            ULONG  OutputBufferLength;
            ULONG POINTER_ALIGNMENT  InputBufferLength;
            ULONG POINTER_ALIGNMENT  IoControlCode;
            PVOID  Type3InputBuffer;
        } DeviceIoControl;
        //
        // Nonsystem service parameters.
        //
        // Parameters for IRP_MN_MOUNT_VOLUME 
        //
        struct 
      {
            PVOID  DoNotUse1;
            PDEVICE_OBJECT  DeviceObject;
        } MountVolume;
        //
        // Parameters for IRP_MN_VERIFY_VOLUME 
        //
        struct 
      {
            PVOID  DoNotUse1;
            PDEVICE_OBJECT  DeviceObject;
        } VerifyVolume;
        //
        // Parameters for Scsi using IRP_MJ_INTERNAL_DEVICE_CONTROL 
        //
        struct 
      { 
            struct _SCSI_REQUEST_BLOCK  *Srb;
        } Scsi;
        //
        // Parameters for IRP_MN_QUERY_DEVICE_RELATIONS 
        //
        struct 
      {
            DEVICE_RELATION_TYPE  Type;
        } QueryDeviceRelations;
        //
        // Parameters for IRP_MN_QUERY_INTERFACE 
        //
        struct 
      {
            CONST GUID  *InterfaceType;
            USHORT  Size;
            USHORT  Version;
            PINTERFACE  Interface;
            PVOID  InterfaceSpecificData;
        } QueryInterface;
        //
        // Parameters for IRP_MN_QUERY_CAPABILITIES 
        //
        struct 
      {
            PDEVICE_CAPABILITIES  Capabilities;
        } DeviceCapabilities;
        //
        // Parameters for IRP_MN_FILTER_RESOURCE_REQUIREMENTS 
        //
        struct 
      {
            PIO_RESOURCE_REQUIREMENTS_LIST  IoResourceRequirementList;
        } FilterResourceRequirements;
        //
        // Parameters for IRP_MN_READ_CONFIG and IRP_MN_WRITE_CONFIG 
        //
        struct 
      {
            ULONG  WhichSpace;
            PVOID  Buffer;
            ULONG  Offset;
            ULONG  POINTER_ALIGNMENT Length;
        } ReadWriteConfig;
        //
        // Parameters for IRP_MN_SET_LOCK 
        //
        struct 
      {
            BOOLEAN  Lock;
        } SetLock;
        //
        // Parameters for IRP_MN_QUERY_ID 
        //
        struct 
      {
            BUS_QUERY_ID_TYPE  IdType;
        } QueryId;
        //
        // Parameters for IRP_MN_QUERY_DEVICE_TEXT 
        //
        struct 
      {
            DEVICE_TEXT_TYPE  DeviceTextType;
            LCID POINTER_ALIGNMENT  LocaleId;
        } QueryDeviceText;
        //
        // Parameters for IRP_MN_DEVICE_USAGE_NOTIFICATION 
        //
        struct 
      {
            BOOLEAN  InPath;
            BOOLEAN  Reserved[3];
            DEVICE_USAGE_NOTIFICATION_TYPE POINTER_ALIGNMENT Type;
        } UsageNotification;
        //
        // Parameters for IRP_MN_WAIT_WAKE 
        //
        struct 
      {
            SYSTEM_POWER_STATE  PowerState;
        } WaitWake;
        //
        // Parameter for IRP_MN_POWER_SEQUENCE 
        //
        struct 
      {
            PPOWER_SEQUENCE  PowerSequence;
        } PowerSequence;
        //
        // Parameters for IRP_MN_SET_POWER and IRP_MN_QUERY_POWER 
        //
        struct 
      {
            ULONG  SystemContext;
            POWER_STATE_TYPE POINTER_ALIGNMENT  Type;
            POWER_STATE POINTER_ALIGNMENT  State;
            POWER_ACTION POINTER_ALIGNMENT  ShutdownType;
        } Power;
        //
        // Parameters for IRP_MN_START_DEVICE 
        //
        struct 
      {
            PCM_RESOURCE_LIST  AllocatedResources;
            PCM_RESOURCE_LIST  AllocatedResourcesTranslated;
        } StartDevice;
        //
        // Parameters for WMI Minor IRPs 
        //
        struct 
      {
            ULONG_PTR  ProviderId;
            PVOID  DataPath;
            ULONG  BufferSize;
            PVOID  Buffer;
        } WMI;
        //
        // Others - driver-specific
        //
        struct 
      {
            PVOID  Argument1;
            PVOID  Argument2;
            PVOID  Argument3;
            PVOID  Argument4;
        } Others;
    } Parameters;
  PDEVICE_OBJECT  DeviceObject;
  PFILE_OBJECT  FileObject;
  .
  .
  .
} IO_STACK_LOCATION, *PIO_STACK_LOCATION;

 

1. IRP請求型別

UCHAR  MajorFunction;
UCHAR  MinorFunction;

在每個驅動的入口函式DriverEntry中,我們經常要做的是就是當前驅動的分發函式進行賦值。即上層應用會很多種不同的呼叫請求,這些請求被windows以IRP_MJ_XX這樣的主功能號進行了分類。例如下面的程式碼。

NTSTATUS
DriverEntry (
    IN PDRIVER_OBJECT DriverObject,
    IN PUNICODE_STRING RegistryPath
    )
{
    ...
    DriverObject->MajorFunction[IRP_MJ_CREATE] = SfCreate;
    DriverObject->MajorFunction[IRP_MJ_CREATE_NAMED_PIPE] = SfCreate;
    DriverObject->MajorFunction[IRP_MJ_CREATE_MAILSLOT] = SfCreate;
        
    DriverObject->MajorFunction[IRP_MJ_FILE_SYSTEM_CONTROL] = SfFsControl;
    DriverObject->MajorFunction[IRP_MJ_CLEANUP] = SfCleanupClose;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = SfCleanupClose;
    ...
}

從面向介面程式設計的角度來理解,windows已經在介面中實現了一個分發例程的原型,並以陣列的形式把介面(即函式地址)放了出來,我們在寫程式碼的時候,如果想在本驅動中對指定的IRP型別進行處理,就必須去對"分發例程(就是這個陣列)"進行賦值,並對我們提供的例程函式進行程式碼實現。

IRP Major Function Codes

IRP_MJ_CREATE 
IRP_MJ_PNP 
IRP_MJ_POWER 
IRP_MJ_READ 
IRP_MJ_WRITE 
IRP_MJ_FLUSH_BUFFERS 
IRP_MJ_QUERY_INFORMATION 
IRP_MJ_SET_INFORMATION 
IRP_MJ_DEVICE_CONTROL 
IRP_MJ_INTERNAL_DEVICE_CONTROL 
IRP_MJ_SYSTEM_CONTROL 
IRP_MJ_CLEANUP 
IRP_MJ_CLOSE 
IRP_MJ_SHUTDOWN 

除了主功能號之外,還有子功能號,這是在一些PnP manager(PnP型別操作的IRP), the power manager(電源管理), file system drivers(檔案系統)中需要通過子功能號來進一步對IRP的操作型別進行區分。所以每個型別的IRP操作(PnP/Power/filesystem)的子功能號都是不一樣的。我們以檔案系統的子功能號為例子。

switch (irpSp->MinorFunction) 
 { 
    //磁碟卷掛載
        case IRP_MN_MOUNT_VOLUME: 
            return SfFsControlMountVolume( DeviceObject, Irp );  
    
    //磁碟卷載入
        case IRP_MN_LOAD_FILE_SYSTEM: 
            return SfFsControlLoadFileSystem( DeviceObject, Irp );

    //磁碟的請求
        case IRP_MN_USER_FS_REQUEST:
        {
            switch (irpSp->Parameters.FileSystemControl.FsControlCode) 
        { 
                case FSCTL_DISMOUNT_VOLUME:
                {
                     ..
                }
            }
            break;
        }
 }        

總之,這個功能號是用來對IRP請求的型別進行區分的,它們只是一些代號而已。

 

 

 

2. UNION Parameters

接下來是一個很大的聯合體,我們仔細觀察,其實這個聯合體還是很有規律的,而且也很簡單,因為有規律的東西往往會相對簡單。

裡面包含了每種IRP請求所需要的引數,可以參考MSDN上的解釋。

http://msdn.microsoft.com/en-us/library/ff550659

 

 

 

3. PDEVICE_OBJECT  DeviceObject

指向這個IO_STACK_LOCATINO所對應的裝置物件。可能是中間的過濾裝置,也可能是底層的真實裝置。

 

 

 

4. PFILE_OBJECT  FileObject

指向一個這個IRP對應的檔案物件,這個檔案物件是一個廣義的概念,在核心中,磁碟/檔案/目錄都算是一種檔案。

typedef struct _FILE_OBJECT 
{
    CSHORT  Type;
    CSHORT  Size;
    PDEVICE_OBJECT  DeviceObject;
    PVPB  Vpb;
    PVOID  FsContext;
    PVOID  FsContext2;
    PSECTION_OBJECT_POINTERS  SectionObjectPointer;
    PVOID  PrivateCacheMap;
    NTSTATUS  FinalStatus;
    struct _FILE_OBJECT  *RelatedFileObject;
    BOOLEAN  LockOperation;
    BOOLEAN  DeletePending;
    BOOLEAN  ReadAccess;
    BOOLEAN  WriteAccess;
    BOOLEAN  DeleteAccess;
    BOOLEAN  SharedRead;
    BOOLEAN  SharedWrite;
    BOOLEAN  SharedDelete;
    ULONG  Flags;
    UNICODE_STRING  FileName;
    LARGE_INTEGER  CurrentByteOffset;
    ULONG  Waiters;
    ULONG  Busy;
    PVOID  LastLock;
    KEVENT  Lock;
    KEVENT  Event;
    PIO_COMPLETION_CONTEXT  CompletionContext;
    KSPIN_LOCK  IrpListLock;
    LIST_ENTRY  IrpList;
    PVOID  FileObjectExtension;
} FILE_OBJECT, *PFILE_OBJECT;

在檔案系統的過濾驅動的程式設計中,我們經常要使用到這個引數,來獲取這次操作所涉及到的檔案物件,以此得到這個檔案的相關資訊。由於本次筆記重點是資料結構的學習,相關的使用場景打算在後續的學習筆記中進行應用。

 

相關文章