环境依据:
Ubuntu2204 x64
gcc version 11.4.0 (Ubuntu 11.4.0-1ubuntu1~22.04)
记录 C 中栈对函数的管理
DEMO
示例代码
int test1(int a, int b) { |
反汇编结果
0000000000001149 <test1>: |
分析
1. main()
00000000000011a0 <main>: |
从这几步指令可以看出对应的执行步骤:
- 首先向栈中了保存了 %rbp 上的数据,然后将栈顶指针做为当前函数内存空间开始的基址
- 栈顶指针偏移
-10H
为当前函数中的局部变量开辟空间 - 将变量的值赋予到开辟的新空间中
前两步对于栈的处理流程可以这样展示
从这里也很容易看出为什么 %rbp 也称为栈帧基址指针(Base Pointer)
2. main()
开始调用 test()
00000000000011a0 <main>: |
执行步骤:
- 将参数赋予到对应的传参寄存器
- 调用
test
函数,同时将下一条指令的地址压入栈中,*%rsp* 向下增长
3. 类推
那么以此类推,则不难看出进入到 test1()
后所形成的栈:
4. 函数返回
0000000000001149 <test1>: |
执行步骤:
- 从栈中弹出 %rbp 的数据,并恢复其原有的数据,同时 %rsp 指针向上增长
- 调用
ret
指令,将栈中存储的下一条指令地址传递给 %rip ,同时 %rsp 指针向上增长
这样函数整体调用就完成了,转换成流程图更容易理解:
test()
中的 leave
指令也是做了和上面一样的流程,只不过是将多个指令合并成了一个
递归和迭代的区别
从上面可以看出,当函数调用时形成对应的栈空间用于数据保存,那么实际上递归隐性的使用了栈进行数据管理,所以这是迭代和递归最关键的区别。因此任何一个递归都可以转换成迭代的方式进行处理,在循环次数少的情况下运用递归是个不错的选择,反之建议使用迭代的方式进行处理
参考
- CSAPP-Ch3
- x86_64架构中rbp/rsp配合实现函数调用