驱动对象处理-狂人楚

来源:互联网 发布:无线点菜软件 编辑:程序博客网 时间:2024/04/28 23:25
7.1 驱动入口与驱动对象
驱动开发程序员所编写的驱动程序对应有一个结构。这个结构名为DRIVER_OBJECT。对应一个“驱动程序”。下面的代码展示的是一个最简单的驱动程序。

#include <ntddk.h>
NTSTATUS
DriverEntry (
    IN PDRIVER_OBJECT   DriverObject,
    IN PUNICODE_STRING  RegistryPath
    )
{
NTSTATUS status = STATUS_UNSUCCESSFUL;
return status;
}


函数DriverEntry是每个驱动程序中必须的。如同Win32应用程序里的WinMain。DriverEntry的第一个参数就是一个DRIVER_OBJECT的指针。这个DRIVER_OBJECT结构就对应当前编写的驱动程序。其内存是Windows系统已经分配的。
第二个参数RegistryPath是一个字符串。代表一个注册表子键。这个子键是专门分配给这个驱动程序使用的。用于保存驱动配置信息到注册表中。至于读写注册表的方法,请参照前面章节中的内容。
DriverEntry的返回值决定这个驱动的加载是否成功。如果返回为STATUS_SUCCESS,则驱动将成功加载。否则,驱动加载失败。
7.2 分发函数与卸载函数
DRIVER_OBJECT中含有分发函数指针。这些函数用来处理发到这个驱动的各种请求。Windows总是自己调用DRIVER_OBJECT下的分发函数来处理这些请求。所以编写一个驱动程序,本质就是自己编写这些处理请求的分发函数。
DRIVER_OBJECT下的分发函数指针的个数为IRP_MJ_MAXIMUM_FUNCTION。保存在一个数组中。下面的代码设置所有分发函数的地址为同一个函数:


NTSTATUS
DriverEntry (
    IN PDRIVER_OBJECT   DriverObject,
    IN PUNICODE_STRING  RegistryPath
    )
{
ULONG i;
for(i=0;i<IRP_MJ_MAXIMUM_FUNCTION;++i)
{
DriverObject->MajorFunctions[i] = MyDispatchFunction;
}

}


这个设置固然不难。难的工作都在编写MyDispatchFunction这个函数上。因为所有的分发函数指针都指向这一个函数,那么这个函数当然要完成本驱动所有的功能。下面是这个函数的原型。这个原型是Windows驱动编程的规范,不能更改:

NTSTATUS MyDispatchFunction(PDEVICE_OBJECT device,PIRP irp)
{
……
}


这里出现了DEVICE_OBJECT和IRP这两大结构。前一个表示一个由本驱动生成的设备对象。后一个表示一个系统请求。也就是说,现在要处理的是:发给设备device的请求irp。请完成这个处理吧。这两个结构在后面再进一步描述。
还有一个不放在分发函数数组中的函数,称为卸载函数也非常重要。如果存在这个函数,则该驱动程序可以动态卸载。在卸载时,该函数会被执行。该函数原型如下:


VOID MyDriverUnload(PDRIVER_OBJECT driver)
{
……
}


这个函数的地址设置到DriverObject->DriverUnload即可。
由于没有返回值,所以实际上在DriverUnload中,已经无法决定这个驱动能否卸载。只能做善后处理。
7.3 设备与符号链接
驱动程序和系统其他组件之间的交互是通过给设备发送或者接受发给设备的请求来交互的。换句话说,一个没有任何设备的驱动是不能按规范方式和系统交互的。当然也不会收到任何IRP,分发函数也失去了意义。
但并不意味着这样的驱动程序不存在。如果一个驱动程序只是想写写日志文件、Hook某些内核函数或者是做一些其他的小动作,也可以不生成任何设备,也不需要关心分发函数的设置。
如果驱动程序要和应用程序之间通信,则应该生成设备。此外还必须为设备生成应用程序可以访问的符号链接。下面的驱动程序生成了一个设备,并设置了分发函数:


