c - FreeBSD 系统调用比 Linux 破坏更多的寄存器?内联汇编优化级别之间的不同行为

标签 c x86-64 system-calls freebsd inline-assembly

最近我在玩 freebsd 系统调用,我对 i386 部分没有问题,因为它在 here. 上有很好的记录。但是我找不到 x86_64 的相同文档。
我看到人们在 linux 上使用相同的方式,但他们只使用程序集而不是 c。我想在我的例子中,系统调用实际上改变了一些被高优化级别使用的寄存器,所以它给出了不同的行为。

/* for SYS_* constants */
#include <sys/syscall.h>

/* for types like size_t */
#include <unistd.h>

ssize_t sys_write(int fd, const void *data, size_t size){
    register long res __asm__("rax");
    register long arg0 __asm__("rdi") = fd;
    register long arg1 __asm__("rsi") = (long)data;
    register long arg2 __asm__("rdx") = size;
    __asm__ __volatile__(
        "syscall"
        : "=r" (res)
        : "0" (SYS_write), "r" (arg0), "r" (arg1), "r" (arg2)
        : "rcx", "r11", "memory"
    );
    return res;
}

int main(){
    for(int i = 0; i < 1000; i++){
        char a = 0;
        int some_invalid_fd = -1;
        sys_write(some_invalid_fd, &a, 1);
    }
    return 0;
}
在上面的代码中,我只是希望它调用 sys_write 1000 次然后返回 main。我使用 truss 检查系统调用及其参数。 -O0 一切正常,但是当我使用 -O3 for 循环时,它会永远卡住。我相信系统调用正在改变 i变量或 1000到一些奇怪的事情。
转储函数 main 的汇编代码:
0x0000000000201900 <+0>:     push   %rbp
0x0000000000201901 <+1>:     mov    %rsp,%rbp
0x0000000000201904 <+4>:     mov    $0x3e8,%r8d
0x000000000020190a <+10>:    lea    -0x1(%rbp),%rsi
0x000000000020190e <+14>:    mov    $0x1,%edx
0x0000000000201913 <+19>:    mov    $0xffffffffffffffff,%rdi
0x000000000020191a <+26>:    nopw   0x0(%rax,%rax,1)
0x0000000000201920 <+32>:    movb   $0x0,-0x1(%rbp)
0x0000000000201924 <+36>:    mov    $0x4,%eax
0x0000000000201929 <+41>:    syscall 
0x000000000020192b <+43>:    add    $0xffffffff,%r8d
0x000000000020192f <+47>:    jne    0x201920 <main+32>
0x0000000000201931 <+49>:    xor    %eax,%eax
0x0000000000201933 <+51>:    pop    %rbp
0x0000000000201934 <+52>:    ret
sys_write()出了什么问题?为什么 for 循环卡住了?

最佳答案

