【转】驱动和应用层的三种通信方式

http://bbs.pediy.com/showthread.php?p=446641

http://book.51cto.com/art/201107/275240.htm

驱动程序和客户应用程序经常需要进行数据交换,但我们知道驱动程序和客户应用程序可能不
在同一个地址空间,因此操作系统必须解决两者之间的数据交换。

驱动层和应用层通信,主要是靠DeviceIoControl函数,下面是该函数的原型:

BOOL DeviceIoControl ( 
  HANDLE hDevice, // 设备句柄 
  DWORD dwIoControlCode, // IOCTL请求操作代码 
  LPVOID lpInBuffer, // 输入缓冲区地址 
  DWORD nInBufferSize, // 输入缓冲区大小 
  LPVOID lpOutBuffer, // 输出缓冲区地址 
  DWORD nOutBufferSize, // 输出缓冲区大小 
  LPDWORD lpBytesReturned, // 存放返回字节数的指针 
  LPOVERLAPPED lpOverlapped // 用于同步操作的Overlapped结构体指针 
);

dwIoControlCode
进行操作的控制码。驱动程序可以通过CTL_CODE宏来组合定义一个控制码,

#define CTL_CODE(DeviceType, Function, Method, Access) \
( ( (DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) )

DeviceType

Defines the type of device for the given IOCTL.
This parameter can be no bigger than a WORD value.
The values used by Microsoft are in the range 0-32767;
the values 32768-65535 are reserved for use by OEMs and IHVs.

Function

Defines an action within the device category.
Function codes 0-2047 are reserved for Microsoft;
codes 2048-4095 are reserved for OEMs and IHVs.

A function code can be no larger then 4095.

Method

Defines the method codes for how buffers are passed for I/O and file system controls.

The following values are possible for this parameter:

METHOD_BUFFERED
METHOD_IN_DIRECT
METHOD_OUT_DIRECT
METHOD_NEITHER

Access

Defines the access check value for any access.

The following table shows the possible flags for this parameter.
The FILE_ACCESS_ANY is generally the correct value.

FlagDescription
FILE_ANY_ACCESS Request all access.
FILE_READ_ACCESS Request read access. Can be used with FILE_WRITE_ACCESS.
FILE_WRITE_ACCESS Request write access. Can be used with FILE_READ_ACCESS.
FILE_READ_ACCESS | FILE_WRITE_ACCESS Request read and write access

 

Remarks

The macro can be used for defining IOCTL and FSCTL function control codes.
All IOCTLs must be defined this way to ensure that values used by Microsoft,
OEMs, and IHVs do not overlap.

The following illustration shows the format of the resulting IOCTL.

根据Ioctl码的不同,DeviceIoControl函数可以发出两种IRP:

IRP_MJ_FILE_SYSTEM_CONTROL 代表了file system I/O control (FSCTL)请求

IRP_MJ_DEVICE_CONTROL 代表了设备的IOCTL请求。

IRP_MJ_INTERNAL_DEVICE_CONTROL,仅仅在内核模式下使用, 它用于内核不同组件间的通信。

DeviceIoControl需要提供输入和输出缓冲区:

并在IRP_MJ_DEVICE_CONTROL的实现中进行控制码的操作。
在驱动层,irpStack->Parameters.DeviceIoControl.IoControlCode表示了这个控制码。


IOCTL请求有四种缓冲策略,下面一一介绍。 

1、 输入输出缓冲I/O(METHOD_BUFFERED)
2、 直接输入缓冲输出I/O(METHOD_IN_DIRECT)
3、 缓冲输入直接输出I/O(METHOD_OUT_DIRECT)
4、 上面三种方法都不是(METHOD_NEITHER)

Buffered方式在输入、输出数据时都要产生用户缓冲区与内核中的拷贝缓冲区间的复制操作,效率上较低,而

Direct方式从字面上理解即为“直接”,从上图也可看出,它的输出操作是通过MDL方式完成的,
这是一种用户模式内存映射到系统(内核)内存的方法,避免了复制操作,效率上就提高了。

在Neither方式下,I/O管理器直接将用户模式下的缓冲区地址传递给内核驱动程序,不做任何映射变换。
驱动如果想直接使用这个地址,必须处于这个进程的上下文中,因为只有在同一个上下文中,
用户进程和驱动例程使用的同一个地址值,才能被系统映射到同一个内存页面。

如果使用了METHOD_BUFFERED,表示系统将用户的输入输出都经过
pIrp->AssociatedIrp.SystemBuffer来缓冲,因此这种方式的通信比较安全。

如果使用了METHOD_IN_DIRECT或METHOD_OUT_DIRECT方式,
表示系统会将输入缓冲在pIrp->AssociatedIrp.SystemBuffer中,
并将输出缓冲区锁定,然后在内核模式下重新映射一段地址,这样也是比较安全的。

但是如果使用了METHOD_NEITHER方式,虽然通信的效率提高了,但是不够安全。
驱动的派遣函数中可以通过I/O堆栈(IO_STACK_LOCATION)的
stack->Parameters.DeviceIo Control.Type3InputBuffer得到。
输出缓冲区可以通过pIrp->UserBuffer得到。
由于驱动中的派遣函数不能保证传递进来的用户输入和输出地址,
因此最好不要直接去读写这些地址的缓冲区。
应该在读写前使用ProbeForRead和ProbeForWrite函数探测地址是否可读和可写。

为了对这些类型更详细的描述,请看msdn上的解释,我抄录如下:

备注:在下面的讨论中,

"输入" 表示数据 从用户模式的应用程序 到 驱动程序,
"输出" 表示数据 从驱动程序 到 用户模式的应用程序。

"缓冲"方法(METHOD_BUFFERED) 

METHOD_BUFFERED可称为"缓冲方式",是指Ring3指定的输入、输出缓冲区的内存读和写
都是经过系统的"缓冲",具体过程如图所示。

这种方式下,首先系统会将Ring3下指定的输入缓冲区(UserInputBuffer)数据, 按指定的输入长度(InputBufferLen)复制到Ring0中事先分配好的缓冲内存 (SystemBuffer,通过pIrp
->AssociatedIrp.SystemBuffer得到)中。 驱动程序就可以将SystemBuffer视为输入数据进行读取,当然也可以将SystemBuffer视为输出数据的缓冲区, 也就是说SystemBuffer既可以读也可以写。 驱动程序处理完后,系统会按照pIrp->IoStatus->Information指定的字节数, 将SystemBuffer上的数据复制到Ring3指定的输出缓冲区(UserOutputBuffer)中。 可见这个过程是比较安全的,避免了驱动程序在内核态直接操作用户态内存地址的问题,这种方式是推荐使用的方式。

对于读取请求,I/O 管理器分配一个与用户模式的缓冲区大小相同的系统缓冲区。
IRP 中的 SystemBuffer 字段包含系统地址。UserBuffer 字段包含初始的用户缓冲区地址。
当完成请求时,I/O 管理器将驱动程序已经提供的数据从系统缓冲区复制到用户缓冲区。

对于写入请求,会分配一个系统缓冲区并将 SystemBuffer 设置为地址。
用户缓冲区的内容会被复制到系统缓冲区,但是不设置 UserBuffer。
对于 IOCTL 请求,会分配一个容量大小足以包含输入缓冲区或输出缓冲区的系统缓冲区,
并将 SystemBuffer 设置为分配的缓冲区地址。
输入缓冲区中的数据复制到系统缓冲区。
UserBuffer 字段设置为用户模式输出缓冲区地址。

内核模式驱动程序应当只使用系统缓冲区,且不应使用 UserBuffer 中存储的地址。

对于 IOCTL,驱动程序应当从系统缓冲区获取输入并将输出写入到系统缓冲区。
当完成请求时,I/O 系统将输出数据从系统缓冲区复制到用户缓冲区。


"直接"方法(METHOD_IN/OUT_DIRECT)

METHOD_IN_DIRECT和METHOD_OUT_DIRECT可称为"直接方式",
是指系统依然对Ring3的输入缓冲区进行缓冲,但是对Ring3的输出缓冲区并没有缓冲,
而是在内核中进行了锁定。这样Ring3输出缓冲区在驱动程序完成I/O请求之前,
都是无法访问的,从一定程度上保障了安全性。如图所示。这两种方式,对于Ring3的输入缓冲区和METHOD_BUFFERED方式是一致的。 对于Ring3的输出缓冲区,首先由系统锁定,并使用pIrp
->MdlAddress来描述这段内存, 驱动程序需要使用MmGetSystemAddressForMdlSafe函数将这段内存映射到内核内存地址 (OutputBuffer),然后可以直接写入OutputBuffer地址,最终在驱动派遣例程返回后, 由系统解除这段内存的锁定。METHOD_IN_DIRECT和METHOD_OUT_DIRECT方式的区别, 仅在于打开设备的权限上,当以只读权限打开设备时,METHOD_IN_DIRECT方式的IoControl将会成功, 而METHOD_OUT_DIRECT方式将会失败。如果以读写权限打开设备,两种方式都会成功。

对于读取和写入请求,用户模式缓冲区会被锁定,并且会创建一个内存描述符列表 (MDL)。
MDL 地址会存储在 IRP 的 MdlAddress 字段中。
SystemBuffer 和 UserBuffer 均没有任何含义。但是,驱动程序不应当更改这些字段的值。


对于 IOCTL 请求,如果在 METHOD_IN_DIRECT 和 METHOD_OUT_DIRECT 中同时有一个输出缓冲区,
则分配一个系统缓冲区(SystemBuffer 又有了地址)并将输入数据复制到其中。
如果有一个输出缓冲区,且它被锁定,则会创建 MDL 并设置 MdlAddress。
UserBuffer 字段没有任何含义。


"两者都不"方法(METHOD_NEITHER)

METHOD_NEITHER可称为"其他方式",这种方式与METHOD_BUFFERED方式正好相反。
METHOD_BUFFERED方式相当于对Ring3的输入输出都进行了缓冲,
而METHOD_ NEITHER方式是不进行缓冲的,在驱动中可以直接使用Ring3的输入输出内存地址,如图所示。

驱动程序可以通过pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer
得到Ring3的输入缓冲区地址(其中pIrpStack是IoGetCurrentIrpStackLocation(pIrp)的返回);
通过pIrp-> UserBuffer得到Ring3的输出缓冲区地址。

由于METHOD_NEITHER方式并不安全,因此最好对Type3InputBuffer读取之前使用ProbeForRead函数进行探测,
对UserBuffer写入之前使用ProbeForWrite函数进行探测,当没有发生异常时,再进行读取和写入操作。

 

对于读取和写入请求,UserBuffer 字段被设置为指向初始的用户缓冲区。不执行任何其他操作。
SystemAddress 和 MdlAddress 没有任何含义。

对于 IOCTL 请求,I/O 管理器将 UserBuffer 设置为初始的用户输出缓冲区,
而且,它将当前 I/O 栈位置的 Parameters.DeviceIoControl.Type3InputBuffer 
设置为用户输入缓冲区。利用该 I/O 方法,
由驱动程序来确定如何处理缓冲区:分配系统缓冲区或创建 MDL。


通常,驱动程序在访问用户数据时不应当将 UserBuffer 字段用作地址,
即使当用户缓冲区被锁定时也是如此。
这是由于在调用驱动程序时,在系统中可能看不到调用用户的地址空间。
(对于该规则的一个例外是,在最高层驱动程序将 IRP 向下传递到较低层的驱动程序之前,
它可能需要使用 UserBuffer 来复制数据。)
如果使用"直接"或"两者都不"方法,在创建 MDL 之后,
驱动程序可以使用 MmGetSystemAddressForMdl 函数来获取有效的系统地址以访问用户缓冲区。


在驱动层,依传输类型的不同,输入缓冲区的位置亦不同,见下表。

传输类型                         位置
METHOD_IN_DIRECT                irp->AssociatedIrp.SystemBuffer
METHOD_OUT_DIRECT               irp->AssociatedIrp.SystemBuffer
METHOD_BUFFERED                 irp->AssociatedIrp.SystemBuffer
METHOD_NEITHER                  irpStack->Parameters.DeviceIoControl.Type3InputBuffer

在驱动层,依传输类型的不同,输出缓冲区的位置亦不同,见下表。

传输类型                         位置
METHOD_IN_DIRECT                irp->MdlAddress
METHOD_OUT_DIRECT               irp->MdlAddress
METHOD_BUFFERED                 irp->AssociatedIrp.SystemBuffer
METHOD_NEITHER                  irp->UserBuffer

所以只要确定了传输方式后,就可以根据各自的位置来读取和写入数据,从而实现应用层和驱动的通信。
下面看驱动层对ioctl控制码的处理代码:

//METHOD_OUT_DIREC方式
NTSTATUS COMM_DirectOutIo(PIRP Irp, PIO_STACK_LOCATION pIoStackIrp, UINT *sizeofWrite)
{
    NTSTATUS status = STATUS_UNSUCCESSFUL;
    PVOID pInputBuffer, pOutputBuffer;
    ULONG  outputLength, inputLength;
    DbgPrint("COMM_DirectOutIo\r\n");
    outputLength = pIoStackIrp->Parameters.DeviceIoControl.OutputBufferLength;
    inputLength  = pIoStackIrp->Parameters.DeviceIoControl.InputBufferLength;
    pInputBuffer = Irp->AssociatedIrp.SystemBuffer;
    pOutputBuffer = NULL;
    if(Irp->MdlAddress)
        pOutputBuffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority); 
    if(pInputBuffer && pOutputBuffer)
    {                                                          
        DbgPrint("COMM_DirectOutIo UserModeMessage = '%s'", pInputBuffer);
        RtlCopyMemory(pOutputBuffer, pInputBuffer, outputLength);
        *sizeofWrite = outputLength;
        status = STATUS_SUCCESS;
    }
    return status;
}