#include <ntifs.h>// 之所以用ntifs.h而不是ntddk.h是因为我习惯开发文件
// 系统驱动,实际上目前对读者来说这两个头文件没区别。
NTSTATUS DriverEntry(
PDRIVER_OBJECT driver,
PUNICODE_STRING reg_path)
{
NTSTATUS status;
PDEVICE_OBJECT device;
// 设备名
UNICODE_STRING device_name = 
RTL_CONSTANT_STRING("\\Device\\MyCDO");
// 符号链接名
UNICODE_STRING symb_link = 
RTL_CONSTANT_STRING("\\DosDevices\\MyCDOSL");


// 生成设备对象
status = IoCreateDevice(
driver,
                0,
                device_name,
                FILE_DEVICE_UNKNOWN,
                0,
                FALSE,
                &device);


// 如果不成功,就返回。
if(!NT_SUCCESS(status))
return status;

// 生成符号链接
status = IoCreateSymbolicLink(
&symb_link,
&device_name);
if(!NT_SUCCESS(status))
{
IoDeleteDevice(device);
return status;
}
// 设备生成之后,打开初始化完成标记
device->Flags &= ~DO_DEVICE_INITIALIZING;
return status;
}


这个驱动成功加载之后,生成一个名叫“\Device\MyCDO”的设备。然后在给这个设备生成了一个符号链接名字叫做“\DosDevices\MyCDOSL”。应用层可以通过打开这个符号链接来打开设备。应用层可以调用CreateFile就像打开文件一样打开。只是路径应该是“"\\.\ MyCDOSL”。前面的“\\.\”意味后面是一个符号链接名,而不是一个普通的文件。请注意,由于C语言中斜杠要双写,所以正确的写法应该是“\\\\.\\”。与应用层交互的例子在下一节“IRP和IO_STACK_LOCATION”中。
7.4 设备的生成安全性限制
上一节的例子只是简单的例子。很多情况下那些代码会不起作用。为了避免读者在实际编程中遇到哪些特殊情况的困绕,下面详细说明生成设备和符号链接需要注意的地方。生成设备的函数原型如下:


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
);

这个函数的参数也非常复杂。但是实际上需要注意的并不多。
第一个参数是生成这个设备的驱动对象。
第二个参数DeviceExtensionSize非常重要。由于分发函数中得到的总是设备的指针。当用户需要在每个设备上记录一些额外的信息(比如用于判断这个设备是哪个设备的信息、以及不同的实际设备所需要记录的实际信息,比如网卡上数据包的流量、过滤器所绑定真实设备指针等等),需要指定的设备扩展区内存的大小。如果DeviceExtensionSize设置为非0,IoCreateDevice会分配这个大小的内存在DeviceObject->DeviceExtension中。以后用户就可以从根据DeviceObject-> DeviceExtension来获得这些预先保存的信息。
DeviceName如前例,是设备的名字。目前生成设备,请总是生成在\Device\目录下。所以前面写的名字是“\Device\MyCDO”。其他路径也是可以的,但是这在本书描述范围之外。
DeviceType表示设备类型。目前的范例无所谓设备类型,所以填写FILE_DEVICE_UNKNOWN即可。
DeviceCharacteristics目前请简单的填写0即可。
Exclusive这个参数必须设置FALSE。文档没有做任何解释。
最后生成的设备对象指针返回到DeviceObject中。
这种设备生成之后,必须有系统权限的用户才能打开(比如管理员)。所以如果编程者写了一个普通的用户态的应用程序去打开这个设备进行交互,那么很多情况下可以(用管理员登录的时候)。但是偶尔又不行(用普通用户登录的时候)。结果困绕很久。其实是权限问题。
为了保证交互的成功与安全性,应该用服务程序与之交互。
但是依然有时候必须用普通用户打开设备。为了这个目的,设备必须是对所有的用户开放的。此时不能用IoCreateDevice。必须用IoCreateDeviceSecure。这个函数的原型如下:


