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

标签 assembly gcc inline-assembly

正如这个问题的标题中提到的,当我出于临时原因修改 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[输入], %[low_byte] ; shr​​ $16, %[input],您需要一个寄存器,它的第二个 8 位 block 别名为高 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?

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

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


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 指令本身在内联 asm 中。但是 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 on the Godbolt compiler explorer ),因为函数调用约定几乎与系统调用约定匹配:

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

在任何输入寄存器上使用 clobber 并将 mov 放入 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 语句的直接输入或输出。不过,这种情况很少见,除非您在内联 asm 中编写整个循环或大块代码。

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

关于assembly - 对于 asm 语句中的临时寄存器,我应该使用 clobber 还是虚拟输出?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54061267/

相关文章:

c - GCC 内联汇编错误 : "Operand size mismatch for ' int'"

linux - 使用交叉编译器为 arm 编译 native GCC

c++ - 三元运算符缓存不友好吗?

c++ asm 将指针移动到寄存器 = 访问冲突错误

c - 通过 x86 汇编进行零除检查的 C 除法宏

c - 将汇编语言翻译成 C [使用 PIC18 写入内存]

汇编 (x86) 循环段错误

c++ - std::vector::push_back() 不能在 MSVC 上为具有已删除移动构造函数的对象编译

gcc - 我的第一个汇编程序出错(GCC 内联汇编)

gcc - ASM x86 相对 JMP