前言
网上很多对函数栈的解释,说的不是很清楚感觉,尤其是对到底是谁的栈,以及指令的微小但是很致命的细节没说,特写本文,一是帮助自己记忆,二是为了帮助大家,如有疏忽错误请指正。
核心概念
首先明白几个概念
1、在x86系统的CPU中,rsp是栈指针寄存器,这个寄存器中存储着栈顶的地址。rbp中存储着栈底的地址。函数栈空间主要是由这两个寄存器来确定的。
2、其次 x86 栈高处是高地址,低处是低地址,rsp 向下增长(sub rsp n)。
3、⚠️警告:有的汇编语言写法是这样的 mov %rbp,%rsp
和 mov rsp rbp
是等价的。
基本x86架构
的栈就是下图这个样子:
解释函数调用前后栈变化
一、函数调用首先都是先执行 call 指令,这个指令执行后做了两件事:
1、把“返回地址”压入栈(即下一条指令的地址);
说明:如果使用 python 解释,比如 main 函数中的一条指令要调用 add 函数了,这里的下一条指令指的是 add(a,b) 的下一条指令。另外压入的这个栈是调用函数自己的栈
,而不是被调用函数
的
2、跳转到目标函数的入口地址(jmp)。
二、建立被调用函数的栈
执行如下指令,建立被调用函数
的栈:
push rbp
mov rbp rsp
push rbp
#调用函数
将自己栈的栈底( rbp 寄存器中的值)压入被调用函数
的栈里,并且 rsp 自动向下移动,指向刚才压入的值处(但是 rbp 寄存器中的值没变化)
说明:rsp 是自己移动的,至于移动多少取决于你 push 进的数据有多长,这个 cpu 电路自己能判断,不用人为干预。
mov rbp rsp
#将rsp 寄存器中的值复制到 rbp 寄存器中(这时候被调用函数
的栈底和栈顶指向同一处即保存调用函数栈底
的位置)
三、函数调用完成后执行如下指令:
mov rsp, rbp ; 恢复栈顶(rsp)到函数基址(rbp)
pop rbp ; 恢复调用者的 rbp(基址指针)
ret ; 隐式执行:pop rip(从栈顶弹出返回地址 → 写入 rip)
mov rsp, rbp ; 恢复栈顶(rsp)到函数基址(rbp)
是函数返回前常见的一步,将 rbp 寄存器中的值复制到 rsp 寄存器中,这时候 rbp 和 rsp 指向同一个地址,并且被调用函数
的栈就只有一个元素,即调用函数的栈底地址
,它的本质作用是撤销本函数在栈上为局部变量分配的空间,恢复 rsp 到进入函数时的位置。
pop rbp 将调用函数的栈底地址弹出到 rbp 寄存器,这时候,被调用函数
的栈被销毁。并且 rsp 自动向上指向调用函数栈
的栈顶(即返回值)。
ret 是 return from procedure
的缩写,它做了两件事:
1、从当前栈顶也就是调用函数的栈顶
(rsp 指向的位置)弹出一个地址(返回地址);返回地址就是上文讲过的比如python 代码中的 add(a,b)
的下一条指令的地址。
2、把这个地址加载到 rip(指令指针寄存器),程序跳转回调用函数的下一条指令继续执行。
由于函数完成调用后的栈变化比较复杂,容易混淆,所以再次总结下函数调用完成后的堆栈变化:
mov rsp, rbp ; 恢复栈顶(rsp)到函数基址(rbp)
pop rbp ; 恢复调用者的 rbp(基址指针)
ret ; 隐式执行:pop rip(从栈顶弹出返回地址 → 写入 rip)
第一条指令执行后,rsp 指向被调用函数
的 rbp ;
然后pop rbp 后,rsp 指向被调用函数
的上一个地址,即调用函数
的栈里边了(此时被调用函数
的栈被销毁掉了已经);
然后执行 ret 后,就把 rsp 指向的东西弹出到 rip 里。
从这里可以看出函数下一条要执行的指令,即返回地址,是调用者自己保存到自己的栈中
的。