NTSTATUS 
  IoCreateDeviceSecure(
    IN PDRIVER_OBJECT  DriverObject,
    IN ULONG  DeviceExtensionSize,
    IN PUNICODE_STRING  DeviceName  OPTIONAL,
    IN DEVICE_TYPE  DeviceType,
    IN ULONG  DeviceCharacteristics,
    IN BOOLEAN  Exclusive,
    IN PCUNICODE_STRING  DefaultSDDLString,
    IN LPCGUID  DeviceClassGuid,
    OUT PDEVICE_OBJECT  *DeviceObject
   )
   
   这个函数增加了两个参数(其他的没变)。一个是DefaultSDDLString。这个一个用于描述权限的字符串。描述这个字符串的格式需要大量的篇幅。但是没有这个必要。字符串“D:P(A;;GA;;;WD)”将满足“人人皆可以打开”的需求。
   另一个参数是一个设备的GUID。请随机手写一个GUID。不要和其他设备的GUID冲突(不要复制粘贴即可)。
   下面是例子:
   
   // 随机手写一个GUID
   const GUID DECLSPEC_SELECTANY MYGUID_CLASS_MYCDO =
   {0x26e0d1e0L, 0x8189, 0x12e0, {0x99,0x14, 0x08, 0x00, 0x22, 0x30, 0x19, 0x03}};
   // 全用户可读写权限
   UNICODE_STRING sddl = 
    RLT_CONSTANT_STRING(L"D:P(A;;GA;;;WD)");
   // 生成设备
   status = IoCreateDeviceSecure( DriverObject,
    0,
    &device_name,
    FILE_DEVICE_UNKNOWN,
    FILE_DEVICE_SECURE_OPEN,
    FALSE, 
    &sddl,
    (LPCGUID)&SFGUID_CLASS_MYCDO,
    &device);
   
   使用这个函数的时候,必须连接库wdmsec.lib。
7.5 符号链接的用户相关性
从前面的例子看,符号链接的命名貌似很简单。简单的符号链接(之所以称为简单,是因为还有一种使用GUID的符号链接,这在本书讨论范围之外)总是命名在\DosDevices\之下。但是实际上这会有一些问题。
比较高级的Windows系统(哪个版本的操作系统很难讲,可能必须判定补丁号),符号链接也带有用户相关性。换句话说,如果一个普通用户创建了符号链接“\DosDevices\MyCDOSL”,那么,其实其他的用户是看不见这个符号链接的。
但是读者又会发现,如果在DriverEntry中生成符号链接,则所有用户都可以看见。原因是DriverEntry总是在进程“System”中执行。系统用户生成的符号链接是大家都可以看见的。
当前用户总是取决于当前启动当前进程的用户。实际编程中并不一定要在DriverEntry中生成符号链接。一旦在一个不明用户环境下生成符号链接,就可能出现注销然后换用户登录之后,符号链接“不见了”的严重错误。这也是常常让初学者抓狂几周都不知道如何解决的一个问题。
其实解决的方案很简单,任何用户都可以生成全局符号链接,让所有其他用户都能看见。路径“\DosDevices\MyCDOSL”改为“\DosDevices\Global\MyCDOSL”即可。
但是在不支持符号链接用户相关性的系统上,生成“\DosDevices\Global\MyCDOSL”这样的符号链接是一种错误。为此必须先判断一下。幸运的是,这个判断并不难。下面是一个例子,这个例子生成的符号链接总是随时可以使用,不用担心用户注销:


