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

时间:2019-01-06 11:58:18

标签: gcc assembly

正如该问题的标题中所述,当我出于临时原因修改asm语句中的某些寄存器时,在clobber和虚拟输出之间哪个选项更好?

例如,我在link中实现了交换功能的两个版本,发现两个版本生成相同数量的输出指令。

我应该使用哪个版本?我是否应该将其与虚拟输出一起使用,以允许编译器选择可以尽可能优化整个功能的寄存器?

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

1 个答案:

答案 0 :(得分:2)

您通常应该让编译器使用带有任何必需约束 1 的早期漏洞虚拟输出来为您选择寄存器。这样可以灵活地为该功能进行寄存器分配。

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

  

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

由于函数可以内联,可能插入调用其他函数的循环中,因此编译器希望将其输入保留在调用保留的寄存器中。如果您要编写一个独立的函数,编译器始终必须调用,从内联asm而不是独立调用中获得的全部是编译器处理调用约定差异和C ++名称处理。

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


  

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

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

// 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指令,也可以。 (很难安全地执行此操作,尤其是由于存在红色区域,但是人们确实会尝试这样做)。您不必选择要注册哪个代码伪造者,因此只需将其告知编译器即可。