Linux 系统调用是否在异常处理程序中执行?

问题描述

我知道在输入系统调用后,例如syscall、int 0x80 (x86/x86-64) 或 svc (ARM) 指令,从 Linux 内核的角度来看,我们停留在调用进程上下文中(但从用户模式切换到内核模式)。然而,从硬件的角度来看,我们跳入了一个 syscall/svc/... 异常处理程序。整个系统调用代码是否在 Linux 的异常处理程序中执行?

解决方法

使用 80x86 常用的术语(来自 Intel 的手册等); CPU 有一个“当前特权级别”(CPL),用于确定代码是否受到限制(例如,是否允许特权指令),这是“用户空间与内核空间”的基础。触发从 CPL=3(“用户空间”)切换到 CPL=0(“内核空间”)的事情是:

  • 异常,通常表示 CPU 检测到问题(例如被零除)

  • IRQ,表示设备需要注意

  • 软件中断、调用门以及 syscallsysenter 指令。这些都是软件向操作系统/内核明确询问某些内容(内核系统调用)的不同方式,其中不同的操作系统/内核可能仅支持其中的一部分或其中之一(64 位代码只需要 syscall 和所有操作系统/内核可能不支持其他替代方案,除非它试图为过时的 32 位内容提供向后兼容性)。

  • 任务门(已过时,不支持 64 位且未被任何知名的 32 位操作系统使用)。

使用这个术语;说 Linux 系统调用在异常处理程序中执行是错误的(因为异常是不涉及的特定事物)。

不过……

不同的人对术语的定义不同;有些人 (ARM) 将“异常”定义为“任何导致切换到内核空间的事物”的同义词。这对于 CPU 设计人员来说是有意义的,他们主要关注任何切换到管理模式对 CPU 的影响,并且几乎没有理由关心差异(因为差异主要是软件开发人员的问题)。对于其他所有人(软件开发人员),通过使用该术语,您可以说内核中的所有内容都在异常处理程序中使用;这主要使“例外”这个词毫无意义(因为“可能是任何东西”并没有提供任何额外的信息)。换句话说,使用该术语,“Linux 系统调用在异常处理程序中执行”在技术上是正确的,但可以缩写为“Linux 系统调用被执行”而不改变语句的含义。

注意:最近英特尔发布了一份未来可能的扩展提案草案(如果被 CPU 采纳和支持并由操作系统启用)用新的“事件”方案替换上述所有内容;其中许多不同/单独的(异常、IRQ、系统调用、...)处理程序被单个“事件处理程序”替换(它必须获取 CPU 提供的“事件原因”,然后分支到“特定事件原因”)代码)。如果发生这种情况,我希望使用第三组术语(例如“异常事件”和“IRQ 事件”和“系统调用事件”,其中所有内核代码都在某种事件的上下文中执行;以及“Linux系统调用在事件处理程序中执行”在技术上是正确的,但可以缩写为“Linux 系统调用被执行”)。

,

没有。最重要的是,syscall / sysenter 既不是异常也不是中断;见下文。

而且,“中断”(包括 int 0x80 之类的软件中断)与英特尔术语中的“异常”(由错误条件引起的事件)不同。

对于“异常”,保存的 RIP 是错误指令(就像您想要的 #PF 页面错误一样,因此使用 iret 返回用户空间将 重试 那个指令。在为 valid 页错误调整页表后,您想要的是什么,而不是导致内核提供 SIGSEGV 的错误)。此外,一些异常会与 RFLAGS 和 CS:RIP 一起推送错误代码。

int 0x80 这样的软件中断会在 之后生成一条已保存的指令的 EIP/RIP,因此 iret 将继续而不是重新运行同一条指令,无需内核必须手动修改保存的上下文。因此,它与异常非常相似,因为它将 RFLAGS 和 CS:RIP 推送到堆栈上并跳转到从 IDT 加载的 CS:RIP 地址,但它的不同之处在于推送的 save-RIP 值究竟是什么。无论哪种方式,代码都在特权级(环)0 执行,但是在捕获之后的 save-RIP = 指令可以方便地将其用作远程过程调用(从用户空间到内核)。>

(半相关的 What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code? 显示了 64 位 Linux 内核中系统调用和 int 0x80 处理程序的一些内核端。从之前对 Meltdown / Spectre 缓解的更改使事情变得更加复杂。)


当然,syscall 根本不使用中断/异常机制(没有 IDT,内核堆栈没有推送任何内容)。相反,它使用 RCX 和 R11 来保存用户空间 RIP 和 RFLAGS,并设置 RIP = IA32_LSTAR_MSR(内核设置为指向其系统调用入口点)。它不使用 TSS 的东西来将 RSP 设置为内核堆栈指针;内核必须自己做这件事。 (通常使用 swapgs 来访问 per-core 或 per-task 存储,它可以保存用户空间 RSP 并加载内核堆栈指针。在 Linux 中,kernelgs 指向内核堆栈的底部,最低地址/最后使用的地址,IIRC。)

sysenter 使用不同的机制,但我认为类似的想法来自 MSR 的内核条目地址,而不是每次都必须从 IDT 加载所有解析 IDT 条目类型的机制。

syscall 和 sysenter 入口点有点像中断处理程序,但 iret 不会让你回到用户空间。 (相反,sysretsysexit 会,给定寄存器/堆栈的状态。)

,

在 32 位 x86 Linux 中,使用 sysenter 指令。 sysenter 指令跳转到 MSR 中指定的地址。 sysenter 指令不是中断。它跳转到 MSR 中指定的地址(Linux 在引导时放置在那里)。

在 x64 Linux 中,改为使用 syscall 指令。它的工作方式与 sysenter 相同。

查看 StackOverflow 上的以下问答:Who sets the RIP register when you call the clone syscall?。我提供了一个非常完整的答案。

另外,我没有提到的是,当您静态链接程序时,所有 glibc 代码都会添加到您的可执行文件中,直到 syscall 指令。因此,您的代码依赖于操作系统的存在来运行(因为否则没有任何可跳转的内容)。

答案是:不,系统调用不在中断处理程序中执行。