优化级别决定了 clang 决定在何处保留其循环计数器:在内存中(未优化)或在寄存器中,在这种情况下 r8d (优化)。 R8D 是编译器的合乎逻辑的选择:它是一个调用破坏的 reg,它可以使用而无需在 main 的开始/结束处保存。 ,并且您已经告诉它它可以在没有 REX 前缀的情况下使用的所有寄存器(如 ECX)是 asm 语句的输入/输出或破坏。
备注 : 如果 FreeBSD 与 MacOS 类似,系统调用错误/无错误状态将在 CF(进位标志)中返回,而不是通过在 -4095..-1 范围内的 RAX。在这种情况下,您需要一个 GCC6 标志输出操作数,如 "=@ccc" (err)int err ( #ifdef __GCC_ASM_FLAG_OUTPUTS__ - example ) 或 setc %cl在模板中手动实现 bool 值。 (CL 是一个不错的选择,因为您可以将其用作输出而不是 clobber。)

FreeBSD的syscall处理垃圾 R8、R9 和 R10 ,除了 Linux 所做的最低限度的破坏之外:RAX (retval) 和 RCX/R11( syscall instruction 本身将它们用于 save RIP / RFLAGS,因此内核可以找到返回用户空间的方法,因此内核永远不会看到原始值。)
也可能是 RDX,we're not sure ;评论称其为“返回值 2”(即作为 RDX:RAX 返回值的一部分?)。我们也不知道 FreeBSD 打算在 future 的内核中维护哪些面向 future 的 ABI 保证。
你不能假设 R8-R10 在 syscall 之后为零因为在跟踪/单步执行时,它们实际上被保留而不是归零。 (因为内核选择不通过 sysret 返回,原因与 Linux 相同:如果寄存器可能在系统调用内部被 ptrace 修改,则硬件/设计错误会使它变得不安全。例如尝试使用 sysret非规范 RIP 将在 Intel CPU 上的 ring 0(内核模式)中 #GP!这是一场灾难,因为此时 RSP = 用户堆栈。)

相关内核代码为the sysret path (@NateEldredge 很好地发现了;我通过搜索 swapgs 找到了 syscall 入口点,但没有查看返回路径)。
函数调用保留的寄存器不需要由该代码恢复,因为调用 C 函数并没有首先破坏它们。并且代码确实恢复了函数调用破坏的“遗留”寄存器 RDI、RSI 和 RDX。
R8-R11 是在函数调用约定中被调用破坏的寄存器,并且在原始 8 x86 寄存器之外。所以这就是让他们“特别”的原因。 (R11 不会归零;syscall/sysret 将其用于 RFLAGS,因此您会在 syscall 之后找到该值)
归零比加载它们略快,并且在正常情况下(syscall libc 包装函数中的指令)您将返回到仅假设函数调用约定的调用者,因此将假设 R8-R11被破坏了(RDI、RSI、RDX 和 RCX 也是如此,尽管 FreeBSD 出于某种原因确实费心去恢复它们。)

这种归零仅在非单步或跟踪时发生 (例如 truss 或 GDB si )。 syscall entry point into an amd64 kernel (Github)确实保存了所有传入的寄存器,因此它们可以通过内核之外的其他方式恢复。

更新 asm() wrapper

// Should be fixed for FreeBSD, plus other improvements
ssize_t sys_write(int fd, const void *data, size_t size){
    register ssize_t res __asm__("rax");
    register int arg0 __asm__("edi") = fd;
    register const void *arg1 __asm__("rsi") = data;  // you can use real types
    register size_t arg2 __asm__("rdx") = size;
    __asm__ __volatile__(
        "syscall"
                    // RDX *maybe* clobbered
        : "=a" (res), "+r" (arg2)
                           // RDI, RSI preserved
        : "a" (SYS_write), "r" (arg0), "r" (arg1)
          // An arg in R10, R8, or R9 definitely would be
        : "rcx", "r11", "memory", "r8", "r9", "r10"   ////// The fix: r8-r10
         // see below for a version that avoids the "memory" clobber with a dummy input operand
    );
    return res;
}
使用 "+r"带有任何需要的参数的输出/输入操作数 register long arg3 asm("r10")或类似的 r8 或 r9。
这是在包装函数内部,因此 C 变量的修改值被丢弃,每次都强制重复调用来设置 args。这将是“防御性”方法,直到另一个答案确定更明确的非垃圾寄存器。

I did break *0x000000000020192b then info registers when break happened. r8 is zero. Program still gets stuck in this case


我假设 r8在你做 GDB 之前不是零 continue跨越syscall操作说明。是的,该测试证实 FreeBSD 内核正在破坏 r8不是单步的时候。 (并且行为方式与我们在源代码中看到的相匹配。)

请注意,您可以告诉编译器 write系统调用仅使用虚拟 "m" 读取内存(不写入)输入操作数而不是 "memory"破坏者。这将让它提升 c 的存储跳出循环。 ( How can I indicate that the memory *pointed* to by an inline ASM argument may be used? )
"m"(*(const char (*)[size]) data)作为输入而不是 "memory"破坏者。
如果你要为你使用的每个系统调用编写特定的包装器,而不是你为每个 3 操作数系统调用使用的通用包装器,它只是将所有操作数强制转换为 unsigned long ,这是您可以从中获得的优势。
说到这里,让你的系统调用参数全部为 long 绝对没有意义。 ;使用户空间符号扩展int fd放入 64 位寄存器只是浪费指令。内核 ABI 将(几乎可以肯定)忽略窄 args 的寄存器的高字节,就像 Linux 一样。 (同样,除非你制作一个通用的 syscall3 包装器,你只是使用不同的 SYS_ 数字来定义写、读和其他 3 操作数系统调用;那么你会将所有内容转换为 register-width 并使用"memory" 破坏者)。
我对下面的修改版本进行了这些更改。
另请注意,对于 RDI、RSI 和 RDX,您可以使用特定寄存器字母约束来代替 register-asm 本地变量,就像您在 RAX ( "=a" ) 中对返回值所做的一样。顺便说一句,您实际上并不需要电话号码的匹配约束,只需使用 "a"输入;它更容易阅读,因为您不需要查看另一个操作数来检查您是否匹配了正确的输出。
// assuming RDX *is* clobbered.
// could remove the + if it isn't.
ssize_t sys_write(int fd, const void *data, size_t size)
{
    // register long arg3 __asm__("r10") = ??;
    // register-asm is useful for R8 and up

    ssize_t res;
    __asm__ __volatile__("syscall"
                    // RDX
        : "=a" (res), "+d" (size)
         //  EAX/RAX       RDI       RSI
        : "a" (SYS_write), "D" (fd), "S" (data),
          "m" (*(const char (*)[size]) data) // tells compiler this mem is an input
        : "rcx", "r11"    //, "memory"
#ifndef __linux__
              , "r8", "r9", "r10"   // Linux always restores these
#endif
    );
    return res;
}
有些人更喜欢register ... asm("")对于所有操作数,因为您可以使用完整的寄存器名称,并且不必记住 RDI/EDI/DI/DIL 的完全不明显的“D”与 RDX/EDX/DX/的“d” DL

关于c - FreeBSD 系统调用比 Linux 破坏更多的寄存器?内联汇编优化级别之间的不同行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66878250/

相关文章:

c++ - Shell/Makefile 链接器

linux - 尝试理解 sys_socketcall 参数

C 读取写入文件

c - FreeRTOS 中 prvGetInterruptControllerInstance() 函数所需的头文件是什么?

c++ - Nasm,C++,传递类对象

linux - Linux 如何在 x86-64 中支持超过 512GB 的虚拟地址范围?

c++ - mmap 系统调用返回 -14(-EFAULT??)

linux - Golang,在Linux中加载Windows DLL

perl - 从带有 @ARGV 的嵌套反引号调用时返回变量

c - 如何在基于 Debian 的发行版中找到某个文件所属的包?