7.2 获得实际数据
这一章我们一直都在开发一个可以捕获串口上的数据的过滤程序。现在虚拟设备已经绑定了真正的串口设备,那么如何从虚拟设备得到串口上流过的数据呢?答案是根据“请求”。操作系统将请求发送给串口设备,请求中就含有要发送的数据,请求的回答中则含有要接收的数据。下面分析这些“请求”,以便得到实际的串口数据流。
7.2.1 请求的区分
Windows内核的开发者确定了很多数据结构,在前面的内容中我们逐渐地与DEVICE_OBJECT(设备对象)、FILE_OBJECT(文件对象)和DRIVER_OBJECT(驱动对象)见了面。文件对象暂时没有什么应用(但是在本书后面的文件系统过滤中,文件对象是极为重要的)。读者需要了解以下内容。
(1)每个驱动程序只有一个驱动对象。
(2)每个驱动程序可以生成若干个设备对象,这些设备对象从属于一个驱动对象。在一个驱动中可否生成从属于其他驱动的驱动对象的设备对象呢?从IoCreateDevice的参数来看,这样做是可以的,但是笔者没有尝试过这样的应用。
(3)若干个设备(它们可以属于不同的驱动)依次绑定形成一个设备栈,总是最顶端的设备先接收到请求。
请注意,IRP是上层设备之间传递请求的常见数据结构,但绝对不是唯一的数据结构。传递请求还有很多其他的方法,不同的设备也可能使用不同的结构来传递请求。但在本书中,在90%的情况下,请求与IRP是等价概念。
串口设备接收到的请求都是IRP,因此只要对所有的IRP进行过滤,就可以得到串口上流过的所有数据。串口过滤时只需要关心两种请求:读请求和写请求。对于串口而言,读指的是接收数据,而写指的是发出数据。串口也还有其他的请求,比如打开和关闭、设置波特率等。但是我们的目标只是获得串口上流过的数据,而不关心打开/关闭和波特率是多少这样的问题,所以一概无视这类问题。
请求可以通过IRP的主功能号进行区分。IRP的主功能号是保存在IRP栈空间中的一个字节,用来标识这个IRP的功能大类。相应地,还有一个次功能号来标识这个IRP的功能细分小类。
读请求的主功能号为IRP_MJ_READ,而写请求的主功能号为IRP_MJ_WRITE。下面的方法用于从一个IRP指针得到主功能号(这里的变量irp是一个PIRP,也就是IRP的指针)。
7.2.2 请求的结局
对请求的过滤,最终的结局有三种。
(1)请求被允许通过了。过滤不做任何事情,或者简单地获取请求的一些信息。但是请求本身不受干扰,这样系统行为不会有变化,皆大欢喜。
(2)请求直接被否决了。过滤禁止这个请求通过,这个请求被返回了错误,下层驱动程序根本收不到这个请求。这样系统行为就变了,后果是常常看见上层应用程序弹出错误框提示权限错误或者读取文件失败之类的信息。
(3)过滤完成了这个请求。有时有这样的需求,比如一个读请求,我们想记录读到了什么。如果读请求还没有完成,那么如何知道到底会读到什么呢?只有让这个请求先完成再去记录。过滤完成这个请求时不一定要原封不动地完成,这个请求的参数可以被修改(比如把数据都加密)。
当过滤了一个请求时,就必须把这个请求按照上面三种方法之一进行处理。当然,这些代码会写在一个处理函数中。如何使用这个处理函数将在后面的内容中进行介绍,这里先介绍这些处理方法的代码应该怎么写。
串口过滤要捕获两种数据:一种是发送出的数据(也就是写请求中的数据);另一种是接收的数据(也就是读请求中的数据)。为了简单起见,我们只捕获发送出的数据,这样,只需要采取第一种处理方法即可。至于第二、三种处理方法,读者会在后面的许多过滤程序中碰到。
这种处理最简单。首先调用IoSkipCurrentIrpStackLocation跳过当前栈空间;然后调用IoCallDriver把这个请求发送给真实的设备。请注意,因为真实的设备已经被过滤设备绑定,所以首先接收到IRP的是过滤设备的对象。代码如下(irp是过滤到的请求):
7.2.3 写请求的数据
一个写请求(也就是串口一次发送出的数据)保存在哪里呢?回忆前面关于IRP结构的描述,一共有三个地方可以描述缓冲区:irp→MDLAddress、irp→UserBuffer和irp→AssociatedIrp.SystemBuffer。不同的IO类别,IRP的缓冲区不同。SystemBuffer是一般用于比较简单且不追求效率情况的解决方案:把应用层(R3层)内存空间中的缓冲数据拷贝到内核空间。
UserBuffer则是最追求效率的解决方案。应用层的缓冲区地址直接放在UserBuffer里,在内核空间中访问。在当前进程和发送请求进程一致的情况下,内核访问应用层的内存空间当然是没错的。但是一旦内核进程已经切换,这个访问就结束了,访问UserBuffer当然是跳到其他进程空间中了。因为在Windows中,内核空间是所有进程共用的,而应用层空间则是各进程隔离的。
一个更简单的解决方案是把应用层的地址空间映射到内核空间,这需要在页表中增加一个映射。当然不需要编程者手工去修改页表,通过构造MDL就能实现这个功能。MDL可以翻译为“内存描述符链”,但是本书按照业界传统习惯一律称之为MDL。IRP中的MDLAddress域是一个MDL的指针,从这个MDL中可以读出一个内核空间的虚拟地址。这就弥补了UserBuffer的不足,同时比SystemBuffer的完全拷贝方法要轻量,因为这个内存实际上还是在老地方,没有拷贝。
回到串口的问题上,那么串口写请求到底用的是哪种方式呢?笔者并不清楚也没有去调查到底是哪种方式。但是如果用下面的编码方式,无论采用哪种方式,都可以把数据正确地读出来。
这其中涉及了MmGetSystemAddressForMdlSafe函数,有兴趣的读者可以在WDK的帮助中查阅一下这个函数的含义。同时也可以深入了解一下MDL,但是对阅读本书的重要性不是很明显。本书的后面在涉及从MDL得到系统空间虚拟地址的情况下,都简单地调用MmGetSystem AddressForMdlSafe。
此外是缓冲区有多长的问题。对于一个写操作而言,长度可以通过如下方式获得: