x86 栈的使用有 rbp 和 rsp,只设计 rsp 可不可以?为什么?

2024-06-01 10:49:51 +08:00
 amiwrong123

https://zhuanlan.zhihu.com/p/656740329

在 x86-64 架构中,rbp 和 rsp 寄存器分别是栈帧基址指针( Base Pointer )和栈指针( Stack Pointer )。

看完了这篇文章,完全搞懂了函数调用过程中,rbp 和 rsp 的使用情况。

但是还是有一点不太理解,就是假如 CPU 只设计 rsp 可以吗?从我简单来看,好像也是可以够用的啊。我总结一下,无外乎是这些:

返回过程:

那从上面分析来看,只有 rsp ,好像也能完成函数调用的工作啊?

2483 次点击
所在节点    程序员
20 条回复
shawnsh
2024-06-01 10:52:24 +08:00
怎么取参数
ThirdFlame
2024-06-01 10:54:19 +08:00
RSP 可以移动
RBP 是不动的

你去取变量肯定是靠 RBP+偏移来取啊。 靠 RSP 那你还得设计个地方去记录 RSP 的变化,还不如直接设计一个 RBP 呢
amiwrong123
2024-06-01 10:54:43 +08:00
@shawnsh #1
你是指,函数调用传参,不是通过寄存器传递;而是通过栈来传递参数的情况呗?
luxor
2024-06-01 10:58:23 +08:00
gcc: -fomit-frame-pointer msvc: /Oy
shawnsh
2024-06-01 11:00:54 +08:00
@amiwrong123 寄存器才能放多少东西,多次的函数调用不用栈内存保存,你用啥
amiwrong123
2024-06-01 11:02:06 +08:00
@shawnsh #1
@ThirdFlame #2
我好像懂你俩的意思了,比如一个函数的汇编里面:
sub $0x10, %rsp
之后取局部变量时,都是用-0x10(%rsp)来取 局部变量的。

是这个意思吧?
amiwrong123
2024-06-01 11:04:13 +08:00
@shawnsh #5
嗯,确实。
很多时候,可能必须用栈 来传递参数。这个时候,只有 rbp 记录了 上一个函数的栈帧基地址(刚进入下一个函数时),如果没有 rbp 就不好办了还。
amiwrong123
2024-06-01 11:12:34 +08:00
@amiwrong123 #6
这里写了,比如一个函数的汇编里面:
sub $0x30, %rsp
之后取局部变量时,都是用-0x10(%rbp)、-0x20(%rbp)、-0x30(%rbp)来取 各个局部变量的。
amiwrong123
2024-06-01 11:12:49 +08:00
这里写错了
vituralfuture
2024-06-01 11:14:22 +08:00
不要 rbp 是可行的,只不过追溯函数调用栈变得困难

不要 rbp 的时候,编译器知道每个函数分配的栈帧大小,但是没有保存起来,例如函数返回时需要恢复 rbp ,而 rbp 就保存在当前 rbp 指向的地址,这时 rsp 减去编译器知道的栈帧大小就能得到 rbp

call 指令会将 pc(指向下一条指令)和 rbp 压栈,这样子程序能够恢复栈帧,并回到函数调用发生的位置的下一条指令,如果要追溯函数调用栈,只需要拿到 rbp-8 指向的返回地址。读取 rbp 可以使用内联汇编。这就需要知道 rbp 的值,而这个值编译器知道却没有保存

gcc 有个参数-fomit-frame-pointer ,就是省略了 rbp 的使用,但不难以调试程序
adoal
2024-06-01 12:45:28 +08:00
别说是没有 BP 可以,就算没有 SP 也不是不可以的
adoal
2024-06-01 12:58:47 +08:00
从正常的 C 编译成 X86 汇编的代码来看,为了维护栈帧而对 BP 的所有操作,其实都是显式的,所以实际上就是个普通寄存器,只不过出于约定,用 BP 来做这事就是了,没有 BP ,也可以拿其它寄存器用。

SP 就比 BP 特殊了,call/ret/push/pop 等指令要隐式操作 SP……但实质上无非就是移动 SP 和读写数值而已,这些指令的设计是因为对应的操作太常见了,所以就为之做了优化,设计出专用指令,但从指令集完整性的角度完全可以编译器拆开做,SP 的功能也可以由任意通用寄存器来承担,在很多 RISC 设计里就是约定到某个通用寄存器。只有 IP (一般叫 PC 的更多)才是无论如何也不可能通用化的。
ysc3839
2024-06-01 15:14:52 +08:00
x86 bp 只是叫 base pointer 吧,跟 stack 无关,用户想怎么使用都可以。sp 是真的 stack pointer ,push pop 等操作会影响 sp 。
amiwrong123
2024-06-01 15:52:57 +08:00
@vituralfuture #10
谢谢。这就是我想要的答案。
也就是说,默认编译出来的汇编,每个函数都会去使用 rbp 的。
但是如果加了-fomit-frame-pointer 参数,那么每个函数就不会去使用 rbp 了。

所以结论就是:函数调用过程中,可以不使用 rbp 。
amiwrong123
2024-06-01 15:54:23 +08:00
@adoal #12
嗯嗯,你的主要意思是:
如果没有 BP ,那么用其他 通用寄存器来当 BP 来用,也是一样的。

在 RISC 里,甚至 SP 都可以用 其他通用寄存器 来代替。
amiwrong123
2024-06-01 19:18:12 +08:00
@vituralfuture #10
不过 1 楼那个问题,要是 函数调用时参数是通过栈来传递参数的话(不然可以直接通过寄存器传参),没有 rbp 的话,会不会有点不好办。

不过我想,有 rbp 的话(在刚进入函数时),就是 rbp 减一个数来 获得传参;如果没有 rbp ,那就通过 rsp 加一个数 来获得传参。好像一样能解决问题。
PTLin
2024-06-01 20:34:55 +08:00
rbp 主要就是为了 backtrack ,假如完全不需要这个功能就可以取消 push rbp,mov rbp rsp 什么的,在 Linux 内核中完全不需要 ftrace 什么的功能也可以编译成不用 rbp 的。
sMil3
2024-06-01 21:10:00 +08:00
mips 就是按 sp 找局部变量吧
iceheart
2024-06-02 04:19:06 +08:00
rbp 是链表指针,
这个链表就是 callstack 链表
kingly
2024-06-02 15:23:15 +08:00
基础不行真的不建议思考这些问题,没啥用,纯浪费精力。你调试的时候看堆栈回溯,没有 rbp 你看个屁啊,栈底上面保存了返回地址,除此之外还有很多其他小用处,比如优化了寻找速度,这都是用处。建议基础没砸实之前不要去产生过多疑问,浪费人生。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1045900

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX