对于 asm 语句中的临时寄存器,我应该使用 clobber 还是虚拟输出?

For temporary registers in the asm statement, should I use clobber or dummy output?

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

比如我在link中实现了两个版本的交换函数,发现两个版本生成的输出指令数量是一样的。

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

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

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

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

Out of curiosity, when we consider a calling convention of amd64, some registers can be freely used inside the functions; and we could implement some functions by only using those registers inside the asm statement. Why allowing the compiler to choose the registers to be used is better than the mentioned one?

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

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


when should I use the clobber list? ... such as syscall instruction requires its parameter should be located in register rdi rsi rdx r10 r8 r9??

通常你会使用输入约束,所以只有 syscall 指令本身在内联汇编中。但是 syscall(指令本身)会破坏 RCX 和 R11,因此使用它进行的系统调用不可避免地会破坏用户 space 的 RCX 和 R11。对这些使用虚拟输出没有意义,除非您使用 return 地址 (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 on the Godbolt compiler explorer),因为函数调用约定几乎与系统调用约定相匹配:

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

在任何输入寄存器上使用 clobber 并在 asm:

中放置一个 mov 真的很愚蠢
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    , %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,并且使用纯输入约束让它知道该值之后仍然存在。


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

或者如果您正在包装 call 指令。 (很难安全地做到这一点,特别是因为红区,但人们确实尝试这样做)。您无法选择代码破坏的寄存器,因此您只需将其告知编译器即可。