最近在学习驱动编程,因为大部分程序在Ring0层获得信息后,都要传递到Ring3层,将从内核获得的信息交由用户层处理。所以了解Ring3层与Ring0层的通信就显得至关重要了。下面简单介绍Ring3层与Ring0层的通信。
首先要了解IRP,开发一个驱动可能要处理各种各样的IRP。应用层为了和驱动通信,首先必须打开设备,然后发送或者接受消息,最后关系这个请求。这个过程涉及到了三个IRP,第一是打开请求;第二是接受或者发送消息;第三个是关闭请求。这三个IRP请求对应的功能号分别为:打开请求的主功能号是IRP_MJ_CREATE,关闭请求的主功能号是IRP_MJ_CLOSE,处理设备控制信息是IRP_MJ_DEVICE_CONTROL。主功能号就是DRIVER_OBJECT中分发函数指针数组中的索引。其中分发函数指针个数为IRP_MJ_MAXIMUM_FUNCTION,所以我们处理IRP请求的时候必须先设置分发函数:
DriverObject -> DriverUnload = MyDriverUnload;
DriverObject -> MajorFunction[IRP_MJ_CREATE]=DispatchCreate;
DriverObject -> MajorFunction[IRP_MJ_CLOSE]=DispatchClose;
DriverObject -> MajorFunction[IRP_MJ_DEVICE_CONTROL]=DispatchIoctl;
分发函数和DriverEntry以及MyDriverUnload函数的格式声明如下:
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING pRegString);
NTSTATUS DispatchCreate(PDEVICE_OBJECT DriverObject, PIRP pIrp);
NTSTATUS DispatchClose(PDEVICE_OBJECT DriverObject, PIRP pIrp);
VOID MyDriverUnload(PDRIVER_OBJECT DriverObject);
NTSTATUS DispatchIoctl(PDEVICE_OBJECT DriverObject, PIRP pIrp);
其中DriverUnload不在MajorFunction指针数组中,后面的MyDriverUnload是函数名称,即为分发函数的指针。在分发函数中可以通过以下语句获得IRP功能号、控制码、输入输出缓冲区长度等。例如io_stack -> MajorFunction则表示IRP的主功能号
PIO_STACK_LOCATION io_stack = IoGetCurrentIrpStackLocation(pIrp);
//控制码
ULONG code =io_stack -> Parameters.DeviceIoControl.IoControlCode;
//输入输出缓冲区长度
ULONG in_len = io_stack -> Parameters.DeviceIoControl.InputBufferLength;
ULONG out_len = io_stack -> Parameters.DeviceIoControl.OutputBufferLength;
一个普通的不做任何处理,仅返回成功的处理IRP请求的函数仅需要三个步骤:
(1)、设置pIrp -> IoStatus.Information=0
(2)、设置pIrp -> IoStatus.Status=STATUS_SUCCESS
(3)、调用IoCompleteRequest(pIrp,IO_NO_INCREMENT)完成IRP请求的处理。
完整的函数如下所示:
NTSTATUS DispatchCreate(PDEVICE_OBJECT DriverObject, PIRP pIrp)
{
pIrp -> IoStatus.Information = 0;
pIrp -> IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(pIrp,IO_NO_INCREMENT);
KdPrint(("Device IRP_MJ_CEATE\n"));
return pIrp -> IoStatus.Status;
}
在应用层打开关闭设备的代码如下:
HANDLE hDevice=CreateFile(L"\\\\.\\MyDevice",GENERIC_READ|GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_SYSTEM,0);
if(hDevice==INVALID_HANDLE_VALUE)
{
printf("Open Device Error\n");
return FALSE;
}
现在来看看应用层信息传入内核层,然后内核层返回信息的IRP请求函数的处理。
在应用层中我们可以使用ReadFile和WriteFile函数来处理打开的设备句柄。使用这两个函数的时候驱动层收到的IRP主功能号为IRP_MJ_READ、IRP_MJ_WRITE。而使用DeviceIpControl函数是双向的,在驱动层收到的IRP的主功能号是IRP_MJ_DEVICE_CONTROL。这个函数原型如下:
BOOL DeviceIoControl (
HANDLE hDevice, // 设备句柄
DWORD dwIoControlCode, // IOCTL请求操作代码
LPVOID lpInBuffer, // 输入缓冲区地址
DWORD nInBufferSize, // 输入缓冲区大小
LPVOID lpOutBuffer, // 输出缓冲区地址
DWORD nOutBufferSize, // 输出缓冲区大小
LPDWORD lpBytesReturned, // 存放返回字节数的指针
LPOVERLAPPED lpOverlapped // 用于同步操作的Overlapped结构体指针
);
当驱动收到一个由DeviceIoControl函数产生的IRP后,应该通过判断控制码来实施不同的操作,控制码的获得前面已经提到过:
ULONG code =io_stack -> Parameters.DeviceIoControl.IoControlCode;
使用控制码的时候需要预先使用一个CTL_CODE宏定义来定义一个控制码,如下所示:
#define MY_CODE \
(ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN, \
0xa01, \
METHOD_BUFFERED, \
FILE_READ_DATA|FILE_WRITE_DATA)
第一行define后表示控制码宏名,在驱动层中获得的code如果判断等于MY_CODE,即执行对应的操作。也是DeviceIoControl的第二个参数:DWORD dwIoControlCode,定义不同的控制码可以指定驱动执行不同的操作。
第三行中的0xa01是用户自定义的。
第四行是IOCTL的缓冲策略,此时的缓冲策略是METHOD_BUFFERED,表示输入输出缓冲IO。
IOCTL请求有四种缓冲策略,下面一一介绍。
1、 输入输出缓冲I/O(METHOD_BUFFERED)
2、 直接输入缓冲输出I/O(METHOD_IN_DIRECT)
3、 缓冲输入直接输出I/O(METHOD_OUT_DIRECT)
4、 上面三种方法都不是(METHOD_NEITHER)
下面红色部分为看雪某大牛的写的驱动与应用程序通信的笔记:
"缓冲"方法(METHOD_BUFFERED)
备注:在下面的讨论中,"输入"表示数据从用户模式的应用程序到驱动程序,"输出"表示数据从驱动程序到应用程序。
对于读取请求,I/O 管理器分配一个与用户模式的缓冲区大小相同的系统缓冲区。IRP 中的 SystemBuffer 字段包含系统地址。UserBuffer 字段包含初始的用户缓冲区地址。当完成请求时,I/O 管理器将驱动程序已经提供的数据从系统缓冲区复制到用户缓冲区。对于写入请求,会分配一个系统缓冲区并将 SystemBuffer 设置为地址。用户缓冲区的内容会被复制到系统缓冲区,但是不设置 UserBuffer。对于 IOCTL 请求,会分配一个容量大小足以包含输入缓冲区或输出缓冲区的系统缓冲区,并将 SystemBuffer 设置为分配的缓冲区地址。输入缓冲区中的数据复制到系统缓冲区。UserBuffer 字段设置为用户模式输出缓冲区地址。内核模式驱动程序应当只使用系统缓冲区,且不应使用 UserBuffer 中存储的地址。
对于 IOCTL,驱动程序应当从系统缓冲区获取输入并将输出写入到系统缓冲区。当完成请求时,I/O 系统将输出数据从系统缓冲区复制到用户缓冲区。
"直接"方法(METHOD_IN/OUT_DIRECT)
对于读取和写入请求,用户模式缓冲区会被锁定,并且会创建一个内存描述符列表 (MDL)。MDL 地址会存储在 IRP 的 MdlAddress 字段中。SystemBuffer 和 UserBuffer 均没有任何含义。但是,驱动程序不应当更改这些字段的值。
对于 IOCTL 请求,如果在 METHOD_IN_DIRECT 和 METHOD_OUT_DIRECT 中同时有一个输出缓冲区,则分配一个系统缓冲区(SystemBuffer 又有了地址)并将输入数据复制到其中。如果有一个输出缓冲区,且它被锁定,则会创建 MDL 并设置 MdlAddress。UserBuffer 字段没有任何含义。
"两者都不"方法(METHOD_NEITHER)
对于读取和写入请求,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
控制码的其他部分基本保持不变即可。下面是一个驱动层与应用层通信的代码,主要使用METHOD_BUFFERED方法进行通信:
驱动层
1 NTSTATUS DispatchIoctl(PDEVICE_OBJECT DriverObject, PIRP pIrp) 2 { 3 PIO_STACK_LOCATION io_stack; 4 NTSTATUS status; 5 io_stack = IoGetCurrentIrpStackLocation(pIrp); 6 if (io_stack->MajorFunction==IRP_MJ_DEVICE_CONTROL) 7 { 8 switch (io_stack->Parameters.DeviceIoControl.IoControlCode) 9 { 10 case MY_CODE: 11 { 12 //获得从应用层传递的信息,并且可以将内核层信息传递给应用层 13 pIrp -> IoStatus.Information = 0; 14 pIrp -> IoStatus.Status = STATUS_SUCCESS; 15 break; 16 } 17 default : 18 { 19 pIrp -> IoStatus.Information = 0; 20 pIrp -> IoStatus.Status = STATUS_INVALID_PARAMETER; 21 break; 22 } 23 } 24 } 25 IoCompleteRequest(pIrp,IO_NO_INCREMENT); 26 return pIrp -> IoStatus.Status; 27 }
应用层
1 char out_buffer[10000]="\0"; 2 3 int main() 4 { 5 BOOL ret; 6 DWORD ret_length=0; 7 HANDLE hDevice=CreateFile(L"\\\\.\\MyDevice",GENERIC_READ|GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_SYSTEM,0); 8 if(hDevice==INVALID_HANDLE_VALUE) 9 { 10 printf("Open Device Error\n"); 11 return FALSE; 12 } 13 ret = DeviceIoControl(hDevice,GETPROC_LIST,NULL,NULL,out_buffer,sizeof(out_buffer),&ret_length,NULL); 14 if(ret==FALSE) 15 { 16 printf("DeviceIoControl Error\n"); 17 return ret; 18 } 19 //处理从out_buffer获得从内核层传递来的信息 20 CloseHandle(hDevice); 21 system("pause"); 22 return ret; 23 }
上面主要是应用层主导,首先将信息传送给内核层,然后内核层处理IRP请求后,获得对应的从应用层传递过来的信息后,在将需要传递的内核层获得信息放入缓冲区传递到应用层。下面我们将要介绍的是驱动层主动传送信息到应用层。
下回分解。