在扩展内联ASM中调用printf

问题描述 投票:2回答:1

我试图在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而不是字符串的实际地址。

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

gcc 64bit x86-64 inline-assembly calling-convention
1个回答
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字节区域被认为是保留的,不应被信号或中断处理程序修改。因此,函数可以将此区域用于函数调用不需要的临时数据。特别是,叶子函数可以将这个区域用于它们的整个堆栈帧,而不是调整序言和尾声中的堆栈指针。这个区域被称为红区。

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

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

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

在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的函数通常不是最好的方法。

© www.soinside.com 2019 - 2024. All rights reserved.