UNICODE_STRING device_name; 
UNICODE_STRING symbl_name; 
if (IoIsWdmVersionAvailable(1, 0x10)) 
{
    // 如果是支持符号链接用户相关性的版本的系统,用\DosDevices\Global.
    RtlInitUnicodeString(&symbl_name, L"\\DosDevices\\Global\\SymbolicLinkName");

else 
{
    // 如果是不支持的,则用\DosDevices
    RtlInitUnicodeString(&symbl, L"\\DosDevices\\SymbolicLinkName");
}
// 生成符号链接
IoCreateSymbolicLink(&symbl_name, &device_name);


第八章 处理请求


8.1 IRP与IO_STACK_LOCATION
开发一个驱动要有可能要处理各种IRP。但是本书范围内,只处理为了应用程序和驱动交互而产生的IRP。IRP的结构非常复杂,但是目前的需求下没有必要去深究它。应用程序为了和驱动通信,首先必须打开设备。然后发送或者接收信息。最后关闭它。这至少需要三个IRP:第一个是打开请求。第二个发送或者接收信息。第三个是关闭请求。
IRP的种类取决于主功能号。主功能号就是前面的说的DRIVER_OBJECT中的分发函数指针数组中的索引。打开请求的主功能号是IRP_MJ_CREATE,而关闭请求的主功能号是IRP_MJ_CLOSE。
如果写有独立的处理IRP_MJ_CREATE和IRP_MJ_CLOSE的分发函数,就没有必要自然判断IRP的主功能号。如果像前面的例子一样,使用一个函数处理所有的IRP,那么首先就要得到IRP的主功能号。IRP的主功能号在IRP的当前栈空间中。
IRP总是发送给一个设备栈。到每个设备上的时候拥有一个“当前栈空间”来保存在这个设备上的请求信息。读者请暂时忽略这些细节。下面的代码在MyDispatch中获得主功能号,同时展示了几个常见的主功能号:


NTSTATUS MyDispatchFunction(PDEVICE_OBJECT device,PIRP irp)
{
// 获得当前irp调用栈空间
PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);
NTSTATUS status = STATUS_UNSUCCESSFUL;
swtich(irpsp->MajorFunction)
{
// 处理打开请求
case IRP_MJ_CREATE:
……
break;
// 处理关闭请求
case IRP_MJ_CLOSE:
……
break;
// 处理设备控制信息
case IRP_MJ_DEVICE_CONTROL:
……
break;
// 处理读请求
case IRP_MJ_READ:
……
break;
// 处理写请求
case IRP_MJ_WRITE:
……
break;
default:

break;
}
return status;
}


用于与应用程序通信时,上面这些请求都由应用层API引发。对应的关系大致如下:


应用层调用的API 驱动层收到的IRP主功能号
CreateFile IRP_MJ_CREATE
CloseHandle IRP_MJ_CLOSE
DeviceIoControl IRP_MJ_DEVICE_CONTROL
ReadFile IRP_MJ_READ
WriteFile IRP_MJ_WRITE

了解以上信息的情况下,完成相关IRP的处理,就可以实现应用层和驱动层的通信了。具体的编程在紧接后面的两小节里完成。
8.2 打开与关闭的处理
如果打开不能成功,则通信无法实现。要打开成功,只需要简单的返回成功就可以了。在一些有同步限制的驱动中(比如每次只允许一个进程打开设备)编程要更加复杂一点。但是现在忽略这些问题。暂时认为我们生成的设备任何进程都可以随时打开,不需要担心和其他进程冲突的问题。
简单的返回一个IRP成功(或者直接失败)是三部曲,如下:
设置irp->IoStatus.Information为0。关于Information的描述,请联系前面关于IO_STATUS_BLOCK结构的解释。
设置irp->IoStatus.Status的状态。如果成功则设置STATUS_SUCCESS,否则设置错误码。
调用IoCompleteRequest (irp,IO_NO_INCREMENT)。这个函数完成IRP。
以上三步完成后,直接返回irp->IoStatus.Status即可。示例代码如下。这个函数能完成打开和关闭请求。


NTSTATUS
MyCreateClose(
    IN PDEVICE_OBJECT device,
    IN PIRP           irp)
{
irp->IoStatus.Information = 0;
    irp->IoStatus.Status = STATUS_SUCCESS;
    IoCompleteRequest (irp,IO_NO_INCREMENT);
    return irp->IoStatus.Status;
}


