7. 内核入口

本文件记录了 arch/x86/entry/entry_64.S 中的一些内核入口。很大一部分解释改编自 Ingo Molnar 的一封电子邮件

https://lore.kernel.org/r/20110529191055.GC9835%40elte.hu

x86 架构有相当多不同的方式来跳转到内核代码中。其中大部分入口点在 arch/x86/kernel/traps.c 中注册,并在 64 位系统下于 arch/x86/entry/entry_64.S 中实现,32 位系统下于 arch/x86/entry/entry_32.S 中实现,最后是 arch/x86/entry/entry_64_compat.S,它实现了 32 位兼容系统调用入口点,从而为 32 位进程在 64 位内核上运行系统调用提供了能力。

IDT 向量分配列在 arch/x86/include/asm/irq_vectors.h 中。

其中一些入口是

  • system_call:来自 64 位代码的 syscall 指令。

  • entry_INT80_compat:来自 32 位或 64 位代码的 int 0x80;无论是哪种情况都是兼容系统调用。

  • entry_INT80_compat, ia32_sysenter:来自 32 位代码的 syscall 和 sysenter

  • interrupt:一个入口数组。每个没有明确指向其他地方的 IDT 向量都会被设置为 interrupts 中的对应值。这些向量指向一整组神奇生成的函数,这些函数以中断号作为参数,最终到达 common_interrupt()

  • APIC 中断:用于 TLB 击落(shootdown)等事情的各种特殊用途中断。

  • 架构定义的异常,如 divide_error。

这里有一些复杂性。不同的 x86-64 入口有不同的调用约定。syscall 和 sysenter 指令有其独特的调用约定。某些 IDT 入口会将错误码压入栈中,而其他则不会。使用 IST 备用栈机制的 IDT 入口需要它们自己的“魔法”来正确处理栈帧。(你可以在 AMD APM 第 2 卷第 8 章和 Intel SDM 第 3 卷第 6 章中找到一些文档。)

处理 swapgs 指令尤其棘手。Swapgs 用于切换 gs 是内核 gs 还是用户 gs。swapgs 指令相当脆弱:它必须完美嵌套且只能是单一深度,它只能在从用户模式进入内核模式时使用,以及在返回用户空间时使用,而且必须非常精确。如果我们哪怕稍微弄砸了一点,就会崩溃。

因此,当我们有一个已经在内核模式下的次要入口时,我们 绝不能 盲目使用 SWAPGS——同样,当它尚未切换/交换时,我们也不能忘记执行 SWAPGS。

现在,还有一个次要的复杂情况:有一种低成本的方法来测试 CPU 处于哪种模式,也有昂贵的方法。

低成本的方法是从内核栈上的入口帧中提取此信息,即从内核栈 ptregs 区域的 CS 中提取

xorl %ebx,%ebx
testl $3,CS+8(%rsp)
je error_kernelspace
SWAPGS

昂贵的(偏执的)方法是读回 MSR_GS_BASE 值(这是 SWAPGS 所修改的)

      movl $1,%ebx
      movl $MSR_GS_BASE,%ecx
      rdmsr
      testl %edx,%edx
      js 1f   /* negative -> in kernel */
      SWAPGS
      xorl %ebx,%ebx
1:    ret

如果我们处于中断或用户陷阱/门类似的边界,那么我们可以使用更快的检查:栈将是 SWAPGS 是否已经完成的可靠指示器:如果我们发现我们是一个中断内核模式执行的次要入口,那么我们就知道 GS 基址已经切换了。如果它表明我们中断了用户空间的执行,那么我们必须执行 SWAPGS。

但是,如果我们处于 NMI/MCE/DEBUG 或其他任何超级原子入口上下文中,它可能在正常入口将 CS 写入栈之后、但在我们执行 SWAPGS 之前触发,那么检查 GS 的唯一安全方法是较慢的方法:RDMSR。

因此,超级原子入口(除了单独处理的 NMI 之外)必须使用带有 paranoid=1 的 idtentry 来正确处理 gsbase。这会触发三个主要的行为变更

  • 中断入口将使用较慢的 gsbase 检查。

  • 来自用户模式的中断入口将关闭 IST 栈。

  • 退出中断到内核模式时不会尝试重新调度。

我们尽量只对绝对需要对 GS 基址进行更昂贵检查的向量使用 IST 入口和 paranoid 入口代码——并且我们使用常规的(更快的)paranoid=0 变体生成所有“正常”的入口点。