专业的编程技术博客社区

网站首页 > 博客文章 正文

[Linux C/C++]帧指针寄存器及-fomit-frame-pointer编译选项

baijin 2024-10-20 04:09:34 博客文章 10 ℃ 0 评论

1. 什么是帧指针寄存器和栈指针寄存器?

帧指针寄存器(Frame Pointer Register)是一种寄存器,主要用于存放函数栈帧的栈底指针,即当前活动记录的底部。与之对应的是栈指针寄存器(Stack Pointer Register),它用于存放函数栈顶指针,即下一个压入栈的活动记录的顶部。

  • 在IA-32平台上,一般寄存器%EBP指向当前栈帧的底部 (高地址),为帧指针寄存器;寄存器%ESP指向当前栈帧的顶部 (低地址),为栈指针寄存器。
  • 在x86-64平台上,一般寄存器%RBP是指向当前栈帧底部(高地址),为帧指针寄存器;寄存器%RSP指向当前栈帧的顶部(低地址),为栈指针寄存器。

2. 帧指针寄存器的用途

在函数执行过程中,需要寻址函数的参数和函数中的局部变量,很多编译器都经常使用帧指针寄存器来寻址,如在IA-32上会使用%EBP寄存器,在x86-64上会使用%RBP寄存器,这是因为栈指针寄存器(如IA-32上的%ESP或x86-64上的%RSP)的值是经常变化的,而帧指针寄存器(%EBP或%RBP)的值对一个函数的栈帧来讲是不变的,寻址比较简单。

例: 通过帧指针寄存器寻址函数参数

源码: test1.c

int func()
{
char c = 1;
short s = 2;
int i = 3;
char *p = 0;
}

编译:

gcc -g -c test1.c -o test1.o

反汇编:

objdump -S test1.o > test1.o.dump

查看反汇编:

[root:~/work/v1/stack/frame-pointer/test1]# cat test1.o.dump

test1.o: file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <func>:

void func()

{

0: f3 0f 1e fa endbr64

4: 55 push %rbp

5: 48 89 e5 mov %rsp,%rbp

char c = 1;

8: c6 45 f1 01 movb $0x1,-0xf(%rbp)

short s = 2;

c: 66 c7 45 f2 02 00 movw $0x2,-0xe(%rbp)

int i = 3;

12: c7 45 f4 03 00 00 00 movl $0x3,-0xc(%rbp)

char *p = 0;

19: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp)

20: 00

}

21: 90 nop

22: 5d pop %rbp

23: c3 retq

[root:~/work/v1/stack/frame-pointer/test1]#

从反汇编中可以看到程序中是如何对局部变量c,s,i,p寻址的:

  • 对局部变量p寻址: char *p = 0;

movq $0x0,-0x8(%rbp)

这里-0x8(%rbp)表示%rbp偏移-0x8字节的地址处,也就是(%rbp - 0x8)。此mov语句就是在(%rbp - 0x8)地址处存入0x0,当然这里的0x0其实表示一个地址,因为p变量为指针变量,但汇编语言并不关心。

char *为指针类型,在x86-64上需要占用8个字节,所以为了保存char *类型,需要对%rbp减小8个字节。这里之所以减少%rbp,是因为栈向下增长,所以需要减小%rbp。

  • 对局部变量i寻址: int i = 3;

movl $0x3,-0xc(%rbp)

由于int类型需要4字节的存储空间,所以为了保存变量i,需要再把%rbp向下移动0x4个字节,也就是在上面char *p变量的地址再往下移动4个字节,也就是%rbp偏移12个字节,也就是i的地址位于(%rbp - 0xc)处。

  • 对局部变量s寻址: short s = 2;

movw $0x2,-0xe(%rbp)

由于变量s为short类型,也就是至少需要2个字节才能保存,所以需要在int i变量的偏移的基础上再至少偏移2个字节,也就是(%rbp - 0xe)地址处。

  • 对局部变量c寻址: char c = 1;

movb $0x1,-0xf(%rbp)

同上,对于char c变量,需要至少偏移0xf,也就是(%rbp - 0xf)。

我们可以通过下图来展示这些局部变量在栈上的位置:

可见,当采用默认的编译选项时,GCC编译器对局部变量的寻址是通过对帧指针寄存器(%RBP)做偏移而得到的,这对函数参数的寻址是完全类似的,这里不再赘述。

3.GCC编译选项-fomit-frame-pointer及-fno-omit-frame-pointer

无论IA-32还是x86-64,标准调用约定并不显式要求使用帧指针寄存器,编译器可以优化调用栈并且不使用帧指针。GCC的命令行选项“-fomit-frame-pointer”就用于告知GCC不使用帧指针寄存器,此时直接使用栈指针寄存器来访问栈上的数据,而命令行选项”-fno-omit-frame-pointer”则强制GCC使用帧指针寄存器访问栈帧。