// METHOD_IN_DIRECT
NTSTATUS COMM_DirectInIo(PIRP Irp, PIO_STACK_LOCATION pIoStackIrp, UINT *sizeofWrite)
{
    NTSTATUS status = STATUS_UNSUCCESSFUL;
    PVOID pInputBuffer, pOutputBuffer;
    ULONG  outputLength, inputLength;
    DbgPrint("COMM_DirectInIo\r\n");
    outputLength = pIoStackIrp->Parameters.DeviceIoControl.OutputBufferLength;
    inputLength  = pIoStackIrp->Parameters.DeviceIoControl.InputBufferLength;
    pInputBuffer = Irp->AssociatedIrp.SystemBuffer;
    pOutputBuffer = NULL;
    if(Irp->MdlAddress)
        pOutputBuffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
    if(pInputBuffer && pOutputBuffer)
    {                                                          
        DbgPrint("COMM_DirectInIo UserModeMessage = '%s'", pInputBuffer);
        RtlCopyMemory(pOutputBuffer, pInputBuffer, outputLength);
        *sizeofWrite = outputLength;
        status = STATUS_SUCCESS;
    }
    return status;
}

// METHOD_BUFFERED
NTSTATUS COMM_BufferedIo(PIRP Irp, PIO_STACK_LOCATION pIoStackIrp, UINT *sizeofWrite)
{
    NTSTATUS status = STATUS_UNSUCCESSFUL;
    PVOID pInputBuffer, pOutputBuffer;
    ULONG  outputLength, inputLength;
    DbgPrint("COMM_BufferedIo\r\n");
    outputLength = pIoStackIrp->Parameters.DeviceIoControl.OutputBufferLength;
    inputLength  = pIoStackIrp->Parameters.DeviceIoControl.InputBufferLength;
    pInputBuffer = Irp->AssociatedIrp.SystemBuffer;
    pOutputBuffer = Irp->AssociatedIrp.SystemBuffer;
    if(pInputBuffer && pOutputBuffer)
    {              
      DbgPrint("COMM_BufferedIo UserModeMessage = '%s'", pInputBuffer);
      RtlCopyMemory(pOutputBuffer, pInputBuffer, outputLength);
      *sizeofWrite = outputLength;
      status = STATUS_SUCCESS;
    }
    return status;
}

