深入理解和使用Windows NT驱动程序的执行上下文(一)

本文深入探讨了Windows NT驱动程序的执行上下文,详细解析了相关数据结构及其在程序开发中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

深入理解和使用Windows NT驱动程序的执行上下文

原作:Open System Resources,Inc.
编译:codewarrior@fudan cse

 

要理解Windows NT驱动程序,最重要的概念之一就是驱动程序执行时的所处的执行上下文Execution Context)。深入理解这个概念,并小心地应用你所掌握的关于它的知识,可以帮助你编写更快速高效的驱动程序。
本节包括下列主题:
l          什么是上下文
l          几种不同的上下文
l          上下文带来的影响
l          最大限度地利用上下文
l          总结
对于NT标准内核态驱动程序而言,一个重要的细节就是驱动程序的某个特定的函数执行时所处的上下文(Context),传统上说来文件系统开发人员大多对此比较关心,但若能牢固理解执行上下文NT所有类型的内核态驱动程序的编写人员而言,都会从中受益。深入理解执行上下文并小心地使用之,可以确保设计出具有更高性能和更低运行代价的驱动程序。
在本文中,我们将对执行上下文的概念作一个探究。在本文结尾处,会举出一个允许用户程序到内核态执行并获得系统全部特权的驱动程序,以此作为应用我们所介绍的内容的一个实例。
什么是执行上下文
当我们提到一个例程的“上下文”的时候,我们指的是它的线程和进程的执行环境。在NT中,这个环境是通过当前的线程环境块TEB和进程环境块PEB构成的。因此所谓的上下文也就包括虚拟内存设置(告诉我们哪个物理内存块对应哪些虚拟内存地址)、句柄的转换(因为句柄是和进程相关联的)、调度器信息、栈,和通用寄存器以及浮点寄存器的集合。当我们问“一个特定的内核例程在哪个上下文中执行”时,我们实际上是在问,“NT内核的调度器当前指定哪个线程在运行?”因为每个线程都只属于一个进程,故而当前的线程也就暗示了一个明确的当前进程。总之,“当前线程”和“当前进程”隐喻了所有这些区别于其他线程和进程的东西(句柄,虚拟内存,调度器状态,寄存器)。
对于内核态驱动程序编写人员而言,虚拟内存上下文也许是“上下文”这一术语所包含的众多方面中最有用的一个。回忆一下,NT把用户进程映射到虚拟地址空间中低端的2GB,而把操作系统本身的代码映射到虚拟地址空间中高端的2GB。当一个用户进程中的一个线程在执行时,它的虚拟地址的范围是从02GB,所有高于2GB的地址都被设为拒绝访问,以防用户直接访问操作系统的代码和数据结构。当操作系统的代码在执行时,它的虚拟地址的范围是从04GB,当前用户进程(如果有这么一个的话)被映射到02GB的地址范围内。在NT V3.51V4.0中,映射到高端的2GB的代码是固定不变的。但是映射到低段2GB的地址空间的代码是在变化的,这取决于当前哪个进程在执行。
让我们比上面所介绍的再更深入一步,在NT这样的内存布局中,给定进程P中一个有效的用户虚拟地址XX应当小于等于2GB),那么它同内核虚拟地址X都对应着相同的物理内存位置,当然,仅当P是当前进程(也正因为如此)且进程P的物理页映射到操作系统低端的2GB虚拟地址空间中去时,这个结论才是正确的。上面的最后一句话换种说法来表述就是:“仅当P是当前进程上下文时上述结论正确”。因此,用户虚拟地址和内核虚拟地址的高端的2GB实际是对应着相同的物理位置,如果进程上下文相同的话。(译注:这里所指的用户虚拟地址空间和内核虚拟地址空间,是两个不同的概念,内核虚拟地址空间和用户虚拟地址空间并不是整个4GB空间中高低2GB的简单划分。实际上,内核虚拟地址空间指从内核的角度所看到的04GB的虚拟地址范围;而用户地址空间指的是从用户态的进程角度看到的04GB的虚拟地址范围——尽管对于用户态的进程而言,它看到的4GB虚拟地址空间中高2GB是拒绝访问的。稍候作者会用一张图来解释之。对内核而言,它所看到的内核虚拟地址空间的低2GB是不断变化的,这是因为进程调度的缘故,只有在当前调度进程P执行时,虚拟地址X才对应着物理内存中同一个位置)。
对于内核态驱动程序编写人员而言,另一个他们感兴趣的关于上下文的细节是线程调度上下文(Thread scheduling context)。当一个线程等待时(譬如使用Win32函数WaitForSingleObject(…)等待一个未激发的对象),所等待的目标的相关信息是存储在该线程的调度上下文中的。如果等待不能得到满足,则线程会被移出就绪队列,当等待条件得到满足之后再重新回到就绪队列。
上下文同样会对句柄的使用产生影响。因为句柄是和某个具体的进程相关联的,因此在一个进程的上下文中创建的句柄,不能在另一个进程上下文中使用。
几种不同的上下文
内核态的例程在下列三种上下文中的一种中执行:
l          系统进程上下文
l          某个具体的用户线程(和进程)上下文
l          任意用户线程(和进程)上下文
在执行过程中,所有内核态驱动程序的各部分都有可能在上述三种上下文中的任一种下执行。譬如,驱动程序的DriverEntry(…)函数总是运行在系统进程上下文中。系统进程上下文没有与之相关联的用户线程上下文(也就没有TEB),并且也就没有任何用户态进程被映射到内核的虚拟地址空间中低端的2GB(译注:回忆一下,内核的虚拟地址空间也是从04GB的)。另一方面来说,DPC(比如用于ISR或计时器定时函数的驱动程序的DPC)运行在无关用户线程上下文。这意味着在DPC的执行过程中,任何用户线程都有可能是“当前”线程(译注:正在执行的意思),也就是说任何用户进程都有可能被映射到内核虚拟地址空间中低端的2GB
驱动程序分发例程执行时所处的上下文可能尤其令人着迷。在许多情况下,内核态驱动程序的分发例程执行在产生调用的用户线程的上下文中。图1解释了为什么会这样。当一个用户线程发出一个针对设备的I/O函数调用时——例如调用了Win32函数ReadFile(…)——会导致一个系统服务请求。在Intel体系结构处理器上,这样的请求是通过软件中断来实现的。软件中断穿过中断门,中断门切换处理器的当前特权
1
CPL到内核模式,这会引发一个切换到内核栈的动作,然后调用系统服务派发器(System service dispatcher)。接着,系统服务派发器调用操作系统提供的用于处理所请求的系统服务的函数。以ReadFile(…)为例,这个函数(译注:指用于处理所请求的系统服务的函数)是I/O子系统的NtReadFile(…)NtReadFile(…)函数创建一个IRP,检查ReadFile(…)请求中传入的文件句柄所引用的文件对象在内核中和哪个驱动程序相关联,接着调用该驱动程序处理“读”的分发例程。所有这些都发生在PASSIVE_LEVEL中断请求级。
在上面介绍的整个处理过程中,并没有发生对用户请求进行调度或者排队的动作。因此,用户线程和进程的上下文不会发生任何改变。在本例中,驱动程序的分发例程执行在发出ReadFile(…)调用的用户线程的上下文中。这表示当驱动程序的“读”分发函数执行的时候,是用户线程在执行内核态驱动的代码。
那么,是不是驱动程序的分发例程总是执行在发出请求的用户线程的上下文中呢?未必。4.0版的《内核模式驱动程序开发指南》的第16.4.1.1节告诉我们,“只有位于最高层的NT驱动程序,如文件系统驱动程序,才可以肯定它们的分发例程是在这样的用户模式线程的上下文中被调用的。”但是严格说来这并不完全对(从我们的例子中可以看出这一点),所能肯定正确的一点是:FSD是在发出请求的用户线程的上下文中被调用的。事实是,当用户发出一个I/O请求后,若一个驱动程序立即被调用且它不把请求传递给其他驱动程序,那么可以担保该驱动程序是在发出请求的用户线程的上下文中被调用的,这包括FSD。但是这个事实的另一层意思是,大部分由用户编写的标准内核态驱动程序会直接向用户应用程序提供一些功能,比如处理控制设备,这些驱动程序的分发函数会在发出请求的用户线程的上下文中被调用。
事实上,只有一种情况下驱动程序的分发例程不在产生调用的用户线程上下文中被调用,那就是用户的请求首先是被传到较高层的驱动程序,如文件系统驱动程序。如果较高层的驱动程序把请求发送到系统工作者线程(System worker thread),上下文会因此而改变。当IRP最终向下传递到较低层的驱动程序手中时,就无法保证较高层驱动程序在执行转发IRP的动作时所在的上下文是发出请求的用户线程的上下文。较低层的驱动程序也因此将执行在任意线程上下文。
较为通用的规则是,如果一个设备直接被用户访问,中途不经过其他驱动程序的干预,则该设备的驱动程序的分发例程通常是运行在发出请求的用户线程的上下文中。当这一切发生时,会产生一些相当有趣的结果,这允许我们作一些同样有趣的事情。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值