gcc - 在扩展内联 ASM 中调用 printf

标签 gcc printf 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 而不是字符串的实际地址。

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

最佳答案

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

我不建议您按照您的建议进行操作(在内联汇编程序中进行函数调用)。编译器将很难优化事物。很容易把事情弄错。除非绝对必要,否则 David Wohlferd 写了一篇关于 reasons not to use inline assembly 的非常好的文章。

除其他外,64-bit System V ABI 要求一个 128 字节的红色区域。这意味着您不能在没有潜在损坏的情况下将任何内容插入堆栈。请记住:执行 CALL 会将返回地址压入堆栈。解决这个问题的快速而肮脏的方法是在您的内联汇编器启动时从 RSP 中减去 128,然后在完成时将 128 加回来。

The 128-byte area beyond the location pointed to by %rsp is considered to be reserved and shall not be modified by signal or interrupt handlers.8 Therefore, functions may use this area for temporary data that is not needed across function calls. In particular, leaf functions may use this area for their entire stack frame, rather than adjusting the stack pointer in the prologue and epilogue. This area is known as the red zone.



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

The end of the input argument area shall be aligned on a 16 (32, if __m256 is passed on stack) byte boundary. In other words, the value (%rsp + 8) is always a multiple of 16 (32) when control is transferred to the function entry point.



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

In context of the C programming language, function arguments are pushed on the stack in the reverse order. In Linux, GCC sets the de facto standard for calling conventions. Since GCC version 4.5, the stack must be aligned to a 16-byte boundary when calling a function (previous versions only required a 4-byte alignment.)



由于我们在内联汇编程序中调用 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 的函数通常不是最好的处理方式。

关于gcc - 在扩展内联 ASM 中调用 printf,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37502841/

相关文章:

c - printf在cygwin中执行.exe文件

c - 通过 '%c' 打印换行符时,Printf 部分忽略格式字符串

c - 是 printf(c);是有效的语法(其中 c 是字符串文字)?

assembly - packuswb 指令是如何工作的? (在低电平位操作)

assembly - 在64位模式下使用MOV指令将SS设置为0x0000是否会导致故障#GP(0)?

c++ - 模板转换函数到 const-reference

c - 加载图形文件

assembly - Intel x86_64 程序集比较带符号的 double float

当我认为它应该在 linux 上时,c fputc 不返回错误,在 windows 上按预期工作

c - 函数定义和外部声明不同;但是 GCC 甚至没有警告并通过了编译