正如您已经知道的那样,Windows 内核向用户空间程序公开了许多设施。 (如果你好奇there's a list of system calls)。这些系统调用都由一个唯一编号标识,该编号不是 Microsoft 提供的公开文档接口的一部分。相反,当您从程序中调用公开公开的函数时,会在安装(或更新)Windows 时安装一个 DLL,该 DLL 的入口点只是一个普通的非特权用户模式函数调用。这个 DLL 知道公共接口和当前运行内核中可用系统调用之间的映射。这些映射并不总是 1:1,这允许在不破坏现有代码的情况下使用稳定的接口进行调整和增强。
当一些用户态代码调用这些函数之一时,它的作用是为系统调用准备参数,然后启动跳转到内核模式。这种跳跃的具体发生方式取决于 Windows 当前运行的体系结构。事实上,它不仅在 x86 和 Arm 之间存在差异,甚至在 AMD 和 Intel x86 系统之间也存在差异。为简单起见,我将在这里只讨论现代 Intel x86 32 位机箱(使用SYSENTER instruction)。在 x86 上,大多数其他变体都相对较小,例如 int 2Eh was used prior to SYSENTER support。
在启动初期,操作系统会做大量工作来准备启用用户空间和系统调用。了解这一点对于了解系统调用的实际工作方式至关重要。
首先让我们稍微回顾一下,考虑一下我们所说的用户态和内核模式到底是什么意思。在 x86 上,当我们谈论特权代码与非特权代码时,我们谈论的是“环”。实际上有 4 个(忽略管理程序),但由于各种原因,除了 ring0(内核)和 ring3(用户空间)之外,没有人真正使用过任何东西。当我们在 x86 上运行代码时,正在执行的地址 (EIP) 和正在读取/写入的数据来自段。
段大多只是在 x86 上的虚拟寻址成为事物之前的日子遗留下来的历史事故。然而,它们在这里对我们很重要,因为有一些特殊的寄存器可以定义当我们执行指令或引用内存时当前正在使用哪些段。 x86 上的段都定义在一个大表中,称为全局描述符表或GDT。 (还有一个本地描述符表,LDT,但这里不会进一步讨论当前的讨论)。我们在这里讨论的重点是表条目的(神秘)布局包括 2 位,称为 DPL,它定义了当前活动段的特权级别。您会注意到 2 位正好足以定义 4 级权限。
所以简而言之,当我们谈论“在内核模式下执行”时,我们实际上只是指我们的活动代码段 (CS) 和数据段选择器指向 GDT 中 DPL 设置为 0 的条目。同样对于用户态,我们有a CS 和数据段选择器指向 DPL 设置为 3 且无法访问内核地址的 GDT 条目。 (还有其他选择器,但为了简单起见,我们现在只考虑“代码”和“数据”)。
回到内核启动期间的早期:在启动期间内核创建GDT entries we need。 (这些必须按特定顺序排列,SYSENTER 才能工作,但这主要只是一个实现细节)。还有一些“机器特定的寄存器”可以控制我们的处理器的行为方式。这些只能由特权代码设置。其中三个重要的是:
- IA32_SYSENTER_ESP
- IA32_SYSENTER_EIP
- IA32_SYSENTER_CS
回想一下,我们在用户空间 (ring3) 中运行了一些想要转换到 ring0 的代码。让我们假设它已经根据调用约定保存了它需要的所有寄存器,并将参数放入调用期望的正确寄存器中。然后我们点击 SYSENTER 指令。 (实际上我认为它使用KiFastSystemCall)。 SYSENTER 指令是特殊的。它根据内核在机器特定寄存器 IA32_SYSENTER_CS 中设置的值修改当前代码和数据段选择器。 (堆栈/数据段值计算为 IA32_SYSENTER_CS 的偏移量)。随后,堆栈指针本身 (ESP) 被设置为内核堆栈,该堆栈之前为处理系统调用而设置并保存到 MSR IA32_SYSENTER_ESP 中,同样对于 EIP,来自 IA32_SYSENTER_EIP 的指令指针。
由于 CS 选择器现在指向 DPL 设置为 0 的 GDT 条目,而 EIP 指向内核堆栈上的内核模式代码,我们此时正在内核中运行。
从这里开始,内核模式代码可以从内核和用户空间读取和写入内存(需要适当注意),以承担执行系统调用所需的实际工作。系统调用的参数可以根据调用约定从寄存器等中读取,但任何实际上是返回用户空间的指针或内核对象句柄的参数也可以访问以读取更大的数据块。
当系统调用结束时,这个过程基本上是相反的,我们最终回到用户态,选择器使用 DPL 3。