3.2 中断请求级别

在应用层开发中,线程有“优先级”的概念,系统调度器以时间片作为粒度,根据线程的优先级来调度线程,线程优先级越高,获得调度的机会越大。与线程优先级概念类似,CPU提供了一个被称为IRQL(中断请求级别)的概念,并且规定,高IRQL的代码,可以中断(抢占)低IRQL的代码的执行过程,从而得到执行机会。

不同级别的IRQL对应不同的数值,软件驱动常见的IRQL及其数值如表3-1所示,数值越大,表示级别越高。

表3-1 软件驱动常见的IRQL

请读者注意,上表中只列出了软件驱动所需要使用到的IRQL,并不代表IRQL只有这三个值。不同体系CPU对IRQL的分级大同小异,为了简单起见,下面的描述中不额外区分CPU体系。

不同IRQL的限制不同:

对于PASSIVE_LEVEL来说,作为级别最低的IRQL,在这个IRQL中可以无限制使用系统提供的API(每个内核API对IRQL有不同的要求),并且可以访问分页内存(Paged)和非分页(NonPaged)内存;

对于APC_LEVEL来说,这个中断级别可以中断PASSIVE_LEVEL的代码,主要用于APC(异步方法调用),在使用系统API时有一定的限制,在内存访问方面,处于APC级别的代码可以访问分页以及非分页内存;

对于DISPATCH_LEVEL来说,存在的限制更多,只有很少一部分API函数可以在这个级别下使用,在内存访问方面,只能使用非分页内存。

读者可能对分页和非分页内存不太了解,在CPU保护模式分页机制开启的情况下,驱动所面对的内存地址都是虚拟地址(指常态下),虚拟地址需要通过页表转换得到实际的物理地址,这个地址转换过程即由CPU完成。这看上去非常简单,虚拟地址对应着物理地址,似乎是一一对应的关系,但事实上虚拟地址与物理地址是多对一的关系,不同的虚拟地址可以对应着某一个相同的物理地址,当这个情况发生的时候,这个物理地址所存的内容会被操作系统置换到磁盘上。举一个简单的例子,虚拟地址A1与A2均指向物理地址P,目前P存放的是A1地址要求存放的内容,由于P没有存放A2的内容(已经置换到硬盘),A2地址在页表中的信息被标记为“缺页”,当程序通过A2地址访问内容的时候会触发缺页异常,系统捕获该异常后,首先会从磁盘中把原来A2地址的内容重新放置回P中,然后恢复程序对A2地址的访问,这个过程对程序来说是透明的,程序也不需要知道这个置换过程。

Windows操作系统定义了两大类内存类型:分页内存与非分页内存。分页是指这些内存的内容可以被置换到磁盘上(也可以是其他介质),而非分页内存是指内存的内容不会被系统置换到磁盘上。

再次提醒读者,在DISPATCH_LEVEL的代码不能访问分页内存。

既然不同IRQL的限制不同,作为开发者来说,就必须清楚地了解自己的代码所处于的IRQL,并且保证代码行为逻辑符合当前IRQL的要求。

判断代码所在的IRQL有两种方法,一种为静态方法,这种方法更多是根据微软WDK帮助文档来判断,比如说驱动的入口函数DriverEntry,系统在调用这个入口函数时,IRQL为PASSIVE_LEVEL,这个是由系统保证的,WDK对这点也有明确的说明;另外一种方法为动态判断方法,如某些回调函数(文件微过滤驱动的回调函数,将在后面章节中介绍)在被系统调用时,IRQL可能是PASSIVE_LEVEL至DISPATCH_LEVEL级别范围,对于这种情况,开发者可以在该回调函数中,通过调用KeGetCurrentIrql函数来获取当前的IRQL,下面给出了一个使用该方法的例子,这个例子依然是基于FirstDriver的:

运行结果如下:

从日志可以看到,DriverEntry与DriverUnload函数得到的IRQL都是0,对照上表,0代表PASSIVE_LEVEL。

最后需要提及的是内核API的中断级别,不同API所能支持的IRQL不同,开发者在使用API前,务必了解清楚这个API所能支持的IRQL。回看上面的例子,在DriverEntry中调用DbgPrint函数打印了Unicode字符串(__FUNCTIONW__为Unicode字符串),对于DbgPrint打印Unicode的情况,WDK帮助文档中有明确的说明:“DbgPrint and DbgPrintEx can be called at IRQL<=DIRQL.However, Unicode format codes (%wc and %ws) can be used only at IRQL=PASSIVE_LEVEL”。

这句话的意思是说:如果使用DbgPrint/ DbgPrintEx函数通过%wc/%ws打印Unicode日志,这个函数只能在PASSIVE_LEVEL下使用。由于DriverEntry以及DriverUnload函数都处于PASSIVE_LEVEL,所以在DriverEntry以及DriverUnload函数内使用DbgPrint打印Unicode是安全的,也符合DbgPrint函数的要求。