具有堆栈操作的 GCC 内联汇编
GCC inline assembly with stack operation
我需要这样的内联汇编代码:
- 我在程序集
中有一对(所以,它是平衡的)push/pop操作
- 我在内存中也有一个变量(所以,没有注册)作为输入
像这样:
__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.
P.S。你可能会建议 我可以把 eax
放在 Clobbers 里面,但它只是一个示例代码 。我不想说明我不接受这个解决方案的全部原因。
直接使用栈指针引用局部变量可能是使用编译器优化造成的。我认为您可以通过几种方式解决问题:
- 禁用帧指针优化(GCC 中的
-fno-omit-frame-pointer
);
- 在 Clobbers 中插入
esp
,这样编译器就会知道它的值正在被修改(检查你的编译器的兼容性)。
不是把move放到汇编代码里的ecx里,而是直接把操作数放到ecx里:
: : "c"(foo)
当您有任何内存输入/输出时,通常应避免在 inline-asm 中修改 ESP,因此您不必禁用优化或强制编译器使用 EBP 进行一些 stack-frame另一种方式。一个主要优点是 您(或编译器)可以使用 EBP 作为额外的免费寄存器;如果您已经需要 spill/reload 东西,则可能会显着加快速度。如果您正在编写内联汇编,大概这是一个热点,因此值得花费额外的 code-size 来使用 ESP-relative 寻址模式。
在 x86-64 代码中,安全使用 push/pop 存在额外的障碍,因为 you can't tell the compiler you want to clobber the red-zone below RSP. (You can compile with -mno-red-zone
, but there's no way to disable it from the C source.) You can get problems 会破坏编译器在堆栈上的数据。但是,没有 32 位 x86 ABI 具有 red-zone,因此这仅适用于 x86-64 System V。(或具有 red-zone 的非 x86 ISA。)
如果你想做 asm-only 像 push
这样的东西作为堆栈数据结构,你只需要 -fno-omit-frame-pointer
这个函数,所以有可变数量的推送。或者如果针对 code-size.
进行优化
您总是可以在 asm 中编写整个 non-inline 函数并将其放在一个单独的文件中,然后您就拥有了完全的控制权。但是只有当你的函数大到值得 call/ret 的开销时才这样做,例如如果它包含一个完整的循环;不要让编译器 call
成为 C 内部循环中的短 non-looping 函数,破坏所有 call-clobbered 寄存器并且必须确保全局同步。
您似乎在内联 asm 中使用 push
/ pop
因为您没有足够的寄存器,需要 save/reload 一些东西。 您不需要为 save/restore 使用 push/pop。相反,使用具有 "=m"
约束的虚拟输出操作数让编译器为您分配堆栈 space,并使用 mov
to/from 这些插槽。 (当然,您不限于 mov
;如果您只需要一次或两次值,那么将内存源操作数用于 ALU 指令可能是一个胜利。)
这对于 code-size 来说可能稍差,但通常不会对性能造成影响(而且可能会更好)。如果这还不够好,请在 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 3, %[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
on the Godbolt compiler explorer 的 asm。请注意 asm-comment 显示了编译器为所有模板操作数选择了什么:它为 %[spill1]
选择了 12(%esp)
为 %[spill2]
选择了 %edi
(因为我使用了 "=&rm"
用于该操作数,因此编译器 saved/restore %edi
在 asm 之外,并将其提供给我们用于该虚拟操作数)。
foo(char*, int, int):
pushl %ebp
pushl %edi
pushl %esi
pushl %ebx
subl , %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 3, 44(%esp), %eax
mov %eax, 12(%esp)
mov 40(%esp), %ecx
mov 44(%esp), %ebx
mov 12(%esp), %eax
# 0 "" 2
#NO_APP
addl , %esp
popl %ebx
popl %esi
popl %edi
popl %ebp
ret
嗯,告诉编译器我们修改了哪个内存的虚拟内存操作数似乎导致了一个专门用于它的寄存器,我猜是因为 p
操作数是 early-clobber 所以它不能' t 使用相同的寄存器。我想如果您确信 none 的其他输入将使用与 p
相同的寄存器,您可能会冒险离开 early-clobber。 (即它们没有相同的值)。
我需要这样的内联汇编代码:
- 我在程序集 中有一对(所以,它是平衡的)push/pop操作
- 我在内存中也有一个变量(所以,没有注册)作为输入
像这样:
__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.
P.S。你可能会建议 我可以把 eax
放在 Clobbers 里面,但它只是一个示例代码 。我不想说明我不接受这个解决方案的全部原因。
直接使用栈指针引用局部变量可能是使用编译器优化造成的。我认为您可以通过几种方式解决问题:
- 禁用帧指针优化(GCC 中的
-fno-omit-frame-pointer
); - 在 Clobbers 中插入
esp
,这样编译器就会知道它的值正在被修改(检查你的编译器的兼容性)。
不是把move放到汇编代码里的ecx里,而是直接把操作数放到ecx里:
: : "c"(foo)
当您有任何内存输入/输出时,通常应避免在 inline-asm 中修改 ESP,因此您不必禁用优化或强制编译器使用 EBP 进行一些 stack-frame另一种方式。一个主要优点是 您(或编译器)可以使用 EBP 作为额外的免费寄存器;如果您已经需要 spill/reload 东西,则可能会显着加快速度。如果您正在编写内联汇编,大概这是一个热点,因此值得花费额外的 code-size 来使用 ESP-relative 寻址模式。
在 x86-64 代码中,安全使用 push/pop 存在额外的障碍,因为 you can't tell the compiler you want to clobber the red-zone below RSP. (You can compile with -mno-red-zone
, but there's no way to disable it from the C source.) You can get problems
如果你想做 asm-only 像 push
这样的东西作为堆栈数据结构,你只需要 -fno-omit-frame-pointer
这个函数,所以有可变数量的推送。或者如果针对 code-size.
您总是可以在 asm 中编写整个 non-inline 函数并将其放在一个单独的文件中,然后您就拥有了完全的控制权。但是只有当你的函数大到值得 call/ret 的开销时才这样做,例如如果它包含一个完整的循环;不要让编译器 call
成为 C 内部循环中的短 non-looping 函数,破坏所有 call-clobbered 寄存器并且必须确保全局同步。
您似乎在内联 asm 中使用 push
/ pop
因为您没有足够的寄存器,需要 save/reload 一些东西。 您不需要为 save/restore 使用 push/pop。相反,使用具有 "=m"
约束的虚拟输出操作数让编译器为您分配堆栈 space,并使用 mov
to/from 这些插槽。 (当然,您不限于 mov
;如果您只需要一次或两次值,那么将内存源操作数用于 ALU 指令可能是一个胜利。)
这对于 code-size 来说可能稍差,但通常不会对性能造成影响(而且可能会更好)。如果这还不够好,请在 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 3, %[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
on the Godbolt compiler explorer 的 asm。请注意 asm-comment 显示了编译器为所有模板操作数选择了什么:它为 %[spill1]
选择了 12(%esp)
为 %[spill2]
选择了 %edi
(因为我使用了 "=&rm"
用于该操作数,因此编译器 saved/restore %edi
在 asm 之外,并将其提供给我们用于该虚拟操作数)。
foo(char*, int, int):
pushl %ebp
pushl %edi
pushl %esi
pushl %ebx
subl , %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 3, 44(%esp), %eax
mov %eax, 12(%esp)
mov 40(%esp), %ecx
mov 44(%esp), %ebx
mov 12(%esp), %eax
# 0 "" 2
#NO_APP
addl , %esp
popl %ebx
popl %esi
popl %edi
popl %ebp
ret
嗯,告诉编译器我们修改了哪个内存的虚拟内存操作数似乎导致了一个专门用于它的寄存器,我猜是因为 p
操作数是 early-clobber 所以它不能' t 使用相同的寄存器。我想如果您确信 none 的其他输入将使用与 p
相同的寄存器,您可能会冒险离开 early-clobber。 (即它们没有相同的值)。