Windows系统调用工作原理
原文标题 | How Do Windows NT System Calls REALLY Work? |
原文 | https://www.codeguru.com/cpp/w-p/system/devicedriverdevelopment/article.php/c8035/How-Do-Windows-NT-System-Calls-REALLY-Work.htm |
作者 | John Gulbrandsen |
原文发表日期 | 2004年8月19日 |
译文 | |
翻译 | tommwq |
大多数描述Windows NT系统调用的文章将大量重要细节隐藏起来,使得人们在尝试理解用户程序“跳转到”内核模式的详细过程时感到困惑。下面的文章将阐明Windows NT在切换到内核模式,执行系统服务时使用的机制。这些描述适用于在保护模式下工作的x86 CPU。在其他平台上运行的Windows NT也采用类似的机制进行内核模块切换。
1 内核模式是什么?
与大多数开发者(甚至一些内核模式开发者)所理解的相反,x86 CPU没有所谓的“内核模式”。其他的CPU,比如Motorola 68000有两个内置的处理器模式。比如,它在状态寄存器中有一个标志位,告诉CPU当前是否在用户模式或超管(supervisor)模式下执行。英特尔x86 CPU没有这样的标志位。相反,当前正在执行的代码段(code segment)的特权级别(privilege level),决定了当前程序的特权级别。在x86 CPU上以保护模式运行的应用程序中,每个代码段由一个称为段描述符(segment descriptor)的8字节数据结构描述。段描述符包含代码段的起始地址、代码段长度,以及代码的执行特权级别。以特权级别3执行的代码称为在用户模式中执行,以特权级别0执行的代码称为在内核模式中执行。换言之,内核模式(特权级别0)和用户模式(特权级别3)是代码的属性,不是CPU的属性。英特尔将权限级别0称作“ring 0”,将权限级别3称作“ring 3”。x86 CPU还有两个特权级别未被Windows NT使用(ring 1和ring 2)。不使用这两个特权级别的原因在于,Windows NT被设计为支持多个硬件平台,这些硬件平台未必像英特尔x86处理器支持4个特权级别。
x86 CPU不允许低特权级别(数值更高)代码调用高特权级别(数值更低)代码。这么做将导致CPU自动生成一个通用保护(GP)异常。操作系统中的通用保护异常处理程序将被调用,执行适当的操作(警告用户、终止应用程序等)。注意,上面讨论的所有内存保护机制,包括特权级别,都是x86 CPU的特性,不是Windows NT的特性。如果没有CPU的支持,Windows NT无法实现如上所述的内存保护。
2 段描述符在哪里?
系统中的每个代码段都由一个段描述符描述,在系统中可能存在大量代码段(每个程序包含多个代码段)。段描述符必须保存在一个地方,让CPU可以读取,并允许或拒绝程序提出的执行某段代码申请。英特尔并没有选择将所有信息存储在CPU芯片上,而是存储在内存中。在内存中有两个表用于保存段描述符:全局描述表(GDT)和本地描述表(LDT)。在CPU中中还有两个寄存器保存这些描述符表的地址和大小,以便CPU找到段描述符。这两个寄存器是全局描述符表寄存器(GDTR)和本地描述符表寄存器(LDTR)。操作系统负责设置这些描述符表,并使用GDT和LDT指令加载对应地址到GDTR和LDTR寄存器。这些动作需要在启动过程的早期完成,在CPU切换到保护模式之前。因为若没有描述符表,在保护模式下无法访问内存段。下面的图1说明了GDTR、LDT、GDT和LDT之间的关系。
Figure 1: 图1
因为有两个段描述符表,仅使用索引来唯一地选择段描述符是不够的。段描述符需要包含一个位标识使用的描述符表。索引和表指示位合在一起叫做段选择子(segment selector)。段选择子格式如下所示。
Figure 2: 图2
正如上面的图2所示,段选择子包含一个佳作Requestor Privilege Level(RPL)的2位字段。这些位用于确定某段代码是否可以访问选择器所指向的代码段描述符。例如,一段运行在特权级别3(用户模式)的代码试图跳转或调用特权级别位0的代码,将引发通用保护异常。这是x86 CPU保护ring 3(用户模式)代码不能访问ring 0(内核模式)代码的方法。实际的过程要更加复杂。有关信息请参考阅读列表。就目前而言,知道RPL字段用于对试图用段选择子读取段描述符的代码进行特权检查,就足够了。
3 中断门
既然用户模式(特权级别3)下的代码不能调用内核模式(特权级别0)中的代码,那么Windows NT的系统调用如何生效的呢?答案是它们使用了CPU的特性。为了控制不同权限级别代码之间的跳转,Windows NT使用了x86 CPU的中断门(interrupt gate)特性。为了理解中断门,我们首先要理解在x86 CPU保护模式下中断是如何工作的。
像大多数CPU一样,x86 CPU有一个中断向量表,其中包含如何处理中断的信息。在实模式(real mode)下,x86 CPU的中断向量表包含指向中断服务例程(interrupt service routine,ISR)的指针。在保护模式下,中断向量表包含中断门描述符(interrupt gate descriptor),这是一个8字节的数据结构,描述了应该如何处理中断。中断门描述符记录了中断服务例程所在的代码段信息,以及ISR的入口地址。使用中断门描述符替代简单的指针的原因是,用户模式代码不能直接跳转到内核模式。通过检查中断门描述符中的特权级别,CPU可以验证用户程序被授权调用适当位置的受保护代码(这也是取名“中断门”的原因,通过这扇大门,用户代码可以将控制转移到内核代码)。
中断门描述符包含一个段选择子,指向一个代码段描述符,这个代码段包含中断服务例程。对于Windows NT系统调用,段选择子指向全局描述符表中的一个描述符。全局描述符表包含了所有“全局可用”的段描述符,因此与系统中运行的特定进程无关(换言之,GDT包含的是操作系统的代码段和数据段的描述符)。下面的图3显示了目标代码段中与int 2e指令相关联的中断描述符表项、全局描述符表项和中断服务例程之间的关系。
Figure 3: 图3
4 回到Windows NT系统调用
在介绍了背景材料之后,我们将开始描述Windows NT系统调用是如何从用户模式进入内核模式的。Windows NT系统调用是通过执行int 2e指令(译注:在新版本Widnwos中使用的是syscall指令)发起的。int指令让CPU执行软中断,CPU找到中断描述符表中索引是2e的条目,读取中断门描述符。中断门描述符记录了包含中断服务例程所在代码段的段选择子,还包含目标代码段中ISR的偏移量。CPU将段选择子作为GDT或LDT(取决于段选择子中的TI位)的索引。一旦CPU知道目标段描述符的信息,它将加载这些信息。同时CPU将中断门描述符的偏移量加载到EIP寄存器。这时CPU已经基本完成了设置,可以开始执行内核模式中的ISR代码了。
5 CPU自动切换到内核模式栈
在CPU开始执行内核模式代码段中的ISR之前,它需要切换到内核模式栈。因为内核代码无法保证用户模式栈有足够的空间来执行内核代码。例如,恶意用户代码可以将栈指针指向无效的内存地址,执行int 2e指令,从而让内核函数在使用无效栈指针时引发系统崩溃。因此在x86保护模式下,每个特权级别都有自己的栈。在通过上述的中断门描述符调用高特权级别函数时,CPU自动保存用户模式的SS、ESP、EFLAGS、CS和EIP寄存器到内核模式栈上。以Windows NT系统调用分派函数(KiSystemService)为例,它需要访问用户代码在调用int 2e压入用户栈的参数。根据调用约定,在执行int 2e指令之前,用户代码需要用户栈的指针保存到EBX寄存器中。然后在调用系统函数之前,KiSystemService可以直接将所需参数从用户栈复制到内核栈。下面的图4对此进行了说明。
Figure 4: 图4
6 我们调用的是哪一个系统函数?
既然所有Windows NT系统调用都使用int 2e软中断来切换到内核模式,用户代码如何告诉内核代码执行哪一个系统函数呢?答案是在执行int 2e指令之前,在EAX寄存器中保存一个索引。内核ISR查看EAX寄存器,如果从用户程序传递的参数是正确的,调用指定的内核函数。调用参数通过ISR传递给内核函数。
7 从系统调用返回
系统调用执行完毕后,CPU通过IRET指令恢复用户程序的寄存器信息。CPU从内核栈中弹出所保存的寄存器值,继续执行int 2e指令后面的用户代码。
8 实验
通过检查中断描述符表中条目2e的中断门描述符,我们可以确认CPU寻找Windows NT系统服务调度程序例程的方式如上文所述。下面的代码示例包含WinDbg内核模式调试器的一个扩展,可以导出GDT、LDT或IDT中的描述符。
下载示例代码:ProtMode.zip
WinDbg调试器扩展的名字是protmode.dll。将dll文件复制到包含目标平台kdextx86.dll的目录中,然后通过以下命令加载到WinDbg中: .load protmode.dll
。连接到目标机器后,break into WinDbg调试器(CTRL-C)。显示int 2e描述符的语法是 !descriptor IDT 2e
。这各命令导出以下信息:
kd>!descriptor IDT 2e ------------------- Interrupt Gate Descriptor -------------------- IDT base = 0x80036400, Index = 0x2e, Descriptor @ 0x80036570 80036570 c0 62 08 00 00 ee 46 80 Segment is present, DPL = 3, System segment, 32-bit descriptor Target code segment selector = 0x0008 (GDT Index = 1, RPL = 0) Target code segment offset = 0x804662c0 ------------------- Code Segment Descriptor -------------------- GDT base = 0x80036000, Index = 0x01, Descriptor @ 0x80036008 80036008 ff ff 00 00 00 9b cf 00 Segment size is in 4KB pages, 32-bit default operand and data size Segment is present, DPL = 0, Not system segment, Code segment Segment is not conforming, Segment is readable, Segment is accessed Target code segment base address = 0x00000000 Target code segment size = 0x000fffff
descriptor命令揭示了下列信息:
- IDT中索引2e处的描述符地址是0x80036570。
- 原始描述符数据是C0 62 08 00 00 EE 46 80。
- 这表示:
- 中断门描述符中段选择子指向的段已经加载到内存。
- 特权级别3级以上的代码可以访问这个中断门。
- 包含系统调用(2e)的中断处理程序的段由GDT中索引1处的段描述符描述。
- KiSystemService入口在目标段的偏移量0x804552c0处。
命令 !descriptor IDT 2e
命令还导出了GDT中索引1处的目标代码段描述符。下面是导出数据的解释:
- GDT索引1处的代码段描述符的地址是0x80036008。
- 原始描述符数据是FF FF 00 00 00 9B CF 00。
- 这意味着:
- 页面大小为4KB。这意味着段大小(0x000fffff)应该乘以虚拟内存页面大小(4096字节),才能得到段的实际大小。计算结果是4GB,正好是内核模式可访问的整个地址空间大小。换言之,整个4GB的地址空间都由这个段描述符描述。这就是为什么内核代码可以访问用户模式和内核模式中的任意地址。
- 段是一个内核模式段(DPL=0)。
- 段是禁止跨特权等级转移(not conforming)的。有关该领域的详细讨论,请参阅进一步阅读《Protected Mode Software Architecture》。
- 段是可读的。即代码可以读取段中的内存。该机制用于内存保护。有关该领域的详细讨论,请参阅进一步阅读《Protected Mode Software Architecture》。
- 段已经被访问过。有关该领域的详细讨论,请参阅进一步阅读《Protected Mode Software Architecture》。
要构建WinDbg调试器扩展ProtMode.dll,在Visual Studio 6.0中打开项目并单击构建菜单。关于创建像protmode.dll这样的调试器扩展的介绍,请参阅Debugging Tools for Windows(可以从微软免费下载)附带的SDK。
9 进一步阅读
关于英特尔x86 CPU保护模式的信息有两个很好的来源:
- 《Intel Architecture Software Developers Manual, Volume 3 - System Programming Guide》,在英特尔网站上可以下载PDF版本。
- 《Protected Mode Software Architecture》,由Tom Shanley编写,可以从Amazon.com获取(由Addison Wesley出版)。
关于x86 CPU编程的更多细节,以下资料是必备的:
- 《Intel Architecture Software Developers Manual, Volume 1 - Basic Architecture》
- 《Intel Architecture Software Developers Manual, Volume 2 - Instruction Set Reference Manual》
这两本书在英特尔网站上都有PDF版本(你也获取得到这两本书的免费纸质版。第3卷只有PDF版本)。
10 关于作者
John Gulbrandsen是Summit Soft Consulting的创始人和总裁。John在微处理器、数字和模拟电子设计以及嵌入式和Windows系统开发方面有深厚的背景。John从1992年开始编写Windows程序(Windows 3.0)。他用c++、c#和VB编写Windows应用程序和web系统,使用SoftIce编写和调试Windows内核设备驱动。
要联系John,请给他发电子邮件:John.Gulbrandsen@SummitSoftConsulting.com。
11 关于Summit Soft Consulting
Summit Soft Consulting是一家位于南加州的咨询公司,专注于微软操作系统和核心技术。我们的专业是Windows系统开发,包括内核模式和NT内部编程。
访问Summit Soft Consulting网站:http://www.summitsoftconsulting.com。