我试图在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。
超出%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的函数通常不是最好的方法。