在学习内核过滤驱动的过程中,遇到了大量的涉及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[];
};
};
.
.
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个地方可以描述缓冲区。

) irp->AssociatedIrp.SystemBuffer
) irp->MdlAddress
) 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 |= ;
}

总结一下,在新增设备的时候这个物理设备的数据缓冲方式就被决定了。这之后你不能该变缓冲方式的设置,因为过滤器驱动程序将复制这个标志设置,并且,如果你改变了设置,过滤器驱动程序没有办法知道这个改变。

接下来解决目前为止的最后一个疑问: 系统是如何来根据设备对象中的缓冲类型码来进行不同缓冲区类型的的缓冲区数据的实际填充过程的(即实际的缓冲区填充过程是什么)?

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例程能够返回两个状态值中的任一个:
. STATUS_PENDING--继续向上完成特定IRP。I/O管理器提升IRP栈指针,并且调用上一个驱动的IoCompletion例程(继续回卷)
. STATUS_MORE_PROCESSING_REQUIRED--中断向上完成特定IRP的过程,并且把IRP栈指针留在当前位置(暂时中断回卷)。
返回这一状态的驱动通常会在过后调用IoCompleteRequest例程来重新开始向上完成特定的IRP的过程。
完成IRP时是忽略还是拷贝当前栈空间(IO_STACK_LOCATION),返回什么状态
值,以及完成函数中如何结束IRP,我们这里做个总结。
) 如果对IRP完成之后的事情无兴趣,则直接忽略当前IO_STACK_LOCATION(即
调用内核API函数IoSkipCurrentIrpStackLocation),然后向下传递请求,返回IoCallDriver所返回的状态。 ) 不但对IRP完成之后的事情无兴趣,而且不打算立刻返回成功或失败(即挡在当前这
一层的驱动这里,不再往下传递了)。那么不用忽略或者拷贝当前IO_STACK_LOCATION,填写IRP的状态参数后调用IoCompleteRequest,
并返回自己想返回的结果。 ) 如果对IRP完成之后的事情有兴趣,并打算在完成函数中处理,则应该首先拷贝当
前IO_STACK_LOCATION(IoCopyCurrentIrpStackLocationToNext),然后指定完成函数,并返回IoCallDriver()所返回的状态。在完成函数中,不需要调用
IoCompleteRequest,直接返回IRP的当前状态即可 ) 如果对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:驱动发送很多请求到下级驱动去处理,下级驱动返回了一个请求的结果。可是,这个结果是个错误,而且是个很严重的错误,比如设备出故障了。这时,就要将设备进行错误恢复,如重启设备,同时,其它送下去的请求都要同时取消掉。

驱动如何被取消? 一般来讲,取消的发起者一定是上层的驱动,而取消的实际执行者,则是下层的驱动。

. 上层的驱动通过调用 IoCancelIrp(Irp)来取消某个请求。这个请求应该是被某个下层驱动放在缓冲队列中等待处理或正在处理。

. 下层驱动为了能支持驱动取消机制,一般都在收到IRP请求时就马上注册一个取消回调例程 CancelRoutine,注册的函数是 IoSetCancelRoutine(Irp, CancelRoutine)

. IoCancelIrp 调用中就会去先调用 IoAcquireCancelSpinLock,再调用这个设置好的CancelRoutine

. CancelRoutine中,驱动一般会根据当前Irp的状态来做相应的操作。如果Irp是在请
求队列中,就会先将它移出队列,设置好返回状态STATUS_CANCELLED,然后再调用IoCompleteRequest来直接完成这个请求,最后千万不要忘记调用
IoReleaseCancelSpinLock(Irp的CancelIrql)来释放自旋锁。

7. Tail(一个很大的联合体)

union
{
struct
{
.
.
union
{
KDEVICE_QUEUE_ENTRY DeviceQueueEntry;
struct
{
PVOID DriverContext[];
};
};
.
.
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[];
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;

在文件系统的过滤驱动的编程中,我们经常要使用到这个参数,来获取这次操作所涉及到的文件对象,以此得到这个文件的相关信息。由于本次笔记重点是数据结构的学习,相关的使用场景打算在后续的学习笔记中进行应用。

IRP IO_STACK_LOCATION 《寒江独钓》内核学习笔记(1)的更多相关文章

  1. KTHREAD 线程调度 SDT TEB SEH shellcode中DLL模块机制动态获取 《寒江独钓》内核学习笔记(5)

    目录 . 相关阅读材料 . <加密与解密3> . [经典文章翻译]A_Crash_Course_on_the_Depths_of_Win32_Structured_Exception_Ha ...

  2. ETHREAD APC 《寒江独钓》内核学习笔记(4)

