首页 > 解决方案 > 对于 asm 语句中的临时寄存器,我应该使用 clobber 还是虚拟输出?

问题描述

正如这个问题的标题中提到的,当我修改 asm 语句中的一些寄存器时,出于临时原因,在 clobber 和 dummy 输出之间哪个选项更好?

比如我在链接中实现了两个版本的交换功能,发现两个版本产生的输出指令量是一样的。

我应该使用哪个版本?我应该使用带有虚拟输出的那个来允许编译器选择可以尽可能优化整个功能的寄存器吗?

如果答案是肯定的,那么我应该什么时候使用clobber 列表?只有当一条指令要求您将其操作数加载到特定寄存器时才可以使用clobber列表吗?比如syscall指令要求它的参数应该位于寄存器rdi rsi rdx r10 r8 r9??

标签: assemblygccinline-assembly

解决方案


您通常应该让编译器为您选择寄存器,使用具有任何所需约束1的 early-clobber 虚拟输出。这使得它可以灵活地为函数进行寄存器分配。

1 例如,您可以+&Q用来获取 RAX/RBX/RCX/RDX 之一:具有 AH/BH/CH/DH 的寄存器。movzbl %h[input], %[high_byte]
如果你想用;解压 8 位字段 movzbl %b[input], %[low_byte]; shr $16, %[input],你需要一个寄存器,它的第二个 8 位块别名为高 8 寄存器。

出于好奇,当我们考虑 amd64 的调用约定时,一些寄存器可以在函数内部自由使用;我们可以只使用 asm 语句中的那些寄存器来实现一些功能。为什么允许编译器选择要使用的寄存器比上面提到的要好?

因为函数可以内联,可能会进入调用其他函数的循环,因此编译器会希望在保留调用的寄存器中为其提供输入。如果您正在编写编译器始终必须调用的独立函数,那么您从内联 asm 而不是独立函数中得到的只是编译器处理调用约定差异和 C++ 名称修改。

或者,周围的代码可能使用了一些需要固定寄存器的指令,例如cl移位计数或 RDX:RAX 用于div.


我什么时候应该使用clobber列表?...例如syscall指令要求其参数应位于寄存器rdi rsi rdx r10 r8 r9??

通常您会改用输入约束,因此只有syscall指令本身位于内联汇编中。但是syscall(指令本身)破坏了 RCX 和 R11,因此使用它进行的系统调用不可避免地会破坏用户空间的 RCX 和 R11。除非您使用返回地址 (RCX) 或 RFLAGS (R11),否则对这些使用虚拟输出是没有意义的。所以是的,clobbers 在这里很有用。

// the compiler will emit all the necessary MOV instructions
#include <stddef.h>
#include <asm/unistd.h>

// the compiler will emit all the necessary MOV instructions
//static inline 
size_t sys_write(int fd, const char *buf, size_t len) {
    size_t retval;
    asm volatile("syscall"
        : "=a"(retval)  //   EDI     RSI       RDX
        : "a"(__NR_write), "D"(fd), "S"(buf), "d"(len)
         , "m"(*(char (*)[len]) buf)   // dummy memory input: the asm statement reads this memory
        : "rcx", "r11"    // clobbered by syscall
           // , "memory"  // would be needed if we didn't use a dummy memory input
    );
    return retval;
}

这个的非内联版本编译如下(gcc -O3在 Godbolt 编译器资源管理器上),因为函数调用约定几乎匹配系统调用约定:

sys_write(int, char const*, unsigned long):
    movl    $1, %eax
    syscall
    ret

在任何输入寄存器上使用 clobbers 并将 amov放入 asm 中是非常愚蠢的:

size_t dumb_sys_write(int fd, const char *buf, size_t len) {
    size_t retval;
    asm volatile(
        "mov %[fd], %%edi\n\t"
        "mov %[buf], %%rsi\n\t"
        "mov %[len], %%rdx\n\t"
        "syscall"
        : "=a"(retval)  //   EDI     RSI       RDX
        : "a"(__NR_write), [fd]"r"(fd), [buf]"r"(buf), [len]"r"(len)
         , "m"(*(char (*)[len]) buf)   // dummy memory input: the asm statement reads this memory
        : "rdi", "rsi", "rdx", "rcx", "r11"
           // , "memory"  // would be needed if we didn't use a dummy memory input
    );

    // if(retval > -4096ULL) errno = -retval;

    return retval;
}

dumb_sys_write(int, char const*, unsigned long):
    movl    %edi, %r9d
    movq    %rsi, %r8
    movq    %rdx, %r10
    movl    $1, %eax     # compiler generated before this
  # from inline asm
    mov %r9d, %edi
    mov %r8, %rsi
    mov %r10, %rdx
    syscall
  # end of inline asm
    ret

除此之外,您不会让编译器利用syscall 不会破坏其任何输入寄存器的事实。编译器很可能仍然需要len一个寄存器,并且使用纯输入约束让它知道该值之后仍然存在。


如果您使用隐式使用某些寄存器的任何指令,您也可以使用clobbers,但这些指令的输入和输出都不是 asm 语句的直接输入或输出。不过,这种情况很少见,除非您在内联汇编中编写整个循环或大块代码。

或者,如果您正在包装call指令。(很难安全地做到这一点,尤其是因为有红区,但人们确实会尝试这样做)。您无法选择哪些注册了代码破坏者,因此您只需告诉编译器即可。


推荐阅读