GCC的-fomit-frame-pointer编译选项是一种优化选项,它告诉编译器在不需要帧指针的函数中省略帧指针,以减少函数调用开销。GCC手册中对-fomit-frame-pointer的说明如下:

-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 -O(or -O1) and higher.

在不需要帧指针的函数中省略帧指针。这避免了保存、设置和恢复帧指针的指令;在许多目标上,它还提供了额外的寄存器。

在某些平台,此标志没有效果,因为标准调用序列始终使用帧指针,因此不能省略。

请注意,-fno-omit-frame-pointer并不能保证所有函数都使用帧指针。一些目标始终在叶子函数中省略帧指针。

  • 验证-fomit-frame-pointer

同样是上面的test1.c,当我们使用-fomit-frame-pointer编译选项时:

[root:~/work/v1/stack/local_variable]# gcc -g -fomit-frame-pointer -c test1.c - test1.o

查看编译后的反汇编代码:

[root:~/work/v1/stack/frame-pointer/test1]# objdump -S test1.o

test1.o: file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <func>:

void func()

{

0: f3 0f 1e fa endbr64

char c = 1;

4: c6 44 24 f1 01 movb $0x1,-0xf(%rsp)

short s = 2;

9: 66 c7 44 24 f2 02 00 movw $0x2,-0xe(%rsp)

int i = 3;

10: c7 44 24 f4 03 00 00 movl $0x3,-0xc(%rsp)

17: 00

char *p = 0;

18: 48 c7 44 24 f8 00 00 movq $0x0,-0x8(%rsp)

1f: 00 00

}

21: 90 nop

22: c3 retq

[root:~/work/v1/stack/frame-pointer/test1]#

通过反汇编可以看到,由于我们使用了-fomit-frame-pointer编译选项,函数中对于局部变量的寻址,将不再使用帧寄存器%RBP,而是使用%RSP。

  • 验证-fno-omit-frame-pointer

同样是上面的test1.c,当我们使用-fno-omit-frame-pointer编译选项时:

[root:~/work/v1/stack/local_variable]# gcc -g -fno-omit-frame-pointer -c test1.c -o test1.o

[root:~/work/v1/stack/local_variable]#

查看编译后的反汇编代码:

[root:~/work/v1/stack/frame-pointer/test1]# objdump -S test1.o

test1.o: file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <func>:

void func()

{

0: f3 0f 1e fa endbr64

4: 55 push %rbp

5: 48 89 e5 mov %rsp,%rbp

char c = 1;

8: c6 45 f1 01 movb $0x1,-0xf(%rbp)

short s = 2;

c: 66 c7 45 f2 02 00 movw $0x2,-0xe(%rbp)

int i = 3;

12: c7 45 f4 03 00 00 00 movl $0x3,-0xc(%rbp)

char *p = 0;

19: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp)

20: 00

}

21: 90 nop

22: 5d pop %rbp

23: c3 retq

[root:~/work/v1/stack/frame-pointer/test1]#

通过反汇编可以看到,由于我们使用了-fno-omit-frame-pointer编译选项,函数中对于局部变量的寻址,再使用帧寄存器%RBP。

4. GCC对帧指针寄存器的设置

? -O0: 预设-fno-omit-frame-pointer,所有函数都使用frame pointer。这是GCC默认的优化选项。

-O0 Reduce compilation time and make debugging produce the expected results. This is the default.

? -O1或以上: 预设-fomit-frame-pointer,只有必要情况才设置frame pointer。指定-fno-omit-leaf-frame-pointer则可得到类似-O0效果。可以额外指定-momit-leaf-frame-pointer去除leaf functions的frame pointer。

  • 如果明确想保留帧指针或者在某些情况下需要使用帧指针进行调试,可以使用-fno-omit-frame-pointer选项来禁用该选项。

另外,可以使用

__attribute__((optimize("omit-frame-pointer")))

__attribute__((optimize("no-omit-frame-pointer")))

来修饰函数,来明确指定函数是否开启帧指针。

例:通过__attribute__改变函数的frame pointer register的使用情况

源码:/root/work/v1/stack/frame-pointer/test3/

#define _frame_pointer __attribute__((optimize("omit-frame-pointer")))
#define _no_frame_pointer __attribute__((optimize("no-omit-frame-pointer")))

_frame_pointer void foo()
{
return;
}

_no_frame_pointer void foo1()
{
return;
}

_no_frame_pointer int foo2()
{
return 0;
}

编译:

