GCC编译器提供了 -fomit-frame-pointer
和 -fno-omt-frame-pointer
两个相对的编译选项。
- GCC手册[1]里对
-fomit-frame-pointer
的说明:
Omit the frame pointer in functions that don’t need one. This avoids the instructions to save, set up and restore the frame pointer; on many targets it also makes an extra register available.
On some targets this flag has no effect because the standard calling sequence always uses a frame pointer, so it cannot be omitted.
Note that -fno-omit-frame-pointer doesn’t guarantee the frame pointer is used in all functions. Several targets always omit the frame pointer in leaf functions.
Enabled by default at -O1 and higher.
该说明的大意就是如果函数不需要frame pointer,就不要将frame pointer保留在寄存器中。当打开优化选项:-O
,-O2
, -O3
, -Os
时或者对某些平台不打开任何优化选项时,-fomit-frame-pointer
会被默认打开,可以通过设置 -fno-omit-frame-pointer
关闭 -fomit-frame-pointer
。
- 什么是 frame pointer ?
所谓的 frame pointer(FP) 即 stack frame pointer 栈帧指针。每个进程的栈空间为一帧,FP指向当前进程栈空间的栈底(stack bottom)。而 stack pointer(SP) 即堆栈指针,总是指向栈顶(stack top)。
图片来源 [2]
在多进程环境中,每个进程都有自己的栈空间,但所有进程的栈空间都在同一块存储空间,怎么确定各进程的栈呢?这就要看FP和SP,FP指向栈底,SP指向栈顶,这样,一个进程的栈空间就确定了。
通过回溯栈帧就可以追踪代码的调用过程和调用时的参数。
-
-fomit-frame-pointer
对编译结果的影响
GCC 版本 7.5.0 Target X86_64
示例代码:
int add(int a,int b){
return a+b;
}
int mul(int a,int b){
return a*b;
}
int ma(int a,int b,int c){
return mul(add(a,b),c);
}
关闭 -fomit-frame-pointer
编译标识
编译命令:
-fno-omit-frame-pointer
编译结果:[3]
add(int, int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
mov edx, DWORD PTR [rbp-4]
mov eax, DWORD PTR [rbp-8]
add eax, edx
pop rbp
ret
mul(int, int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
mov eax, DWORD PTR [rbp-4]
imul eax, DWORD PTR [rbp-8]
pop rbp
ret
ma(int, int, int):
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
mov DWORD PTR [rbp-12], edx
mov edx, DWORD PTR [rbp-8]
mov eax, DWORD PTR [rbp-4]
mov esi, edx
mov edi, eax
call add(int, int)
mov edx, eax
mov eax, DWORD PTR [rbp-12]
mov esi, eax
mov edi, edx
call mul(int, int)
leave
ret
打开 -fomit-frame-pointer
编译标识
编译命令:
-fomit-frame-pointer
编译结果:[4]
add(int, int):
mov DWORD PTR [rsp-4], edi
mov DWORD PTR [rsp-8], esi
mov edx, DWORD PTR [rsp-4]
mov eax, DWORD PTR [rsp-8]
add eax, edx
ret
mul(int, int):
mov DWORD PTR [rsp-4], edi
mov DWORD PTR [rsp-8], esi
mov eax, DWORD PTR [rsp-4]
imul eax, DWORD PTR [rsp-8]
ret
ma(int, int, int):
sub rsp, 16
mov DWORD PTR [rsp+12], edi
mov DWORD PTR [rsp+8], esi
mov DWORD PTR [rsp+4], edx
mov edx, DWORD PTR [rsp+8]
mov eax, DWORD PTR [rsp+12]
mov esi, edx
mov edi, eax
call add(int, int)
mov edx, eax
mov eax, DWORD PTR [rsp+4]
mov esi, eax
mov edi, edx
call mul(int, int)
add rsp, 16
ret
编译结果分析
从上面的编译结果中可以看到,是否忽略frame pointer
会导致编译生成的汇编代码有以下的区别:
func(int, ...):
push rbp
mov rbp, rsp
...
pop rbp
ret
rbp:基址指针寄存器,用于提供堆栈内某个单元的偏移地
rsp:栈顶指针寄存器,提供堆栈栈顶单元的偏移地址
由这一点区别可以看到,当保留frame pointer
时,首先将保存在rbp
寄存器中的FP入栈(push rbp
);然后将rbp
寄存器设置为本函数的rsp
,即:将基址指针指向栈顶指针(mov rbp,rsp
);最后在函数执行结束时恢复rbp
(pop rbp
)
-
GCC Command Options >> 3.11 Options That Control Optimization ↩
-
《深入理解计算机系统(第三版)》3.7.1 The Run Time Stack ↩