2.3重要的数据结构
2.3.1驱动对象:
Windows内核认为许多东西都是“对象”,比如一个驱动、一个设备、一个文件,甚至其他的一些东西。(采用面对对象的编程方式,但是使用的是C语言)
一个驱动对象代表了一个驱动程序,或者说一个内核模块。
驱动对象的结构如下:
typdef struct _DRIVER_OBJECT {
// 结构的类型和大小
CSHORT Type;
CSHORT Size;
// 设备对象,这里实际上是一个设备对象的链表的开始。因为DeviceObject
// 中有相关的链表信息。阅读下一小节“设备对象”会得到更多的信息
PDEVICE_OBJECT DeviceObject;
...
// 这个内核模块再内核空间中的开始地址和大小
PVOID DriverStart;
ULONG DriverSize;
...
// 驱动的名字
UNICODE_STRING DriverName;
...
// 快速IO分发函数
PFAST_IO_DISPATCH FastIoDispatch;
...
// 驱动的卸载函数
PDRIVER_UNLOAD DriverUnload;
// 普通分发函数
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT;
⑴如果写一个驱动程序(或者说内核模块),要在Windows中加载,则必须填写这样一个结构,来告诉Windows 程序提供哪些功能。
⑵编写应用程序时,Windows直接从main()函数开始执行来生成一个进程。
内核模块并不生成一个进程,而是填写一组回调函数让Windows来调用,而且这组回调函数必须符合Windows内核规定。
⑶这组
回调函数包括上面的“普通分发函数”和“快速IO分发函数”。这些函数用来处理发送给这个内核模块的请求。一个内核模块所有的功能都由它们提供给Windows。
⑷如果编写内核程序,能找到这些关键的驱动对象结构(比如NTFS文件系统),然后改写下面的分发函数,替换成我们自己的函数,可能就可以捕获Windows的文件操作,让我们的内核程序处理完毕后,再交给NTFS文件系统处理。这样就可以加入我们自己的功能(比如扫描病毒,文件加密等)。这就是所谓的
分发函数Hook技术,本书后面会有所描述。
2.3.2设备对象:
important: 在内核世界里,大部分“消息”都以请求(IRP)的方式传递。而设备对象(DEVICE_OBJECT)是唯一可以接受请求的实体,任何一个“请求”(IRP)都是发送给某个设备对象的。
设备对象的结构是DEVICE_OBJECT,简称DO。
因为我们总是在内核程序中生成一个DO,而一个内核程序是用一个驱动对象表示的,所以一个设备对象总是属于一个驱动对象。
typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT)
_DEVICE_OBJECT {
// 结构的类型和大小
CSHORT Type;
USHORT Size;
// 引用计数
ULONG ReferenceCount;
// 这个设备所属的驱动对象
struct _DRIVER_OBJECT* DriverObject;
// 下一个设备对象。在一个驱动对象中有n个设备,
//
这些设备用这个指针连接
起来作为一个单向的链表。
struct _DEVICE_OBJECT* NextDevice;
// 设备类型
DEVICE_TYPE DeviceType;
// IRP栈大小
HAR StackSize;
...
} DEVICE_OBJECT;
驱动对象生成多个设备对象。
当Windows向设备对象发送请求时,这些请求被驱动对象的分发函数所捕获。当Windows内核向一个设备发送一个请求时,驱动对象的分发函数中的某一个会被调用。分发函数原型如下:
// 一个典型的分发函数,第一个参数device是请求的目标设备
// 第二个参数irp是请求的指针
NTSTATUS MyDispatch(PDEVICE_OBJECT device, PIRP irp);
2.3.3请求:
⑴何为请求?举一个浅显的例子:如果要求网卡发送一个数据包,或者向网卡请求把已经存在缓冲区里接收到的包读出来,这就是一个请求;如果读取一个文件从0开始的512个字节,这也是一个请求;如果在磁盘的64MB位置写入长达512字节的一组数据,这还是一个请求。
⑵应用程序的开发者是看不到这些请求的,只需调用API即可。但是这些操作最终在内核中会被IO管理器翻译成请求(IRP或者与之等效的其他形式,比如快速IO调用)发送往某个设备对象。
⑶
大部分请求以IRP的形式发送。IRP也是一个内核数据结构,非常复杂,因为这个结构要表示无数种实际请求的缘故。在WDK的wdm.h中能找到IRP的结构如下:
typedef struct _IRP {
// 类型和大小
CSHORT
Type;
USHORT
Size;
// 内存描述符链表指针。实际上,这里用来描述一个缓冲区。可以想象
// 一个内核请求一般都需要一个缓冲区(如读硬盘需要有读出缓冲区)
...
// 下面这个共用体中也有一个SystemBuffer。这是比MdlAddress稍微简单
// 的表示缓冲区的一种方式。IRP用MdlAddress还是用SystemBuffer取决于
// 这次请求的IO方式。总之二者都有可能。
union {
struct _IRP *
MasterIrp;
__volatile LONG
IrpCount;
PVOID
SystemBuffer;
}
AssociatedIrp;
.
// IO状态,一般请求完成之后的返回情况放在这里
// IRP栈空间大小
CHAR
StackCount;
// IRP当前栈空间
CHAR
CurrentLocation;
...
// 用来取消一个未决请求的函数
// 与MdlAddress和SystemBuffer一样都可以表示缓冲区,
// 但是缓冲区的特性稍有不同。以后再详细解释
PVOID
UserBuffer;
union {
struct {
.
.
union {
KDEVICE_QUEUE_ENTRY
DeviceQueueEntry;
struct {
PVOID
DriverContext[4];
};
};
.
// 发出这个请求的线程
PETHREAD
Thread;
.
.
LIST_ENTRY
ListEntry;
.
.
}
Overlay;
.
.
}
Tail;
} IRP, *PIRP;
⑷这里值得注意的是
IRP栈空间。
因为一个IRP往往需要传递n个设备才得以完成(第三章有描述)。可以想象,在传递过程中,有可能又些“中间变换”,导致请求的参数变化。为了保存这种参数变化,我们给每次“中转”都留一个“栈空间”,用来保存中间参数。所以一个请求并非简单的一个输入,并等待一个输出,而是经过许多中转才得以完成。而且在中转的每个步骤,输入都可以改变,所以可变部分的输入信息保存在一个栈似的结构中。每中转一次,都使用其中一个位置。域CurrentLocation表示当前使用了哪一个。
⑸本书后面内容所使用的词语“请求”如不特殊说明,就是指IRP。衍生说法如下:
生成请求:主功能号为 IRP_MJ_CREATE 的 IRP
查询请求:主功能号为 IRP_MJ_QUERY_INFORMATION 的 IRP
设置请求:主功能号为 IRP_MJ_SET_INFORMATION 的 IRP
控制请求:主功能号为 IRP_MJ_DEVICE_CONTROL,或者是仅仅在本书的第10章TDI过滤中出现的 IRP_MJ_INTERAL_DEVICE_CONTROL 的 IRP
关闭请求:主功能号为 IRP_MJ_CLOSE 的 IRP
请求指针:IRP 的指针。类型写作 PIRP 或者 IRP*。
2.4函数调用
2.4.1查阅帮助:
查阅WDK Documentation
2.4.2帮助中有的几类函数:
⑴大部分内核API都有前缀,主要的函数以Io-,Ex-,Rtl-,Ke-,Zw-,Nt-和Ps-开头。此外,与NDIS网络驱动开发的相关函数几乎都是以Ndis-开头的,与开发WDF驱动相关的函数都是以Wdf-开头的。
⑵部分函数相关介绍:看课本P30
有分配内存、获取互斥体
文件操作、注册表操作
字符串操作、获取当前Windows版本
IO管理器操作、IRP操作
进程线程操作
2.4.3帮助中没有的函数:
⑴并不是所有可以调用的函数都在帮助里。比如C运行时库中的 stdlib.h stdio.h 和 memory.h 三个头文件里有很多函数可以使用。也并非全部,比如printf,scanf,fopen,fclose,fwrite,fread就不行,因为内核里没有控制台,而且读/写文件也不是那么轻松。但是,如spirntf,strlen,strcpy,wcslen,wcscpy,memcpy,memset都是可以的,相应的malloc,free,strdup是不行的。
⑵基本上可以认为,C运行时库中的函数,如果
只涉及字符串和内存数据(不涉及内存管理,比如内存的分配和释放),则是可以在内核程序里调用的。但是MS
不提倡这样做。
⑶如果函数涉及内存管理,文件操作,网络操作,线程等。则往往头文件中有这个函数存在,甚至编译可以通过,但是连接的时候会出问题。
2.5 Windows的驱动开发模型
⑴“模型”源于单词“Mode”
⑵在Windows NT上,驱动程序被称为 Kernel Driver Mode 驱动程序。
⑶在Windows 9x上的驱动程序,都叫做VXD。
⑷Windows 98~2000这个时期出现的新模型叫做WDM。
⑸Windows的驱动模型概念,本来是就驱动程序的行为而言的。比如
WDM驱动,必须要满足n种被要求的特性(如电源管理、即插即用)才被称为WDM驱动。如果不提供这些功能,那么统一称为NT式驱动。
⑹本书采用简单的区分方法。将一切在Windows 2000 ~ Windows Vista下能正常运动且未调用WDF相关的内核API函数的驱动都称为传统型驱动(包括NT式和WDM)。如果调用了WDF相关的内核API则称为
WDF驱动。
WDF驱动是可以调用传统型驱动所调用的内核API的,WDF可以视为传统型的升级版。
WDF与其说是新的驱动开发模型,还不如说是在已有的内核API和数据结构的基础上,又封装出一套让使用者觉得更简单、更易用的 Wdf- 开头的一组API。从KDM到WDM再到WDF是一脉相承的。