在网上找到一篇 AntBean写的 《驱动入门科普:从WRK理解IRP IRP Stack之理论篇》特别好,所以摘抄下来。
由于网速问题 我把原文截取的图片源码替换成文字源码。
写这篇文章的主要目的是整理一下自己最近的驱动学习,从读源码的角度解析驱动程序中极其重要的概念IRP和IRP Stack。暗合侯捷老师所言:源码之下,并无新事。
对像我这种死脑筋的初学者,驱动编写教程上,如果要实现某功能就按这个格式的说法并不能让我满意,反而让我更加糊涂。为啥就要这样编写?IRP到底是啥?IRP Stack呢?为啥我使用DeviceTree看到的IRP Stack的Stack Size并不像教程上讲的那样,我找到的Device Stack为啥Attach时最下面的Device的Stack Size不是1?为啥过滤驱动中要跳过某个IRP就使用
IoSkipCurrentIrpStackLocation而要处理某个IRP就IoCopyCurrentIrpStackLocationToNext()呢?设置IoCompleteRoutine到底有啥用呢?如果我想实现文件过滤驱动,我又怎么知道我的过滤驱动该Attach到哪个Device上呢?啥是CDO?跟普通的Device Object有啥区别?
如果我想直接发送IRP ,我该怎么做呢?能不能给我一个完整的实现文件夹保护的文件过滤驱动的代码供我研究呢?
这些事实上就是我初次接触文件过滤驱动时的疑问。楚狂人在文件过滤驱动教程二中有些说法让我很困惑,我决定自己探究。
一、 IoCallDriver 与 IO_STACK_LOCATION
看WRK中IoCallDriver的代码:
NTSTATUS
FORCEINLINE
IopfCallDriver(
IN PDEVICE_OBJECT DeviceObject,
IN OUT PIRP Irp
)
/*++
Routine Description:
This routine is invoked to pass an I/O Request Packet (IRP) to another
driver at its dispatch routine.
Arguments:
DeviceObject - Pointer to device object to which the IRP should be passed.
Irp - Pointer to IRP for request.
Return Value:
Return status from driver's dispatch routine.
--*/
{
PIO_STACK_LOCATION irpSp;
PDRIVER_OBJECT driverObject;
NTSTATUS status;
//
// Ensure that this is really an I/O Request Packet.
//
ASSERT( Irp->Type == IO_TYPE_IRP );
//
// Update the IRP stack to point to the next location.
//
<strong>Irp->CurrentLocation--;</strong>
if (Irp->CurrentLocation <= 0) {
KiBugCheck3( NO_MORE_IRP_STACK_LOCATIONS, (ULONG_PTR) Irp, 0, 0 );
}
<strong>irpSp = IoGetNextIrpStackLocation( Irp );
Irp->Tail.Overlay.CurrentStackLocation = irpSp;</strong>
//
// Save a pointer to the device object for this request so that it can
// be used later in completion.
//
<strong>irpSp->DeviceObject = DeviceObject;</strong>
//
// Invoke the driver at its dispatch routine entry point.
//
<strong>driverObject = DeviceObject->DriverObject;</strong>
//
// Prevent the driver from unloading.
//
<strong> status = driverObject->MajorFunction[irpSp->MajorFunction]( DeviceObject,
Irp );</strong>
return status;
}
可以看到,IoCallDriver做了以下几件事:
1. 递减了IRP的CurrentLocation
2. 获得下个位置的StackLocation,并设置到IRP的CurrentStackLocation中
3. 设置StackLocation的DeviceObject,并调用该DeviceObject的DriverObject中该StackLocation指定的MajorFunction。
此刻,我相信,你一定对IoGetNextIrpStackLocation做的事很好奇,同时对IO_STACK_LOCATION的结构产生了浓厚的兴趣。
先看IoGetNextIrpStackLocation
#define IoGetNextIrpStackLocation( Irp ) (\
(Irp)->Tail.Overlay.CurrentStackLocation - 1 )
看来IoGetNextIrpStackLocation啥事也没做,一切的奥秘尽在IO_STACK_LOCATION的结构中。
如果你读过OSROnline上那篇著名的Build your own Irp,我想你一定对作者给出的创建自己的IRP并填充的方法感到好奇,心里想他是怎么知道这么做的?
一般是先IoAllocateIrp,在填充后调用IoCallDriver。
此刻我们先看看IoAllocateIrp函数的实现,这将有助于我们理解IO_STACK_LOCATION。
WRK中IoAllocateIrp函数的实际实现是IopAllocateIrpPrivate,代码先尝试从look aside list中分配该IRP,如果不行就从NonPagedPool中分配该IRP,有意思的是分配的IRP的大小,
PIRP
IopAllocateIrpPrivate(
IN CCHAR StackSize,
IN BOOLEAN ChargeQuota
)
/*++
Routine Description:
This routine allocates an I/O Request Packet from the system nonpaged pool.
The packet will be allocated to contain StackSize stack locations. The IRP
will also be initialized.
Arguments:
StackSize - Specifies the maximum number of stack locations required.
ChargeQuota - Specifies whether quota should be charged against thread.
Return Value:
The function value is the address of the allocated/initialized IRP,
or NULL if one could not be allocated.
--*/
{
USHORT allocateSize;
UCHAR fixedSize;
PIRP irp;
UCHAR lookasideAllocation;
PGENERAL_LOOKASIDE lookasideList;
PP_NPAGED_LOOKASIDE_NUMBER number;
USHORT packetSize;
PKPRCB prcb;
CCHAR largeIrpStackLocations;
//
// If the size of the packet required is less than or equal to those on
// the lookaside lists, then attempt to allocate the packet from the
// lookaside lists.
//
if (IopIrpProfileStackCountEnabled()) {
IopProfileIrpStackCount(StackSize);
}
irp = NULL;
fixedSize = 0;
packetSize = IoSizeOfIrp(StackSize);
allocateSize = packetSize;
prcb = KeGetCurrentPrcb();
//
// Capture this value once as it can change and use it.
//
largeIrpStackLocations = (CCHAR)IopLargeIrpStackLocations;
if ((StackSize <= (CCHAR)largeIrpStackLocations) &&
((ChargeQuota == FALSE) || (prcb->LookasideIrpFloat > 0))) {
fixedSize = IRP_ALLOCATED_FIXED_SIZE;
number = LookasideSmallIrpList;
if (StackSize != 1) {
allocateSize = IoSizeOfIrp((CCHAR)largeIrpStackLocations);
number = LookasideLargeIrpList;
}
lookasideList = prcb->PPLookasideList[number].P;
lookasideList->TotalAllocates += 1;
irp = (PIRP)InterlockedPopEntrySList(&lookasideList->ListHead);
if (irp == NULL) {
lookasideList->AllocateMisses += 1;
lookasideList = prcb->PPLookasideList[number].L;
lookasideList->TotalAllocates += 1;
irp = (PIRP)InterlockedPopEntrySList(&lookasideList->ListHead);
if (irp == NULL) {
lookasideList->AllocateMisses += 1;
}
}
if (IopIrpAutoSizingEnabled() && irp) {
//
// See if this IRP is a stale entry. If so just free it.
// This can happen if we decided to change the lookaside list size.
// We need to get the size of the IRP from the information field as the size field
// is overlayed with single list entry.
//
if (irp->IoStatus.Information < packetSize) {
lookasideList->TotalFrees += 1;
ExFreePool(irp);
irp = NULL;
} else {
//
// Update allocateSize to the correct value.
//
allocateSize = (USHORT)irp->IoStatus.Information;
}
}
}
//
// If an IRP was not allocated from the lookaside list, then allocate
// the packet from nonpaged pool and charge quota if requested.
//
lookasideAllocation = 0;
if (!irp) {
//
// There are no free packets on the lookaside list, or the packet is
// too large to be allocated from one of the lists, so it must be
// allocated from nonpaged pool. If quota is to be charged, charge it
// against the current process. Otherwise, allocate the pool normally.
//
if (ChargeQuota) {
irp = ExAllocatePoolWithQuotaTag(NonPagedPool|POOL_QUOTA_FAIL_INSTEAD_OF_RAISE,
allocateSize,' prI');
} else {
//
// Attempt to allocate the pool from non-paged pool. If this
// fails, and the caller's previous mode was kernel then allocate
// the pool as must succeed.
//
irp = ExAllocatePoolWithTag(NonPagedPool, allocateSize, ' prI');
}
if (!irp) {
return NULL;
}
} else {
if (ChargeQuota != FALSE) {
lookasideAllocation = IRP_LOOKASIDE_ALLOCATION;
InterlockedDecrement( &prcb->LookasideIrpFloat );
}
ChargeQuota = FALSE;
}
//
// Initialize the packet.
// Note that irp->Size may not be equal to IoSizeOfIrp(StackSize)
//
IopInitializeIrp(irp, allocateSize, StackSize);
irp->AllocationFlags = (fixedSize | lookasideAllocation);
if (ChargeQuota) {
irp->AllocationFlags |= IRP_QUOTA_CHARGED;
}
return irp;
}
<pre name="code" class="cpp">//++
//
// USHORT
// IoSizeOfIrp(
// IN CCHAR StackSize
// )
//
// Routine Description:
//
// Determines the size of an IRP given the number of stack locations
// the IRP will have.
//
// Arguments:
//
// StackSize - Number of stack locations for the IRP.
//
// Return Value:
//
// Size in bytes of the IRP.
//
//--
#define IoSizeOfIrp( StackSize ) \
((USHORT) (sizeof( IRP ) + ((StackSize) * (sizeof( IO_STACK_LOCATION )))))
也就是说,连着该StackSize的IO_STACK_LOCATION一并分配了。还记得在Build your own Irp中,该处的StackSize是从哪儿获得的?我先将一个自己分配Irp实现读文件的代码贴在下面,然后在分析的过程中回头看该代码,再结合WRK,将会有通透的感觉。
PIRP pIrp;
PMDL pMdl = 0;
KEVENT kEvent;
pIrp = IoAllocateIrp(fileObj->DeviceObject->StackSize, FALSE);
if (!pIrp)
{
return -1;
}
if (fileObj->DeviceObject->Flags & DO_BUFFERED_IO)
{
pIrp->AssociatedIrp.SystemBuffer = lpBuffer;
}else if (fileObj->DeviceObject->Flags & DO_DIRECT_IO)
{
pMdl = IoAllocateMdl(lpBuffer, bufferSize, 0, 0, 0);
MmBuildMdlForNonPagedPool(pMdl);
pIrp->MdlAddress = pMdl;
}else
{
pIrp->UserBuffer = lpBuffer;
}
PIO_STACK_LOCATION pIoStackLoc = IoGetNextIrpStackLocation(pIrp);
pIoStackLoc->FileObject = fileObj;
pIoStackLoc->MajorFunction = IRP_MJ_READ;
pIoStackLoc->MinorFunction = IRP_MN_NORMAL;
pIoStackLoc->Parameters.Read.ByteOffset = 0;
pIoStackLoc->Parameters.Read.Key = 0;
pIoStackLoc->Parameters.Read.Length = bufferSize;
IoSetCompletionRoutine(pIrp, IoCompletion, &kEvent, TRUE, TRUE, TRUE);
ntSattus = IoCallDriver(fileObj->DeviceObject, pIrp);
好了,这里可以看到,该StackSize来源于那个DeviceObject。这就验证了那句设备栈和IRP Stack是一一对应的。
事实上,IO_STACK_LOCATION才是指定Driver调用的函数和参数的东东。IRP只是IO_STACK_LOCATION的集合而已。
换句话讲,我们在应用层都是直接调用一个API的。而驱动层呢?驱动之间相互调用,并不是直接调该驱动提供的API,而是初始化好IO_STACK_LOCATION,在IO_STACK_LOCATION中保存了我们要调用的MajorFunction和传递的参数。
现在可以来看IO_STACK_LOCATION结构了。
在WRK中的定义比较长,这里我省略了,贴出简化版。
struct IO_STACK_LOCATION
{
MajorFunction //要调用的函数
Parameters; //函数传入的参数
DeviceObject; //该DeviceObject
CompletionRoutine //下面具体讲解
}
这里我省略了FileObjectMinorFunction Control等。因为这些到具体要用到的时候再查。这样的省略并不妨碍我们理解整个IRP Stack。这才是困扰我们的重点。我们现在的目标是在驱动中给某个DeviceObject发IRP。让我们的IRP能正确执行。显然,我们得调用IoCallDriver发送该IRP。在IoCallDriver时,IRP中的IoStackLocation递减1后该IoStackLocation的某些值被取出来使用。这就意味着,我们分配IRP后得将其IoStackLocation减1的位置的MajorFunction和其他参数正确初始化。这样就理解了前面那个自己发送IRP为啥使用GetNextIrpStackLocation获得后再初始化。
对照上面的自己发送IRP的过程,我们已经理解了IoAllocateIrp, IoGetNextIrpStackLocation和IoCallDriver。现在,我们还有点不清楚,那个IoSetCompleteRoutine有啥用
一、 IoCompleteRequest 与Completion Routine
先看IoSetCompletionRoutine干啥了:
<pre name="code" class="cpp">#define IoSetCompletionRoutine( Irp, Routine, CompletionContext, Success, Error, Cancel ) { \
PIO_STACK_LOCATION __irpSp; \
ASSERT( ((Success) | (Error) | (Cancel)) ? (Routine) != NULL : TRUE ); \
__irpSp = IoGetNextIrpStackLocation( (Irp) ); \
__irpSp->CompletionRoutine = (Routine); \
__irpSp->Context = (CompletionContext); \
__irpSp->Control = 0; \
if ((Success)) { __irpSp->Control = SL_INVOKE_ON_SUCCESS; } \
if ((Error)) { __irpSp->Control |= SL_INVOKE_ON_ERROR; } \
if ((Cancel)) { __irpSp->Control |= SL_INVOKE_ON_CANCEL; } }
也就是仅仅设置了下一层IO_STACK_LOCATION的CompletionRoutine。到底有啥用呢?
我们来看一段常见的驱动处理完一个IRP后的代码:
pIrp->IoStatus.Status = STATUS_ACCESS_DENIED;
pIrp->IoStatus.Information = 0;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_ACCESS_DENIED;
也就是说,一个驱动在完成IRP后会调用IoCallDriver该函数中到底做了啥?代码比较长,这里就不贴了,直接说,IoCompleteRequest()中做了一些清理操作。最重要的是调用了IRP该层的IO_STACK_LOCATION中的CompleteRoutine函数。也就是说,我们在IO_STACK_LOCATION中设置的CompleteRoutine会在某层被调用。调用后根据返回值,IoCompleteRequest做一定的处理。如果是STATUS_MORE_PROCESSING_REQUIRED直接返回。否则清空当前栈,并做一些处理。
NTSTATUS
FORCEINLINE
IopfCallDriver(
IN PDEVICE_OBJECT DeviceObject,
IN OUT PIRP Irp
)
{
PIO_STACK_LOCATION irpSp;
PDRIVER_OBJECT driverObject;
NTSTATUS status;
//
// Ensure that this is really an I/O Request Packet.
//
ASSERT( Irp->Type == IO_TYPE_IRP );
//
// Update the IRP stack to point to the next location.
//
Irp->CurrentLocation--;
if (Irp->CurrentLocation <= 0) {
KiBugCheck3( NO_MORE_IRP_STACK_LOCATIONS, (ULONG_PTR) Irp, 0, 0 );
}
irpSp = IoGetNextIrpStackLocation( Irp );
Irp->Tail.Overlay.CurrentStackLocation = irpSp;
//
// Save a pointer to the device object for this request so that it can
// be used later in completion.
//
irpSp->DeviceObject = DeviceObject;
//
// Invoke the driver at its dispatch routine entry point.
//
driverObject = DeviceObject->DriverObject;
//
// Prevent the driver from unloading.
//
status = driverObject->MajorFunction[irpSp->MajorFunction]( DeviceObject,
Irp );
return status;
}
我相信,这样你应该能理解很多教程中告诉你的Complete Routine的编写法则,何时返回STATUS_MORE_PROCESSING_REQUIRED。
到此,IRP和IRP Stack的理论知识科普完了。下一节将会结合文件过滤驱动深入理解IRP IRPSTACK,同时将提供一个文件过滤驱动实现文件夹保护的示例代码。