C++第三十三篇 -- 研究一下Windows驱动开发(一)内部构造介绍
阅读原文时间:2023年07月08日阅读:3

因为工作原因,需要做一些与网卡有关的测试,其中涉及到了驱动这一块的知识,虽然程序可以运行,但是不搞清楚,心里总是不安,觉得没理解清楚。因此想看一下驱动开发。查了很多资料,看到有人推荐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汇编指令,而必须通过驱动程序来执行。在一层层地调用中,每层又会严格地检查,保证参数的合法性。