Windows IOCTL Methods

在学习Windows内核开发之初,每个新手都会在DeviceIoControl上纠结一阵,然后就选择一个最得心应手的。拿在手中,去走遍全天下。等走了一圈之后就会感慨天下竟如此简单!

或许,天下就是这般简单,但DeviceIoControl却稍有些复杂。

DeviceIoControl的Buffer传输方式有三种:METHOD_BUFFERED,METHOD_IN_DIRECT & METHOD_OUT_DIRECT,METHOD_NEITHER。METHOD_IN_DIRECT和METHOD_OUT_DIRECT算作是同一类。

DeviceIoControl是用户层程序和内核驱动的一种通信方式。这个通信可完全看成是两人之间的私密,二者想怎么样就怎么样,只要双方都知晓并应用同一个规则即可。但如果还有个第三者的话,沟通起来就没有这么方便了,所以Windows内核要定义一个规则。可以说,根本上是因为Windows内核这个第三者的存在,也正是根据Windows内核在其中的参与度的不同才衍生出了这三种不同的ioctl方法。

用户层API定义:

BOOL WINAPI DeviceIoControl(
__in         HANDLE hDevice,
__in         DWORD dwIoControlCode,
__in_opt     LPVOID lpInBuffer,
__in         DWORD nInBufferSize,
__out_opt    LPVOID lpOutBuffer,
__in         DWORD nOutBufferSize,
__out_opt    LPDWORD lpBytesReturned,
__inout_opt  LPOVERLAPPED lpOverlapped
);

内核层Irp Stack中相关DeviceIoControl的参数(取自IRP及IO_STACK_LOCATION结构):

typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _IRP {
CSHORT Type;
USHORT Size;
PMDL MdlAddress;

union {
struct _IRP *MasterIrp;
LONG IrpCount;
PVOID SystemBuffer;
} AssociatedIrp;

……
PVOID UserBuffer;
……
} IRP, *PIRP;

typedef struct _IO_STACK_LOCATION {
UCHAR MajorFunction;
UCHAR MinorFunction;
……
union {
struct {
……
} Create;
……
struct {
ULONG OutputBufferLength;
ULONG POINTER_ALIGNMENT InputBufferLength;
ULONG POINTER_ALIGNMENT IoControlCode;
PVOID Type3InputBuffer;
} DeviceIoControl;
……
} Parameters;
……
} IO_STACK_LOCATION, *PIO_STACK_LOCATION;

用户层DeviceIoControl()参数及Irp成员之间的关系,针对不同的ioctl method不同的对应,详见下面的表格:

IOCTL-MTHODs

METHOD_BUFFERED:

这种方式是最简单,也是最常用的。内核驱动不用任何处理即可操作从应用层发过来的数据。实际上用户使用上的简单,正是因为内核做了相关的处理而省去了用户自己处理的繁琐。

在DeviceIoContro()阶段:NtDeviceIoControlFile()在构造Irp时,会于nInBufferSize及nOutBufferSize中选择大者以分配irp->AssociatedIrp.SystemBuffer,并将lpInBuffer中的数据复制至新分配的SystemBuffer中。

如果有SystemBuffer的分配,Irp->Flags则会被设置标志位:    IRP_BUFFERED_IO | IRP_DEALLOCATE_BUFFER。如果lpOutBuffer不为空的话,Irp->Flags也会被置上标志: IRP_INPUT_OPERATION。这三个标志在Irp完成时会用到。

在此要注意一点,针对METHOD_BUFFERED,即使lpInputBuffer和lpOutputBuffer为同一个Buffer,调用DeviceIoControl()时也必须全部指定,否则不能正确将请求数据发送至内核层或者用户层接收不到返回的数据。

在IRP_MJ_DEVICE_CONTROL完成阶段:IopCompleteRequest()会根据Irp->Flags标志来完成后续的处理,针对IRP_BUFFERED_IO,即METHOD_BUFFERED:

  • 如果设置了IRP_INPUT_OPERATION标志,会将返回数据从SystemBuffer中转移至用户传递过来的lpOutBuffer
  • 如果设置了IRP_DEALLOCATE_BUFFER标志,则释放SystemBuffer内存至PagedPool

METHOD_IN_DIRECT & METHOD_OUT_DIRECT:

在DeviceIoContro()阶段:如果lpInBuffer存在的话(并且满足nInBufferSize > 0),I/O Manager会以METHOD_BUFFERED的方式来处理lpInBuffer,即会分配SystemBuffer并将输入数据从lpInBuffer转移至SystemBuffer。如果lpOutBuffer存在,则会创建Irp->MdlAddress并将lpOutBuffer所用页面全部锁定。

在IRP_MJ_DEVICE_CONTROL完成阶段:IopCompleteRequest()会解锁irp->MdlAddress中所占用的页面,并释放irp->MdlAddress。

可以看出,如果设置了lpInBuffer的话,系统还是会另外分配内存空间,并造成内存的浪费。好在用户完全可以不用lpInBuffer,而是将lpOutBuffer同时存放输入数据Buffer。另外METHOD_IN_DIRECT及METHOD_OUT_DIRECT的使用也完全由用户决定,系统只是个传递者,并不参与lpInBuffer及lpOutBuffer中数据的处理。

METHOD_NEITHER:

这种方式下,系统参与度最小,一般用于能够快速处理的操作。I/O Manager所做的工作只是将输入参数传递至内核,其它的什么事也不做。如果内核驱动要将Irp挂起来的话,它要将irpSp->Parameters.DeviceIoControl.Type3InputBuffer及Irp->UserBuffer所用页面锁定起来,并在完成时释放所占资源(如果将lpOutBuffer/Irp->UserBuffer锁定至irp->MdlAddress的话,IopCompleteRequest()在调用时会做释放工作)。

总结起来,METHOD_BUFFERED最省事,但会多消耗系统内存;METHOD_NEITHER最直接,虽然看起来并不“直/DIRECT”  :-) METHOD_IN_DIRECT或METHOD_OUT_DIRECT最适合大内存区的操作,即使需要将讲求挂起来,内核驱动也不用做额外的工作。但在内存虚拟地址空间紧缺时,特别是在32位系统上,对于特大的内存区操作,宜采用METHOD_NEITHER方式以节省地址资源。