【问题标题】:why linux kernel don't restore all register when using sysenter/sysexit?为什么linux内核在使用sysenter/sysexit时不恢复所有寄存器?
【发布时间】:2015-06-27 23:44:50
【问题描述】:

在 linux 内核 2.6.11 中, 当使用 sysenter 进行系统调用时, 和init 0x80几乎一样,使用save_all将所有寄存器压入内核栈,但是调用完成后, 如果没有设置相关标志,我们使用 sysexit 返回,但不会恢复所有已保存在堆栈上的寄存器。

一些系统调用可能会改变寄存器的值,为什么我们不需要reotre所有寄存器

我已经阅读了相应的 i386 文档,上面写着

"Intel386 上的所有寄存器都是全局的,因此对调用函数和被调用函数都是可见的。寄存器 %ebp、%ebx、%edi、%esi 和 %esp“属于”调用函数。换句话说,被调用函数必须为其调用者保留这些寄存器的值。剩余的寄存器“属于”被调用函数。如果调用函数想要在函数调用中保留这样的寄存器值,它必须将值保存在其本地堆栈帧中。 "

所以保存工作是 glibc 包装函数的责任,我已经阅读了一些 glibc 代码来确定这一点。 因此,当使用 sysenter/sysexit 进行系统调用时, 我们首先将 %ebp,%edx,%ecx 推送到用户堆栈上 因为 %edx 和 %ecx 不在保存寄存器中,所以我们需要在完成系统调用后恢复它们 并且我们在调用系统服务例程之前也使用 %ebp 来保存用户堆栈指针,所以我们需要恢复它来传递参数

【问题讨论】:

  • Linux 调度到的sys_whatever 函数的编译器生成实现将自己保留调用保留寄存器,因此调度代码只需要在返回给用户之前恢复调用破坏的regs-空间。

标签: linux x86 system-calls calling-convention sysenter


【解决方案1】:

原因与 64 位模式下的 why RCX is not used for passing parameters to system calls, being replaced with R10 相同:因为 sysentersysexit 指令的工作方式。即,来自英特尔文档sysexit 指令:

在执行 SYSEXIT 之前,软件必须指定特权级 3 代码段和代码入口点,以及特权级 3 堆栈段和堆栈指针,通过将值写入以下 MSR 和通用 注册:

• IA32_SYSENTER_CS(MSR 地址 174H)— 包含一个 32 位值,用于确定段 特权级别 3 代码和堆栈段的选择器(请参阅操作部分)

RDX — 该寄存器中的规范地址被加载到 RIP(因此,该值引用第一条指令 在用户代码中执行)。如果返回不是 64 位模式,则只加载 31:0 位。

ECX — 该寄存器中的规范地址被加载到 RSP 中(因此,该值包含用于 特权级别 3 堆栈)。如果返回不是 64 位模式,则只加载 31:0 位。

因此rdx (edx) 和rcx (ecx) 被指令保留。现在ebp 呢?好吧,来自sysenter 上的文档说明:

SYSENTER 和 SYSEXIT 指令是伴随指令,但它们不构成调用/返回对。 执行 SYSENTER 指令时,处理器不会保存用户代码的状态信息(例如, 指令指针),并且 SYSENTER 和 SYSEXIT 指令都不支持在 堆栈。

这很明显,RSPsysenter 上被IA32_SYSENTER_ESP 替换,因此操作系统甚至不知道用户空间堆栈应该在哪里,至少这不是易学的。所以 Linux 保留ebp 正是为了这个目的:为操作系统提供用户堆栈。现在调用者必须保存ebp,因为它必须在执行sysenter 之前用esp 覆盖它。

为什么 Linux 不使用 edxecx 来传递堆栈指针——这两个寄存器不会在 sysenter 上被覆盖?我认为这是为了速度:ebp,当用于在通常的int 0x80 调用中传递参数时,是最后一个可能的(第六个)参数。系统调用很少需要超过 5 个参数,因此无需为几乎所有系统调用读取用户空间堆栈(如果 edxecx 用于堆栈指针),Linux 只需为具有 6 个参数的系统调用执行此操作. (注意在执行sysenter 之前必须最后推送ebp ——这正是因为内核必须知道在哪里可以找到第六个参数)。

这一切都在 Linux 源代码中进行了总结,arch/x86/entry/vdso/vdso32/sysenter.S:

/*
 * The caller puts arg2 in %ecx, which gets pushed. The kernel will use
 * %ecx itself for arg2. The pushing is because the sysexit instruction
 * (found in entry.S) requires that we clobber %ecx with the desired %esp.
 * User code might expect that %ecx is unclobbered though, as it would be
 * for returning via the iret instruction, so we must push and pop.
 *
 * The caller puts arg3 in %edx, which the sysexit instruction requires
 * for %eip. Thus, exactly as for arg2, we must push and pop.
 *
 * Arg6 is different. The caller puts arg6 in %ebp. Since the sysenter
 * instruction clobbers %esp, the user's %esp won't even survive entry
 * into the kernel. We store %esp in %ebp. Code in entry.S must fetch
 * arg6 from the stack.
 *
 * You can not use this vsyscall for the clone() syscall because the
 * three words on the parent stack do not get copied to the child.
 */

【讨论】:

    【解决方案2】:

    这应该由使用的 ABI(调用约定)定义。一些寄存器在函数调用中保留,而有些则没有。您可以查看您平台上使用的 ABI。

    至于 X64,http://x86-64.org/documentation/abi.pdf 记录了它。见图3.4

    preserved 跨调用意味着寄存器是被调用者保存的,因此函数应该在返回之前恢复它;

    不保留表示调用者保存,因此函数可以直接使用它但不能恢复它。

    【讨论】:

    • 您能否简要解释一下图 3.4,以及它如何应用于这里提出的问题?
    • 您可以发布具体的汇编代码,让我们知道您对哪个寄存器有问题。 :-)
    • 不要在帖子中使用“EDIT:”;这不是一个论坛。编辑历史记录可用here
    • 系统调用“几乎从不”使用来自任何特定语言或工具集的任何特定 ABI。原因是通常涉及特权级别更改,这需要堆栈或寄存器使用方面的重大差异,这使得符合任何正常 ABI 是不可能的。
    猜你喜欢
    • 1970-01-01
    • 2018-01-16
    • 2012-10-16
    • 2018-02-11
    • 2020-06-04
    • 2020-06-05
    • 2012-04-24
    • 1970-01-01
    • 2021-11-01
    相关资源
    最近更新 更多