[root:~/work/v1/stack/frame-pointer/test3]# gcc -c test3.c

[root:~/work/v1/stack/frame-pointer/test3]#

检查test.o的反汇编:

[root:~/work/v1/stack/frame-pointer/test3]# objdump -d test3.o

test3.o: file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <foo>:

0: f3 0f 1e fa endbr64

4: 55 push %rbp

5: 48 89 e5 mov %rsp,%rbp

8: 90 nop

9: 5d pop %rbp

a: c3 retq

000000000000000b <foo1>:

b: f3 0f 1e fa endbr64

f: 90 nop

10: c3 retq

0000000000000011 <foo2>:

11: f3 0f 1e fa endbr64

15: b8 00 00 00 00 mov $0x0,%eax

1a: c3 retq

[root:~/work/v1/stack/frame-pointer/test3]#

可以看到,foo()函数的反汇编保留了帧指针,而foo1和foo2()函数没有保留帧指针。

4.1 检查GCC的默认优化选项对函数使用帧指针寄存器的影响

例: 检查GCC默认优化选项对函数使用帧寄存器的影响

源码: /root/work/v1/stack/frame-pointer/test2/

对于采用GCC默认优化选项进行编译时,此时优化选项为-O0

源码:

[root:~/work/v1/stack/frame-pointer/test2]# cat test2.c 
 
void foo()
{
	return;
}

void foo1()
{
        return;
}

int foo2()
{
	return 0;
}
[root:~/work/v1/stack/frame-pointer/test2]#

编译及反汇编:

  • gcc -c test2.c
  • objdump -d test2.o >test2.o.dump

[root:~/work/v1/stack/frame-pointer/test2]# objdump -d test2.o

test2.o: file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <foo>:

0: f3 0f 1e fa endbr64

4: 55 push %rbp

5: 48 89 e5 mov %rsp,%rbp

8: 90 nop

9: 5d pop %rbp

a: c3 retq

000000000000000b <foo1>:

b: f3 0f 1e fa endbr64

f: 55 push %rbp

10: 48 89 e5 mov %rsp,%rbp

13: 90 nop

14: 5d pop %rbp

15: c3 retq

0000000000000016 <foo2>:

16: f3 0f 1e fa endbr64

1a: 55 push %rbp

1b: 48 89 e5 mov %rsp,%rbp

1e: b8 00 00 00 00 mov $0x0,%eax

23: 5d pop %rbp

24: c3 retq

[root:~/work/v1/stack/frame-pointer/test2]#

可以看到,采用GCC默认优化选项进行编译时,此时的优化选项为-O0, 每个函数都会使用帧指针寄存器。所以每个函数的入口都会把帧指针(frame pointer)寄存器压入栈中,当然在退出函数时,再对帧指针寄存器做出栈的操作。

4.2 检查GCC在优化选项为-O1时对函数使用帧指针寄存器的影响

  • gcc -O1 -c test2.c
  • objdump -d test2.o >test2.o.dump

[root:~/work/v1/stack/frame-pointer/test2]# gcc -O1 -c test2.c

[root:~/work/v1/stack/frame-pointer/test2]# objdump -d test2.o

test2.o: file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <foo>:

0: f3 0f 1e fa endbr64

4: c3 retq

0000000000000005 <foo1>:

5: f3 0f 1e fa endbr64

9: c3 retq

000000000000000a <foo2>:

a: f3 0f 1e fa endbr64

e: b8 00 00 00 00 mov $0x0,%eax

13: c3 retq

[root:~/work/v1/stack/frame-pointer/test2]#

可以看到由于优化,每个函数都没有使用帧指针寄存器。其他的非-O0优化选项,都不使用帧指针寄存器。

5. GDB调试与-fomit-frame-pointer

在x86上,当GCC使用选项-fomit-frame-pointer进行编译时,就会在不需要帧指针的函数中省略帧指针,以减少函数调用开销。虽然没有帧指针,但这个并不会影响GDB查找栈帧,这主要是当GCC在编译时使用-g选项后,会使用CFI(Call Frame Information)伪指令来生成调试信息,所以并不会影响GDB跟踪栈帧。

6. 总结与疑问

  • 帧指针寄存器(Frame Pointer Register)主要用于存放函数栈帧的栈底指针,通过使用帧指针寄存器可以比较容易的寻址函数的局部变量和函数参数等。
  • 可以通过GCC的-fomit-frame-pointer和-fno-omit-frame-pointer编译选项来通知GCC是否是否帧指针寄存器。

疑问: 留一个疑问给大家思考

  • 既然可以不使用帧指针寄存器,那我们在什么情况下要使用帧指针寄存器?使用帧指针寄存器有什么好处呢?

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表