我试图在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
,而不是字符串的实际地址。
我无法解释为什么,似乎在第一次调用后堆栈已损坏?我是否必须以某种方式恢复它?
答案 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的函数通常不是最好的方法。