在扩展内联ASM中调用printf

时间:2016-05-28 19:05:07

标签: gcc 64-bit x86-64 inline-assembly calling-convention

我试图在64位Linux上的 GCC 中的扩展内联ASM中输出相同的字符串两次。

int main()
{
    const char* test = "test\n";

    asm(
        "movq %[test], %%rdi\n"    // Debugger shows rdi = *address of string*  
        "movq $0, %%rax\n"

        "push %%rbp\n"
        "push %%rbx\n"
        "call printf\n"         
        "pop %%rbx\n"
        "pop %%rbp\n"

        "movq %[test], %%rdi\n" // Debugger shows rdi = 0
        "movq $0, %%rax\n"

        "push %%rbp\n"
        "push %%rbx\n"
        "call printf\n"     
        "pop %%rbx\n"
        "pop %%rbp\n"
        : 
        :  [test] "g" (test)
        : "rax", "rbx","rcx", "rdx", "rdi", "rsi", "rsp"
        );

    return 0;
}

现在,字符串只输出一次。我尝试了很多东西,但我想我错过了关于调用约定的一些注意事项。我甚至不确定clobber列表是否正确,或者我是否需要保存和恢复 RBP RBX

为什么字符串不会输出两次?

使用调试器查看我,以某种方式将字符串第二次加载到rdi时,它的值为0,而不是字符串的实际地址。

我无法解释为什么,似乎在第一次调用后堆栈已损坏?我是否必须以某种方式恢复它?

1 个答案:

答案 0 :(得分:8)

代码的特定问题: RDI 不会在函数调用中维护(参见下文)。在第一次调用printf之前它是正确的,但被printf破坏了。您需要先将其暂存到其他地方。一个没有被破坏的寄存器将很方便。然后,您可以在printf之前保存副本,然后将其复制回 RDI

我不建议您执行您的建议(在内联汇编程序中进行函数调用)。编译器很难优化。弄错了很容易。除非绝对必要,David Wohlferd在reasons not to use inline assembly上写了一篇非常好的文章。

除其他事项外,64-bit System V ABI要求一个128字节的红色区域。这意味着你不能将任何东西推到堆栈上而不会有潜在的腐败。请记住:执行 CALL 会在堆栈上推送返回地址。解决此问题的快速而肮脏的方法是在内联汇编程序启动时从 RSP 中减去128,然后在完成后再添加128。

  

%rsp指向的位置之外的128字节区域被认为是   保留,不得被信号或中断处理程序修改.8因此,   函数可以将此区域用于跨函数不需要的临时数据   调用。特别是,叶子函数可以将这个区域用于它们的整个堆栈帧,   而不是在序言和尾声中调整堆栈指针。这个区域是   被称为红区。

另一个需要关注的问题是在任何函数调用之前,要求堆栈为16字节对齐(或者可能是32字节对齐,具体取决于参数)。这也是64位ABI所必需的:

  

输入参数区域的末尾应对齐16(32,如果__m256是   传递堆栈)字节边界。换句话说,值(%rsp + 8)始终是   当控制转移到函数入口点时,为16(32)的倍数。

注意 CALL 对函数的16字节对齐的要求对于 GCC >也是required on 32-bit Linux。 = 4.5:

  

在C编程语言的上下文中,函数参数以相反的顺序被压入堆栈。在Linux中,GCC为调用约定设定了事实上的标准。从GCC 4.5版开始,在调用函数时,堆栈必须与16字节边界对齐(以前的版本只需要4字节对齐。)

由于我们在内联汇编程序中调用printf,因此我们应确保在进行调用之前将堆栈对齐到16字节边界。

您还必须注意,在调用函数时,某些寄存器会在函数调用中保留,而某些寄存器则不会。具体而言,可能被函数调用破坏的那些在64位ABI的图3.4中列出(参见前面的链接)。这些寄存器是 RAX RCX RDX RD8 - RD11 XMM0 - XMM15 MMX0 - MMX7 ST0 - ST7 。这些都可能被破坏,所以如果它们不出现在输入和输出约束中,应该放在clobber列表中。

以下代码应该满足大多数条件,以确保调用另一个函数的内联汇编程序不会无意中破坏寄存器,保留redzone,并在调用之前保持16字节对齐:

int main()
{
    const char* test = "test\n";
    long dummyreg; /* dummyreg used to allow GCC to pick available register */

    __asm__ __volatile__ (
        "add $-128, %%rsp\n\t"   /* Skip the current redzone */
        "mov %%rsp, %[temp]\n\t" /* Copy RSP to available register */
        "and $-16, %%rsp\n\t"    /* Align stack to 16-byte boundary */
        "mov %[test], %%rdi\n\t" /* RDI is address of string */
        "xor %%eax, %%eax\n\t"   /* Variadic function set AL. This case 0 */
        "call printf\n\t"
        "mov %[test], %%rdi\n\t" /* RDI is address of string again */
        "xor %%eax, %%eax\n\t"   /* Variadic function set AL. This case 0 */
        "call printf\n\t"
        "mov %[temp], %%rsp\n\t" /* Restore RSP */
        "sub $-128, %%rsp\n\t"   /* Add 128 to RSP to restore to orig */
        :  [temp]"=&r"(dummyreg) /* Allow GCC to pick available output register. Modified
                                    before all inputs consumed so use & for early clobber*/
        :  [test]"r"(test),      /* Choose available register as input operand */
           "m"(test)             /* Dummy constraint to make sure test array
                                    is fully realized in memory before inline
                                    assembly is executed */
        : "rax", "rcx", "rdx", "rsi", "rdi", "r8", "r9", "r10", "r11",
          "xmm0","xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7",
          "xmm8","xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15",
          "mm0","mm1", "mm2", "mm3", "mm4", "mm5", "mm6", "mm6",
          "st", "st(1)", "st(2)", "st(3)", "st(4)", "st(5)", "st(6)", "st(7)"
        );

    return 0;
}

我使用输入约束来允许模板选择用于传递str地址的可用寄存器。这可确保我们有一个寄存器,用于在str的调用之间存储printf地址。我还得到汇编程序模板,通过使用虚拟寄存器来临时选择存储 RSP 的可用位置。所选择的寄存器不包括已经选择/列为输入/输出/ clobber操作数的任何寄存器。

这看起来非常混乱,但是如果程序变得更加复杂,那么未能正确执行此操作可能会导致问题。这就是为什么在内联汇编程序中调用符合System V 64位ABI的函数通常不是最好的方法。