当然,在前面设置分发函数的时候,应该加上:


DriverObject->MajorFunctions[IRP_MJ_CREATE] = MyCreateClose;
DriverObject->MajorFunctions[IRP_MJ_CLOSE] = MyCreateClose;

在应用层,打开和关闭这个设备的代码如下:

HANDLE device=CreateFile("\\\\.\\MyCDOSL",
GENERIC_READ|GENERIC_WRITE,0,0,
OPEN_EXISTING,
FILE_ATTRIBUTE_SYSTEM,0);
   if (device == INVALID_HANDLE_VALUE)
   {
    // …. 打开失败,说明驱动没加载,报错即可
   }
   
   // 关闭
   CloseHandle(device);


8.3 应用层信息传入
应用层传入信息的时候,可以使用WriteFile,也可以使用DeviceIoControl。DeviceIoControl是双向的,在读取设备的信息也可以使用。因此本书以DeviceIoControl为例子进行说明。DeviceIoControl称为设备控制接口。其特点是可以发送一个带有特定控制码的IRP。同时提供输入和输出缓冲区。应用程序可以定义一个控制码,然后把相应的参数填写在输入缓冲区中。同时可以从输出缓冲区得到返回的更多信息。
当驱动得到一个DeviceIoControl产生的IRP的时候,需要了解的有当前的控制码、输入缓冲区的位置和长度,以及输出缓冲区的位置和长度。其中控制码必须预先用一个宏定义。定义的示例如下:


#define MY_DVC_IN_CODE \
(ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN, \
0xa01, \
METHOD_BUFFERED, \
FILE_READ_DATA|FILE_WRITE_DATA)


其中0xa01这个数字是用户可以自定义的。其他的参数请照抄。
下面是获得这三个要素的例子:

NTSTATUS MyDeviceIoControl(
PDEVICE_OBJECTdev,
PIRP irp)
{
// 得到irpsp的目的是为了得到功能号、输入输出缓冲
// 长度等信息。
PIO_STACK_LOCATION irpsp = 
IoGetCurrentIrpStackLocation(irp);
// 首先要得到功能号
ULONG code = irpsp->Parameters.DeviceIoControl.IoControlCode;
// 得到输入输出缓冲长度
ULONG in_len = 
irpsp->Parameters.DeviceIoControl.InputBufferLength;
ULONG out_len =
irpsp->Parameters.DeviceIoControl.OutputBufferLength;
// 请注意输入输出缓冲是公用内存空间的
PVOID buffer = irp->AssociatedIrp.SystemBuffer;

// 如果是符合定义的控制码,处理完后返回成功
if(code == MY_DVC_IN_CODE)
{
… 在这里进行需要的处理动作


// 因为不返回信息给应用,所以直接返回成功即可。
// 没有用到输出缓冲
irp->IoStatus.Information = 0;
irp->IoStatus.Status = STATUS_SUCCESS;
}
else
{
// 其他的请求不接受。直接返回错误。请注意这里返
// 回错误和前面返回成功的区别。
irp->IoStatus.Information = 0;
    irp->IoStatus.Status = STATUS_INVALID_PARAMETER;
}
    IoCompleteRequest (irp,IO_NO_INCREMENT);
    return irp->IoStatus.Status;
}


在前面设置分发函数的时候,要加上:


DriverObject->MajorFunctions[IRP_MJ_DEVICE_CONTROL] = MyCreateClose;
应用程序方面,进行DeviceIoControl的代码如下:


HANDLE device=CreateFile("\\\\.\\MyCDOSL",
GENERIC_READ|GENERIC_WRITE,0,0,
OPEN_EXISTING,
FILE_ATTRIBUTE_SYSTEM,0);
BOOL ret;
DWORD length = 0;// 返回的长度


   if (device == INVALID_HANDLE_VALUE)
   {
    // … 打开失败,说明驱动没加载,报错即可
   }
   