    继续学习windows 中和线程有关系的数据结构: ETHREAD.KTHREAD.TEB 1. 相关阅读材料 <windows 内核原理与实现> --- 潘爱民 2. 数据结构分析 我们 ...

  3. EPROCESS 进程/线程优先级 句柄表 GDT LDT 页表 《寒江独钓》内核学习笔记(2)

    在学习笔记(1)中,我们学习了IRP的数据结构的相关知识,接下来我们继续来学习内核中很重要的另一批数据结构: EPROCESS/KPROCESS/PEB.把它们放到一起是因为这三个数据结构及其外延和w ...

  4. 《寒江独钓_Windows内核安全编程》中修改类驱动分发函数

    最近在阅读<寒江独钓_Windows内核安全编程>一书的过程中,发现修改类驱动分发函数这一技术点,书中只给出了具体思路和部分代码,没有完整的例子. 按照作者的思路和代码,将例子补充完整,发 ...

  5. Linux内核学习笔记-2.进程管理

    原创文章,转载请注明:Linux内核学习笔记-2.进程管理) By Lucio.Yang 部分内容来自:Linux Kernel Development(Third Edition),Robert L ...

  6. Linux内核学习笔记-1.简介和入门

    原创文章,转载请注明:Linux内核学习笔记-1.简介和入门 By Lucio.Yang 部分内容来自:Linux Kernel Development(Third Edition),Robert L ...

  7. Linux内核学习笔记二——进程

    Linux内核学习笔记二——进程   一 进程与线程 进程就是处于执行期的程序,包含了独立地址空间,多个执行线程等资源. 线程是进程中活动的对象,每个线程都拥有独立的程序计数器.进程栈和一组进程寄存器 ...

  8. 20135316王剑桥Linux内核学习笔记

    王剑桥Linux内核学习笔记 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 计算机是如何工作的 个人理 ...

  9. KPROCESS IDT PEB Ldr 《寒江独钓》内核学习笔记(3)

    继续上一篇(2)未完成的研究,我们接下来学习 KPROCESS这个数据结构. 1. 相关阅读材料 <深入理解计算机系统(原书第2版)> 二. KPROCESS KPROCESS,也叫内核进 ...

随机推荐

  1. Oracle中sequence的使用方法

    在Oracle数据库中,sequence等同于序列号,每次取的时候sequence会自动增加,一般会作用于需要按序列号排序的地方. 1.Create Sequence (注释:你需要有CREATE S ...

  2. 原生js颗粒页换图效果

    <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8&quo ...

  3. rsyslog 日志统一搜集&message格式

        日志格式修改: http://jiechao2012.blog.51cto.com/3251753/1143762 http://yulei7633.blog.51cto.com/149275 ...

  4. CENTOS7 添加自定义快捷键(启动TERMINAL,显示桌面等)

    centos7中不自带启动终端的快捷键,可以自定义添加. 点击右上角的用户名,点击设置,在设置面板点击键盘,选择快捷键选项卡进入快捷键设置界面. 点击“+”添加快捷键.名称自定义(如"启动终 ...

  5. Ajax实现异步操作实例_针对XML格式的请求数据

    js分类中有一节[原生js异步请求,XML解析]主要说明了js前台是如何处理XML格式请求和如何接受由服务器返回的XML数据的解析,今天我将用一个实例来说明具体要如何操作. 前台的参数类型也是XML使 ...

  6. Sublime Text 2 - 性感无比的代码编辑器!程序员必备神器!跨平台支持Win/Mac/Linux

    我用过的编辑器不少,真不少- 但却没有哪款让我特别心仪的,直到我遇到了 Sublime Text 2 !如果说“神器”是我能给予一款软件最高的评价,那么我很乐意为它封上这么一个称号.它小巧绿色且速度非 ...

  7. Caffe学习系列(12):训练和测试自己的图片

    学习caffe的目的,不是简单的做几个练习,最终还是要用到自己的实际项目或科研中.因此,本文介绍一下,从自己的原始图片到lmdb数据,再到训练和测试模型的整个流程. 一.准备数据 有条件的同学,可以去 ...

  8. ant命令总结

    ant命令总结 博客分类: 版本管理 svn , maven , ant   ant命令总结 1 Ant是什么?  Apache Ant 是一个基于 Java的生成工具. 生成工具在软件开发中用来将源 ...

  9. 使用Windows Live Writer发布日志

    前言 Windows Live Writer是非常不错的一个日志发布工具,支持本地写文章,然后通过点击一个按钮就发布到网站上,如果借助插件,还可以同时发布到多个博客网站,功能非常强大,很多博友认识她之 ...

  10. 从0开始学Java——@override的作用

    早上跟着<jsp&Servlet学习笔记>来学习jsp,在使用eclipse创建了一个servlet类之后,发现自动创建的类和书上相比,doGet方法的前面少了@override, ...