标题: 【原创】Windows驱动程序框架windows驱动程序入门比较坑爹一点,本文旨在降低入门的门槛。
注:下面的主要以NT式驱动为例,部分涉及到WDM驱动的差别会有特别说明。
首先,肯定是配置好对应的开发环境啦,不懂的就百度下吧,这里不再次描述了。
在Console控制台下,我们的有一个入口函数main;在Windows图形界面平台下,有另外一个入口函数Winmain。
我们只要在这入口函数里面调用其他相关的函数,程序就会按照我们的意愿跑起来了。
在我们用IDE开发的时候,也许你不会发现这些细微之处是如何配置出来的,一般来说我们也不用理会,因为在新建工程的时候,IDE已经帮我们把编译器(Compiler)以及连接器(Linker)的相关参数设置好,在正式编程的时候,我们只要按照规定的框架编程就行了。
同样,在驱动程序也有一个入口函数DriverEntry,这并不是一定的,但这是微软默认的、推荐使用的。
在我们配置开发环境的时候我们有机会指定入口函数,这是链接器的参数/entry:"DriverEntry"。
入口函数的声明代码:DriverEntry主要是对驱动程序进行初始化工作,它由系统进程(System)创建,系统启动的时候System系统进程就被创建了。
驱动加载的时候,系统进程将会创建新的线程,然后调用执行体组件中的对象管理器,创建一个驱动对象(DRIVER_OBJECT)。
另外,系统进程还得调用执行体组件中的配置管理程序,查询此驱动程序在注册表中对应项。
系统进程在调用驱动程序的Driv erEntry的时候就会将这两个值传到pDriverObject和pRegistryPath。
接下来,我们介绍下上面出现的几个数据结构:typedef LONG NTSTATUS在驱动开发中,我们应习惯于用NTSTATUS返回信息,NTSTATUS各个位有不同的含义,我们可以也应该用宏NT_SUCCESS来判断是否返回成功。
代码:NTSTAUS的编码意义:其中Ser是Serviity的缩写,代表严重程度。
00:成功01:信息10:警告11:错误C是Customer的缩写,代表自定义的位。
Facility:设备位Code:设备的状态代码。
根据这定义编码,还有补码的概念,那么只要是错误的时候,最高位就是1,NTSTATUS的值就是负数,所以可以大于零来判断,但无论如何都希望读者用NT_SUCCESS宏来判断是否成功,因为这可能在以后会有所改动,即使这么多年来都一直沿用着。
同样的,微软也为我们定义了其他几个判断宏:代码:有了之前的介绍,这三个相信不说大家也能领会了。
但最常用的还是NT_SUCCESS。
我们继续说其他的两个数据结构,先说PUNICODE_STRING吧,P代表这是一个指针类型,指向一个UNICODE_STRING结构。
宽字符串结构体(UNICODE_STRING)代码:其中,ØLength:Unicode字符串当前的字符长度。
注意不是字节数,每个Unicode字符占用两个字节。
ØMaximumLength:该Unicode字符串的最大容纳长度。
ØBuffer:Unicode字符串的缓冲地址。
UNICODE_STRING是Windows驱动开发里面经常用到的一个结构,用Length来标记字符串的长度而不再用\0来表示结束。
可以用RtlInitUnicodeString来对其初始化,但这里的pRegistryPath是直接由创建驱动程序的线程传进来的参数,如果在接下来仍需要用到该值,最好是使用RtlCopyUnicodeString函数将其值另外保存下来,因为这个字符串并不是长期存在的,DriverEn try函数返回的时候可能就会被销毁了。
PDRIVER_OBJECT,P代表这是一个指针类型,指向一个驱动对象(DRIVER_OBJECT),每个驱动程序都有一个驱动对象。
这是一个半透明的数据结构,微软没有公开它的完全定义,只是有提到几个成员,但我们依旧可以通过WinDbg看到它的定义,只是不同的系统可能会存在不同的结构。
不过我另外在WDK的头文件WDM.h里面发现了它的定义:驱动对象(DRIVER_OBJECT)代码:这里提下几个比较重要的字段,ØDeviceObject:指向由此驱动创建的设备对象。
每个驱动程序都会有一个或多个的设备对象。
其中,每个设备对象都会有一个指针指向下一个设备对象,这在我们介绍设备对象的时候再继续说。
ØDriverName:驱动的名字,该字符串一般为\Driver\[驱动程序名称]。
ØHardwareDatabase:记录设备的硬件数据库键名。
该字符串一般为"\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services \[服务名]"。
ØFastIoDispatch:指向快速I/O函数入口,是文件驱动中用到的排遣函数。
ØDriverStartIo:记录StartIo例程的函数地址,用于串行化操作。
ØDriverUnload:指定驱动卸载时所用的回调函数地址。
ØMajorFunction:这是一个函数指针数组,每个指针指向的是一个函数,该函数就是处理相应IRP的排遣函数,数组的索引值与IRP_MJ_XXX相对应。
我们已经了解了DriverEntry函数头的那个数据结构了,但这还不够,在DriverEntry里,我们主要是对驱动程序进行初始化,这就涉及到其他的一些数据结构了,下面我们继续逐一地介绍。
设备对象(DEVICE_OBJECT)代码:这里只对几个比较重要的字段进行说明:ØDriverObject:指向创建此设备对象的驱动程序对象。
同属于一个驱动程序的设备对象指向的是同一个驱动对象。
ØNextObject:指向同一个驱动程序创建的下一个设备对象。
同一个驱动对象可以创建若干个设备对象,每个设备对象根据N extDevice来连成一个链表,最后一个设备对象的NextDevice域为NULL。
ØAttachedDevice:指向附加到此设备对象之上的最近设备对象。
这里需要理解分层驱动程序的概念。
ØDeviceExtension:指向设备的扩展对象。
每个设备都会指定一个设备扩展对象,这个数据结构由驱动程序开发者自行定义,可以用来记录一些与设备相关的一些信息,同时应尽量避免使用全局变量,将数据存放在设备扩展里,具有很大的灵活性。
ØCurrentIrp:在使用StartIO例程的时候,该成员指向的是当前IRP结构。
ØFlags:指定了该设备对象的标记。
下面列出了常用的几个标记:ØDeviceType:指定设备的类型。
一般在开发虚拟设备时,选择FILE_DEVICE_UNKNOW。
其他的请自行参考WDK文档。
ØStackSize:在多层驱动情况下,驱动与驱动之间会形成类似堆栈的结构,称之为设备栈。
IRP会依次从最高层传递到最底层。
StackSize描述的就是该层数。
最底层的设备层数为1。
ØAlignmentRequirement:在进行大容量传输的时候,往往需要进行内存对齐,以保证传输速度。
请使用类似FILE_XXX_AL IGNMENT的方式进行赋值。
下面给大家展示一下DriverEntry的最基本框架:代码:这是用C++写的,所以必要的地方加上了extern“C”,否则会引起一些错误,这是因为C++与C在进行名称粉碎的时候处理得不一样,C++这个改进主要是为了实现一些高级功能,比如多态。
虽然加上extern “C”会有点麻烦,但可以用上C++那些功能,个人觉得也有所值。
如果用C,直接忽略上面的extern “C”。
NTDDK.h是NT式驱动需要加载的头文件,如果是WDM式驱动,那么加载的是WDM.h#define INITCODE code_seg("INIT")定义一个宏,#prama INITCODE还原后就是#pramacode_seg(“INIT”),表示接下来的代码加载到INIT内存区域中,成功加载后,可以退出内存。
对于DriverEntry这种一次性的函数而言,这是最适合的选择,可以节省内存。
函数结束后需要显式地切换回来,如:#prama LOCKEDCODE。
同样,PAGECODE表示分页内存,作用是将此部分代码放入分页内存中运行,在里面的代码切换进程上下文时可能会被换回分页文件。
LOCKEDCODE表示默认内存,也就是非分页内存,里面的代码常驻内存。
IRQL处于DISPATCH_LEVEL或者以上的等级,必须处于非分页内存里面。
同理,对于数据段也有同样的机制,于是有了PAGEDATA、LOCKEDDATA、INITDATA。
KdPrint是一个宏,在调试版本(Checked)里面(具备DBG宏定义),有代码:而在正式版本(Free)里面,KdPrint被定义为空。
所以可以用来作为调试输出。
但注意Kdprint后面是两层括号,用法与C语言运行库的printf差不多。
pDriverObject->DriverUnload = UnloadRoutine;将卸载例程函数告诉驱动对象,驱动对象在前面已经有定义,这里不做深入讨论。
pDriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchRoutine;注册排遣例程。
Windows是消息驱动,而驱动程序是IRP驱动的,I/O管理器将发送到驱动的“消息”封装在IRP里面,驱动程序也将结果告诉IRP。
类似windows的消息机制,对于不同的“消息”,驱动程序需要注册不同的处理例程来区别对待,当然也可以放在同一个例程里面,然后用sw itch语句来区别对待,但当处理过程比较长的时候,会比较凌乱。
IRP_MJ_CREATE是当RING3应用程序在使用CreateFile函数建立与驱动程序的通信通道时所激活的。
IRP_MJ_READ是ReadFile,IRP_MJ_WRITE是WriteFile,而IRP_MJ_CLOSE是CloseHandle关闭文件句柄的时候产生的。
小知识:对于WDM式驱动,仍需要注册AddDevice例程,pDriverObject->DriverExtension->AddDevice = WDMAddDeviceRou tine,设备对象的初始化将在AddDevice里面进行而不是DriverEntry。
另外还需要注册IRP_MJ_PNP排遣函数。