3.1 上下文环境

应用层EXE程序工作在用户态,内核驱动程序工作在内核态。所谓用户态与内核态,是基于CPU的特权环来定义的,CPU提供了0环~3环(ring 0 ~ ring 3 )共四个特权环,Windows操作系统使用了其中的0环和3环,0环为内核态,3环为用户态。不同环之间代码特权不同,访问地址空间也不同,如对于0环的指令来说,可以执行特权指令,访问内核态的地址空间范围。

应用层EXE有独立进程的概念,比如开发者开发一个EXE程序,当这个EXE运行的时候,开发者可以很清楚地知道程序的代码运行在哪一个线程中。对于内核驱动开发来说,进程的概念显得相当模糊,初学者往往不清楚自己的驱动代码具体运行在什么进程或线程中,但搞清楚这些细节是驱动入门的重要途径。

这就是后面需要介绍的上下文(Context)概念,上下文(Context)泛指CPU在执行代码时,该代码所处的环境与状态。通俗来讲,这些环境状态包括(不仅限):当前代码所属线程、中断请求级别、CPU寄存器各状态等。

还记得第1章与第2章介绍的FirstDriver工程吗?这个工程里面有两个函数,一个是驱动的入口函数DriverEntry,另一个是响应驱动停止的DriverUnload函数,这两个函数都是由系统调用的,那么这两个函数被调用时处于哪一个进程中呢?

笔者通过在这两个函数中调用PsGetCurrentProcessId,获取当前进程的ID,并且打印出来,具体代码如下:

驱动启动时日志如下:

驱动卸载时日志如下:

从日志可以看出,无论是驱动入口函数,还是驱动卸载函数,都隶属于进程ID为4的进程,在笔者的Windows 10测试环境中,进程ID为4的进程为SYSTEM进程,如图3-1所示。

图3-1 SYSTEM进程ID

SYSTEM进程其实是操作系统虚拟出来的一个进程,代表系统内核。一般来说,内核代码都处于SYSTEM进程空间中,但是驱动对象(DRIVER_OBJECT)的派遣例程一般工作在发起请求的进程中,关于驱动对象的派遣例程,后面会有专门的章节介绍。

与上下文概念相关联的是地址空间,对32位系统来说,应用层程序有独立的2GB低地址空间,这2GB地址是虚拟地址,不同进程之间相互独立,互不影响,而高地址的2GB是内核共享的地址空间。64位系统与32位系统类似,在 64 位 Windows 中,虚拟地址空间的理论大小为264字节,但实际上仅使用264字节范围的一小部分,范围从 0x000'00000000 至 0x7FF'FFFFFFFF 的 8 TB 用于应用层空间,范围从 0xFFFF0800'00000000 至 0xFFFFFFFF'FFFFFFFF 的 248 TB 用于内核空间。

关于独立的应用层地址空间与共享的内核地址空间的区别,举一个通俗的例子来说明:比如进程P1与P2,在各自进程空间内修改0x6abb0000地址处的内容,那么P1只能看到自己修改后的内容,P2也只能看到自己修改后的内容,相互不影响;而对于内核空间来说,由于内核空间是共享的(提醒一下,并非全部内核地址空间都是共享的,但读者初期可以暂时认为是全部),所以对于两个驱动程序来说,驱动D1修改内核某个地址的内容,驱动D2可以读取到驱动D1修改后的内容。

一名合格的驱动开发者,必须清楚自己的代码在运行时所对应的上下文,以免造成驱动异常。下面是一个由上下文不正确导致出现错误的例子:驱动程序需要访问P1进程的应用层地址空间A,但当前驱动代码工作在进程P2,当驱动访问A时,实际上访问到的却是P2进程的A,这个后果是不可预料的。