我目前正在学习汇编的基础知识,并且在查看gcc(6.1.1)生成的指令时遇到了一些奇怪的事情。
来源如下:
#include <stdio.h>
int foo(int x, int y){
return x*y;
}
int main(){
int a = 5;
int b = foo(a, 0xF00D);
printf("0x%X\n", b);
return 0;
}
用于编译的命令:gcc -m32 -g test.c -o test
在检查gdb中的功能时,我得到了:
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
0x080483f7 <+0>: lea ecx,[esp+0x4]
0x080483fb <+4>: and esp,0xfffffff0
0x080483fe <+7>: push DWORD PTR [ecx-0x4]
0x08048401 <+10>: push ebp
0x08048402 <+11>: mov ebp,esp
0x08048404 <+13>: push ecx
0x08048405 <+14>: sub esp,0x14
0x08048408 <+17>: mov DWORD PTR [ebp-0xc],0x5
0x0804840f <+24>: push 0xf00d
0x08048414 <+29>: push DWORD PTR [ebp-0xc]
0x08048417 <+32>: call 0x80483eb <foo>
0x0804841c <+37>: add esp,0x8
0x0804841f <+40>: mov DWORD PTR [ebp-0x10],eax
0x08048422 <+43>: sub esp,0x8
0x08048425 <+46>: push DWORD PTR [ebp-0x10]
0x08048428 <+49>: push 0x80484d0
0x0804842d <+54>: call 0x80482c0 <printf@plt>
0x08048432 <+59>: add esp,0x10
0x08048435 <+62>: mov eax,0x0
0x0804843a <+67>: mov ecx,DWORD PTR [ebp-0x4]
0x0804843d <+70>: leave
0x0804843e <+71>: lea esp,[ecx-0x4]
0x08048441 <+74>: ret
End of assembler dump.
(gdb) disas foo
Dump of assembler code for function foo:
0x080483eb <+0>: push ebp
0x080483ec <+1>: mov ebp,esp
0x080483ee <+3>: mov eax,DWORD PTR [ebp+0x8]
0x080483f1 <+6>: imul eax,DWORD PTR [ebp+0xc]
0x080483f5 <+10>: pop ebp
0x080483f6 <+11>: ret
End of assembler dump.
令我感到困惑的部分是它正在尝试使用堆栈。
据我了解,这是做什么的:
首先,它引用了堆栈中高出4个字节的某个内存地址,据我所知应该是传递给main的变量,因为esp当前指向内存中的返回地址。
其次,出于性能原因,它将堆栈对齐到0边界。
第三,它压入新的堆栈区域ecx + 4,这应该转换为将我们假定要返回的地址压入堆栈。
第四,它将旧的框架指针推入堆栈并设置新的框架指针。
第五,它将ecx(仍指向它应该是main的参数)推入堆栈。
程序将执行应做的事情,并开始返回过程。
首先,它通过在ebp上使用-0x4偏移来恢复ecx,该偏移应访问第一个局部变量。
其次,它执行离开指令,该指令实际上只是将esp设置为ebp,然后从堆栈中弹出ebp。
因此,堆栈上的下一个内容是返回地址,并且esp和ebp寄存器应该返回到返回所需的状态了吗?
显然不是因为接下来要做的是用ecx-0x4加载esp,因为ecx仍然指向传递给main的变量,所以应该将其放在堆栈上的返回地址地址。
这工作得很好,但是引起了一个问题,为什么它会在第3步中麻烦将返回地址放到堆栈上,因为它在实际上从函数返回之前将堆栈返回到末尾的原始位置。
最佳答案
更新:gcc8至少在正常用例(-fomit-frame-pointer
,并且没有alloca
或需要可变大小分配的C99 VLA)中简化了此操作。可能是由于AVX使用量的增加导致更多功能需要32字节对齐的本地或数组而引起的。
另外,可能是What's up with gcc weird stack manipulation when it wants extra stack alignment?的副本
如果仅运行几次(例如在32位代码中的main
开头),则此复杂的序言就可以了,但是它看起来越多,就越有必要对其进行优化。 GCC有时仍会在函数中对堆栈进行过度对齐,在这些函数中,所有> 16字节对齐的对象都已优化到寄存器中,这已经错过了优化,但是当堆栈对齐更便宜时,这种情况就不那么糟糕了。
即使在启用优化的情况下,对齐函数中的堆栈时,gcc也会生成一些笨拙的代码。我有一个可能的理论(见下文),为什么gcc可能将返回地址复制到刚好保存ebp
的上面,以形成堆栈框架(是的,我同意这就是gcc在做的事情)。在此函数中看起来没有必要,而clang并没有做任何类似的事情。
除此之外,ecx
的废话可能只是gcc并没有优化掉其对齐堆栈样板中不需要的部分。 (需要esp
的预对齐值来引用堆栈上的args,因此将第一个可能为arg的地址放入寄存器中是有意义的)。
通过32位代码的优化,您会看到相同的结果(即使当前版本的ABI要求在进程启动时使用gcc生成的main
也不假定16B堆栈对齐,而调用< cc>要么对齐堆栈本身,要么保留内核提供的初始对齐(我忘记了)。您还会在将堆栈对齐到大于16B的函数中看到这一点(例如,使用main
类型的函数,有时即使它们从未溢出到堆栈中。或者带有C ++ 11 __m256
声明的数组的函数,或任何其他要求对齐的方式。)在64位代码中,gcc似乎总是为此使用alignas(32)
而不是r10
。
gcc的执行方式不需要ABI合规性,因为clang的功能要简单得多。
我添加了一个对齐变量(使用rcx
作为强制编译器实际上在其堆栈上为其保留对齐空间而不是对其进行优化的一种简单方法)。我将您的代码on the Godbolt compiler explorer放入volatile
的汇编中。我在gcc 4.9、5.3和6.1中看到了相同的行为,但是在clang中却看到了不同的行为。
int main(){
__attribute__((aligned(32))) volatile int v = 1;
return 0;
}
Clang3.8的
-O3
输出在功能上与其-O3 -m32
输出相同。请注意,-m64
启用-O3
,但是某些功能仍然会生成堆栈帧。 push ebp
mov ebp, esp # make a stack frame *before* aligning, so ebp-relative addressing can only access stack args, not aligned locals.
and esp, -32
sub esp, 32 # esp is 32B aligned with 32 or 48B above esp reserved (depending on incoming alignment)
mov dword ptr [esp], 1 # store v
xor eax, eax # return 0
mov esp, ebp # leave
pop ebp
ret
gcc的输出在
-fomit-frame-pointer
和-m32
之间几乎相同,但是它将-m64
和v
放在red-zone中,因此-m64
输出有两个额外的指令: # gcc 6.1 -m32 -O3 -fverbose-asm. Most of gcc's comment lines are empty. I guess that means it has no idea why it's emitting those insns :P
lea ecx, [esp+4] #, get a pointer to where the first arg would be
and esp, -32 #, align
xor eax, eax # return 0
push DWORD PTR [ecx-4] # No clue WTF this is for; this looks batshit insane, but happens even in 64bit mode.
push ebp # make a stackframe, even though -fomit-frame-pointer is on by default and we can already restore the original esp from ecx (unlike clang)
mov ebp, esp #,
push ecx # save the old esp value (even though this function doesn't clobber ecx...)
sub esp, 52 #, reserve space for v (not present with -m64)
mov DWORD PTR [ebp-56], 1 # v,
add esp, 52 #, unreserve (not present with -m64)
pop ecx # restore ecx (even though nothing clobbered it)
pop ebp # at least it knows it can just pop instead of `leave`
lea esp, [ecx-4] #, restore pre-alignment esp
ret
似乎gcc想要在对齐堆栈后制作其堆栈框架(使用
-m32
)。我想这很有意义,因此它可以引用相对于push ebp
的本地语言。否则,如果要对齐本地,则必须使用ebp
相对寻址。我关于gcc为什么这样做的理论:
对齐后但按
esp
之前返回地址的额外副本意味着返回地址相对于保存的ebp
值(以及调用子函数时将在ebp
中的值)复制到预期位置)。因此,通过遵循堆栈框架的链接列表并查看返回地址以找出涉及的功能,这确实可以帮助希望放松堆栈的代码。我不确定这是否与现代堆栈展开信息有关,该信息允许通过
ebp
展开堆栈(回溯/异常处理)。 (这是-fomit-frame-pointer
部分中的元数据。这是围绕.eh_frame
进行每次修改的.cfi_*
指令的目的。)我应该看看clang在非叶函数中必须对齐堆栈时所执行的操作。函数内部需要
esp
的原始值,以引用堆栈上的函数args。我认为gcc不知道如何优化其align-the-stack方法中不需要的部分。 (例如out esp
不会查看其args(并且声明不接受任何参数))这种代码生成是您在需要对齐堆栈的函数中看到的典型代码。这并不奇怪,因为使用了带有自动存储功能的
main
。
关于assembly - 为什么gcc会产生额外的寄信人地址?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38781118/