BOOL ret = DeviceIoControl(device,
MY_DVC_IN_CODE,// 功能号
in_buffer, // 输入缓冲,要传递的信息,预先填好
in_buffer_len,// 输入缓冲长度
NULL, // 没有输出缓冲
0, // 输出缓冲的长度为0
&length, // 返回的长度
NULL);
   
   if(!ret)
   {
    // … DeviceIoControl失败。报错。
   }
   
   // 关闭
   CloseHandle(device);

8.4 驱动层信息传出
  驱动主动通知应用和应用通知驱动的通道是同一个。只是方向反过来。应用程序需要开启一个线程调用DeviceIoControl,(调用ReadFile亦可)。而驱动在没有消息的时候,则阻塞这个IRP的处理。等待有信息的时候返回。
有的读者可能听说过在应用层生成一个事件,然后把事件传递给驱动。驱动有消息要通知应用的时候,则设置这个事件。但是实际上这种方法和上述方法本质相同:应用都必须开启一个线程去等待(等待事件)。而且这样使应用和驱动之间交互变得复杂(需要传递事件句柄)。这毫无必要。
让应用程序简单的调用DeviceIoControl就可以了。当没有消息的时候,这个调用不返回。应用程序自动等待(相当于等待事件)。有消息的时候这个函数返回。并从缓冲区中读到消息。
实际上,驱动内部要实现这个功能,还是要用事件的。只是不用在应用和驱动之间传递事件了。
驱动内部需要制作一个链表。当有消息要通知应用的时候,则把消息放入链表中(请参考前面的“使用LIST_ENTRY”),并设置事件(请参考前面的“使用事件”)。在DeviceIoControl的处理中等待事件。下面是一个例子:这个例子展示的是驱动中处理DeviceIoControl的控制码为MY_DVC_OUT_CODE的部分。实际上驱动如果有消息要通知应用,必须把消息放入队列尾并设置事件g_my_notify_event。MyGetPendingHead获得第一条消息。请读者用以前的知识自己完成其他的部分。


NTSTATUS MyDeviceIoCtrlOut(PIRP irp,ULONG out_len)
{
MY_NODE *node;
ULONG pack_len;
// 获得输出缓冲区。
PVOID buffer = irp->AssociatedIrp.SystemBuffer;


// 从队列中取得第一个。如果为空,则等待直到不为空。
while((node = MyGetPendingHead()) == NULL)
{
KeWaitForSingleObject(
&g_my_notify_event,// 一个用来通知有请求的事件
Executive,KernelMode,FALSE,0);
}


// 有请求了。此时请求是node。获得PACK要多长。
pack_len = MyGetPackLen(node);
if(out_len < pack_len)
{
irp->IoStatus.Information = pack_len;// 这里写需要的长度
    irp->IoStatus.Status = STATUS_INVALID_BUFFER_SIZE;
    IoCompleteRequest (irp,IO_NO_INCREMENT);
    return irp->IoStatus.Status;
}


// 长度足够,填写输出缓冲区。
MyWritePackContent(node,buffer);
// 头节点被发送出去了,可以删除了
MyPendingHeadRemove ();
// 返回成功
irp->IoStatus.Information = pack_len;// 这里写填写的长度
    irp->IoStatus.Status = STATUS_SUCCESS;
    IoCompleteRequest (irp,IO_NO_INCREMENT);
    return irp->IoStatus.Status;
}


这个函数的处理要追加到MyDeviceIoControl中。如下:


NTSTATUS MyDeviceIoControl(
PDEVICE_OBJECTdev,
PIRP irp)
{

if(code == MY_DVC_OUT_CODE)
return MyDeviceIoCtrlOut(dev,irp);

}

在这种情况下,应用可以循环调用DeviceIoControl,来取得驱动驱动通知它的信息。



原创粉丝点击