因为工作原因,需要做一些与网卡有关的测试,其中涉及到了驱动这一块的知识,虽然程序可以运行,但是不搞清楚,心里总是不安,觉得没理解清楚。因此想看一下驱动开发。查了很多资料,看到有人推荐Windows驱动开发技术详解这本书,因此本篇文章也是基于这本书进行学习的。有些图片也是按照书上自己画的。
Windows操作系统示意图
首先,需要下载相应的工具,将环境搭建起来,VS和WDK,由于我已经安装了VS2017,所以需要找对应版本的WDK(方法)。如果想要查OS的版本,可以WIN+R输入winver就可以看到OS的版本了,
老版本对应链接:https://docs.microsoft.com/zh-cn/windows-hardware/drivers/other-wdk-downloads
安装好了后就需要写一下程序了,参考链接:https://blog.csdn.net/liny000/article/details/81260385
Windows架构简图
Win32子系统将API函数转化为Native API函数。在Native API接口中,已经没有了子系统的概念,它将这种调用转化为系统服务函数的调用。其中,Native API穿过了用户模式和内核模式的界面,达到了内核模式。系统服务函数通过I/O管理器将消息传递给驱动程序。
在内核模式下,执行体组件提供了大量的内核函数供驱动程序调用。内核主要负责进程、线程的调度情况。驱动程序通过硬件抽象层与具体硬件进行操作。
Windows API分为三类,分别是USER函数、GDI函数和KERNEL函数。
》USER函数:这类函数管理窗口、菜单、对话框和控件。
》GDI函数:这类函数在物理设备商执行绘图操作。
》KERNEL函数:这类函数管理非GUI资源,例如:进程、线程、文件和同步服务等。
可以发现Windows系统目录中有对应的三个系统文件,分别是USER32.dll、GDI32.dll和KERNEL32.dll。这三个文件提供了以上三类API的接口。当应用程序加载的时候,操作系统出了将应用程序加载到内存中,同时将以上三个DLL文件加载到内存中。
1、Native API
大部分Win32子系统的API,都通过Native API实现的。Native API的函数一般都是在Win32API上加上Nt两个字母。例如,CreateFile函数对应着NtCreateFile函数。所有Native API都是在Ntdll.dll中实现的。以上三个Win32子系统的核心dll文件都是依赖于Ntdll.dll的。
在Win32的底下设置一层Native API的调用,是基于版本兼容的考虑。Win32 API从Windows NT到Windows 2000,再到Windows XP,基本保持一致,变化的只是Native API。作为应用程序的开发者,只需了解Win32的API,而不用关心Native API的变化。这种机制,可以让WindowsNT上的程序直接在更高版本的Windows上运行,而不用重新编译。
2、I/O管理器
I/O管理器负责发起I/O请求,并且管理这些请求。它由一系列内核模式下的例程所组成,这些例程为用户模式下的进程提供了统一接口。I/O管理器的目标是使来自用户模式的I/O请求独立于设备。
无论是对端口的读写、对键盘的访问,还是对磁盘文件的操作都统一为IRP(I/O Request Packages)的请求形式。其中IRP包含了对设备操作的重要数据,例如是读操作还是写操作、读多少字节、写多少字节,是直接读到用户进程中,还是先读到系统缓冲中,在读到用户进程中等。
IRP被传递到具体设备的驱动程序中,驱动程序负责“完成”这些IRP,并将完成的状态按原路返回到用户模式下的应用程序中。实际上,I/O管理器担当着用户模式代码和设备驱动程序之间的接口。
3、配置管理程序
在Windows上,配置管理程序记录所有计算机软件、硬件的配置信息。它使用一个被称为注册表(Registry)的数据库保存这些数据。设备驱动程序根据注册表中的信息进行加载。
另外,驱动程序还会从注册表中提取相应的参数,这样可以提高驱动程序的灵活性。例如:设备操作的延时时间,可以作为参数写进注册表。驱动程序加载的时候读取该值,而不是将延时时间在编写程序的时候写成定值。
4、驱动程序
I/O管理器接收应用程序的请求后,创建相应的IRP,并传送至驱动程序进行处理,有如下几种处理的方法。
(1)根据IRP的请求,直接操作具体硬件,然后完成此IRP,并返回。
(2)将此IRP的请求,转发到更底层的驱动中去,并等待底层驱动的返回。
(3)接受到IRP请求,不是急于完成。而是分配新的IRP发到其他驱动程序中,并等待返回。
驱动程序处理IRP的过程往往不是单独操作,而是将以上几种操作结合在一起。
5、内核
内核被认为是Windows操作系统的心脏。Windows的内核从执行体组件分割出来。和执行体组件相比,内核是非常小的。内核为执行体组件提供最基本的支持,它负责提供进程和线程的调度,通过自旋锁(Spin Lock)提供对多CPU同步支持,提供中断处理等。
内核提供了以下功能:
》对内核对象的支持。
》对线程的调度。
》对多处理器同步的支持。
》中断处理函数的支持。
》对错误陷阱的支持。
》对其他硬件特殊功能的支持。
Windows内核执行在最高的特权之上,它被设计成可以并行地运行在多处理器商。内核在调度线程的时候不能被其他线程所打断,即不能允许线程的切换。但是内核可以被更高的中断请求级别(IRQL)所打断。
从应用程序到驱动程序
打开Windows的设备管理器,可以发现这里罗列着计算机里安装的所有设备,这些设备有的是真实的物理设备。例如:网卡设备、显卡设备等。有些设备则是虚拟设备。例如,自己编写的驱动,它没有对应着PC的任何设备,而完全是虚拟出来的“假”设备。虚拟光驱也是这样的虚拟设备。还有些设备介于真实物理设备和虚拟设备之间,比如磁盘的卷设备,磁盘对应磁盘设备,磁盘上的分区又会产生卷设备,这个完全是逻辑上的概念,对卷设备的所有操作,全部会转化成磁盘设备的操作。
设备分类 | 功能 |
文件设备 | 对存储文件的操作 |
目录设备 | 对目录的操作 |
逻辑磁盘设备 | 对逻辑磁盘的操作 |
物理磁盘设备 | 对物理磁盘的操作 |
串口设备 | 对诸如串口鼠标自、串口Modem设备的操作 |
并口设备 | 对诸如并口打印机的操作 |
PC上的设备千差万别,所实现的功能完全不同,如何用统一的接口操作不同的设备,是一个很麻烦的问题。Windows的设计者们为了简化对不同设备的操作,实现对不同设备统一接口,将所有设备以普通文件看待。也就是说在Windows中,无论何种设备,都用操作文件的办法去操作设备。
对所有设备的操作统一成和文件操作一样的操作,这一方法非常巧妙。文件操作和设备操作有很多类似的地方。例如,二者都有打开、关闭、读、写、取消等操作。下表列举了文件操作和设备操作所使用的的Win32 API函数。
Win32 API | 对文件操作 | 对设备操作 |
CreateFile | 打开或创建文件 | 打开或创建设备 |
CloseHandle | 关闭文件 | 关闭设备 |
ReadFile | 读文件 | 读设备 |
WriteFile | 写文件 | 写设备 |
CancelIO | 取消读写文件操作 | 取消读写设备操作 |
DeviceIoControl | (无) | 对设备进行特殊操作 |
下面更深入的介绍一下,Win32 API是如何一步步对设备驱动程序进行读写操作的。
下图是对Windows架构简图的简化,可以很清晰地看到应用程序到驱动程序是怎样操作设备的。
这里以CreateFile API为例,其他操作设备的API类似。首先应用程序调用CreateFile API,这个API是由Win32子系统的三大模块中的Kernel32.dll实现的。CreateFile函数会调用Ntdll.dll中的NtCreateFile函数,其中NtCreateFile是未文档化的函数,程序员最好不要直接使用这个未文档化的函数。
NtCreateFile的作用是穿越用户模式的边界,进入到内核模式,这个步骤是通过软中断实现的。进入到内核模式后,会调用系统的服务函数,这里会调用同名的系统服务NtCreateFile。(这里很容易搞混,虽然都叫NtCreateFile,但一个是位于用户模式的Native API,另一个是位于内核模式的系统服务调用)
NtCreateFile系统函数调用通过I/O管理器,创建IRP并传输到设备的驱动程序中。IRP(I/O Request Package)即输入输出请求包,是驱动程序开发中重要的数据结构。驱动程序的运行,完全是靠IRP驱动的。下面会有对IRP的专门介绍,这里可以将IRP理解为一个消息。这个消息告诉驱动程序,是需要读操作还是写操作。
驱动程序根据IRP,进行相应的操作。这些操作一般是对设备的直接操作,例如对端口的读操作。对端口的读操作根据不同的硬件平台,实现方法会有所不同,Windows根据不同的硬件平台,会有不同的硬件抽象层(HAL)。硬件抽象层提供一组宏,如READ_PORT_BUFFER_UCHAR。例如,对32位X86系列CPU中的Windows,READ_PORT_BUFFER_UCHAR被解释为汇编代码IN操作。
回想这个复杂的过程,经过了多个层次的交互,只是为了执行一个读写端口的操作。对于有过DOS变成经验的程序员来说,在DOS中操作硬件完全可以不使用驱动,直接使用IN汇编代码就可以实现。事实的确如此,但Windows这样做完全是为了安全的考虑。所有直接操作硬件的指令,如读写物理内存、读写端口都认为是危险的操作,必须经过驱动才能完成。
试想一下,如果应用程序能任意执行IN和OUT汇编指令,那么就可轻易地对磁盘进行控制,这会给操作系统带来安全隐患。又例如,如果能直接读写物理内存,黑客会很容易地对当前进程以外的其他进程进行内存读写,那么盗取账号密码将会变得非常简单。
因此,在应用程序中无法执行IN汇编指令,而必须通过驱动程序来执行。在一层层地调用中,每层又会严格地检查,保证参数的合法性。