// METHOD_NEITHER
NTSTATUS COMM_NeitherIo(PIRP Irp, PIO_STACK_LOCATION pIoStackIrp, UINT *sizeofWrite)
{
    NTSTATUS status = STATUS_UNSUCCESSFUL;
    PVOID pInputBuffer, pOutputBuffer;
    ULONG  outputLength, inputLength;
    DbgPrint("COMM_NeitherIo\r\n");
    outputLength  = pIoStackIrp->Parameters.DeviceIoControl.OutputBufferLength;
    inputLength   = pIoStackIrp->Parameters.DeviceIoControl.InputBufferLength;
    pInputBuffer  = pIoStackIrp->Parameters.DeviceIoControl.Type3InputBuffer;
    pOutputBuffer = Irp->UserBuffer;
    if(pInputBuffer && pOutputBuffer)
    {              
      DbgPrint("COMM_NeitherIo UserModeMessage = '%s'", pInputBuffer);
      RtlCopyMemory(pOutputBuffer, pInputBuffer, outputLength);
      *sizeofWrite = outputLength;
      status = STATUS_SUCCESS;
    }
    return status;
}

代码比较简单,都是取得输入的数据,然后把数据直接拷贝到输出,传输给应用层。应用层的代码:

procedure TfrmMain.Send_Recv_Data(AInData: String; var AOutData:String; IoctlCode: DWORD);
var
  dwReturn: DWORD;
  inData:array[0..1023] of char;
  outData:array[0..1023] of char;
begin
  StrPCopy(inData, AInData);
  if m_hCommDevice <> 0 then
  begin
    DeviceIoControl(m_hCommDevice, IoctlCode, 
@inData, Length(inData), @outData, Length(outData), dwReturn,
nil);
AOutData :
= StrPas(@outData); end; end;

上面是进行发送和接受的过程。需要通信,只要如下做:

procedure TfrmMain. btnDirect_IN_IOClick (Sender: TObject);
var
  outData:String;
begin
  Send_Recv_Data( Trim(edtDirect_in_in.Text), outData, IOCTL_COMM_DIRECT_IN_IO );
  edtDirect_in_out.Text := outData;
end;

这是 direct_in方式通信,其他通信方式类似,大家可以参考代码了,这里就不列举了,
由于代码比较简单,我就不多说了,大家还是看代码吧,很好明白。


原文地址:https://www.cnblogs.com/shangdawei/p/3108086.html