我需要这样的内联汇编代码:
像这样:
__asm__ __volatile__ ("push %%eax\n\t"
// ... some operations that use ECX as a temporary
"mov %0, %%ecx\n\t"
// ... some other operation
"pop %%eax"
: : "m"(foo));
// foo is my local variable, that is to say, on stack
反汇编编译后的代码时,编译器给出的内存地址如
0xc(%esp)
,它是相对于 esp
,因此,这段代码将无法正常工作,因为我有一个 push
操作前mov
.因此,我怎么能告诉编译我不喜欢
foo
相对于 esp
,但任何类似 -8(%ebp)
相对于 ebp。附言您可以建议我可以放
eax
在 Clobbers 里面,但它只是一个示例代码 .我不想展示我不接受此解决方案的完整原因。
最佳答案
当您有任何内存输入/输出时,通常应该避免在 inline-asm 中修改 ESP,因此您不必禁用优化或强制编译器以其他方式使用 EBP 制作堆栈帧。一大优势是然后您(或编译器)可以使用 EBP 作为额外的空闲寄存器 ;如果您已经不得不溢出/重新加载东西,则可能会显着加速。如果您正在编写内联汇编,大概这是一个热点,因此值得花费额外的代码大小来使用 ESP 相对寻址模式。
在 x86-64 代码中,安全使用 push/pop 还有一个额外的障碍,因为 you can't tell the compiler you want to clobber the red-zone低于 RSP。 (您可以使用 -mno-red-zone
进行编译,但无法从 C 源代码中禁用它。)您可能会遇到问题 like this您在堆栈上破坏编译器数据的地方。但是,没有 32 位 x86 ABI 具有红色区域,因此这仅适用于 x86-64 System V。(或具有红色区域的非 x86 ISA。)
您只需要-fno-omit-frame-pointer
对于那个函数,如果你想做像 push
这样的 asm-only 的东西作为堆栈数据结构,因此存在可变数量的推送。或者如果优化代码大小。
你总是可以在 asm 中编写一个完整的非内联函数并将其放在一个单独的文件中,然后你就有了完全的控制权。但只有在您的函数足够大以值得调用/ret 开销时才这样做,例如如果它包括一个完整的循环;不要让编译器call
C 内部循环中的一个简短的非循环函数,破坏所有调用破坏的寄存器,并且必须确保全局变量同步。
看来您正在使用 push
/pop
内联汇编,因为你没有足够的寄存器,需要保存/重新加载一些东西。 您不需要使用 push/pop 进行保存/恢复。相反,使用带有 "=m"
的虚拟输出操作数让编译器为您分配堆栈空间的约束 ,并使用 mov
到/从这些插槽。 (当然,您不仅限于 mov
;如果您只需要一次或两次该值,那么将内存源操作数用于 ALU 指令可能是一种胜利。)
这对于代码大小来说可能会稍差一些,但对于性能来说通常不会更差(并且可能会更好)。如果这还不够好,请在 asm 中编写整个函数(或整个循环),这样您就不必与编译器搏斗。
int foo(char *p, int a, int b) {
int t1,t2; // dummy output spill slots
int r1,r2; // dummy output tmp registers
int res;
asm ("# operands: %0 %1 %2 %3 %4 %5 %6 %7 %8\n\t"
"imull $123, %[b], %[res]\n\t"
"mov %[res], %[spill1]\n\t"
"mov %[a], %%ecx\n\t"
"mov %[b], %[tmp1]\n\t" // let the compiler allocate tmp regs, unless you need specific regs e.g. for a shift count
"mov %[spill1], %[res]\n\t"
: [res] "=&r" (res),
[tmp1] "=&r" (r1), [tmp2] "=&r" (r2), // early-clobber
[spill1] "=m" (t1), [spill2] "=&rm" (t2) // allow spilling to a register if there are spare regs
, [p] "+&r" (p)
, "+m" (*(char (*)[]) p) // dummy in/output instead of memory clobber
: [a] "rmi" (a), [b] "rm" (b) // a can be an immediate, but b can't
: "ecx"
);
return res;
// p unused in the rest of the function
// so it's really just an input to the asm,
// which the asm is allowed to destroy
}
这与
gcc7.3 -O3 -m32
编译为以下 asm on the Godbolt compiler explorer .请注意显示编译器为所有模板操作数选择的内容的 asm 注释:它选择了 12(%esp)
为 %[spill1]
和 %edi
为 %[spill2]
(因为我使用 "=&rm"
作为该操作数,所以编译器在 asm 之外保存/恢复 %edi
,并将其提供给我们作为该虚拟操作数)。foo(char*, int, int):
pushl %ebp
pushl %edi
pushl %esi
pushl %ebx
subl $16, %esp
movl 36(%esp), %edx
movl %edx, %ebp
#APP
# 19 "/tmp/compiler-explorer-compiler118120-55-w92ge8.v797i/example.cpp" 1
# operands: %eax %ebx %esi 12(%esp) %edi %ebp (%edx) 40(%esp) 44(%esp)
imull $123, 44(%esp), %eax
mov %eax, 12(%esp)
mov 40(%esp), %ecx
mov 44(%esp), %ebx
mov 12(%esp), %eax
# 0 "" 2
#NO_APP
addl $16, %esp
popl %ebx
popl %esi
popl %edi
popl %ebp
ret
嗯,告诉编译器我们修改哪个内存的虚拟内存操作数似乎导致了专用寄存器,我猜是因为
p
操作数是 early-clobber,所以它不能使用相同的寄存器。如果您确信其他输入都不会使用与 p
相同的寄存器,我想您可能会冒着放弃早期破坏的风险。 . (即它们没有相同的值)。
关于带堆栈操作的 GCC 内联汇编,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48853757/