【问题标题】:Are Linux system calls executed inside an exception handler?Linux 系统调用是否在异常处理程序中执行?
【发布时间】:2021-07-15 03:03:55
【问题描述】:

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

【问题讨论】:

  • 从某种意义上说,是的。但是我不确定将其视为“在处理程序内部”是否有用;而是使用中断/异常/系统调用处理机制作为在非特权代码和特权代码之间转换的一种方式。

标签: linux operating-system system-calls cpu-architecture


【解决方案1】:

使用 80x86 通用的术语(来自英特尔的手册等); 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 系统调用被执行”)。

【讨论】:

    【解决方案2】:

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

    此外,“中断”(包括像int 0x80 这样的软件中断)不同于英特尔术语中的“异常”(由错误条件引起的事件)。

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

    int 0x80 这样的软件中断会在之后 生成一个保存的指令的EIP/RIP,因此iret 将继续而不是重新运行相同的指令,而无需内核手动修改保存的上下文。因此,它与异常非常相似,因为它将 RFLAGS 和 CS:RIP 压入堆栈并跳转到从 IDT 加载的 CS:RIP 地址,但在压入的保存的 RIP 值上有所不同。无论哪种方式,代码都在特权级(环)0 上执行,但在捕获后保存的 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 会,给定寄存器/堆栈的状态。)

    【讨论】:

    • 请注意,英特尔的术语与您的不同。英特尔将 exception 用于由错误条件引起的事件,并且可以选择推送错误代码。术语 interrupt 表示硬件中断或int n 指令。只有在不推送错误条件的情况下,才能使用中断模拟异常。例如,int3 专门生成了一个异常,但由于这个 excp 没有错误代码,因此可以模拟(并且完全等同于)普通的 int 3
    • 对于中断,RIP 将始终指向“下一条指令”(您知道,对于硬件中断,下一条的概念可能很难定义,我们不要花时间在上面),对于异常取决于类型。错误会将 RIP 设置为错误指令,将陷阱设置为下一条指令(例如,int3 是一个陷阱,否则调试器将循环而不调整 RIP)。
    • @MargaretBloom:感谢术语提醒,让我们了解英特尔的术语究竟是什么意思。更新以避免出现给出“例外”的定义;我认为这是您指出的唯一问题,其余的 cmets 是一个很好的脚注。
    • 是的,确实 :) 有时我只是为了好玩而写作 :)
    【解决方案3】:

    在 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 指令。因此,您的代码依赖于操作系统的存在来运行(因为否则没有任何东西可以跳转)。

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

    【讨论】:

      猜你喜欢
      • 2012-06-12
      • 1970-01-01
      • 2012-07-25
      • 1970-01-01
      • 2014-05-17
      • 2018-12-15
      • 2020-07-03
      • 2012-11-26
      • 1970-01-01
      相关资源
      最近更新 更多