ZhiBing's blog(码上看世界) ZhiBing's blog(码上看世界)
首页
  • Linux工具链

    • shell命令
  • 构建

    • CMake
    • Makefile
  • 版本管理

    • Git
    • Github
  • IDE及工具

    • vscode
    • CLion
  • 设计模式

    • 设计原则
  • 编程语言

    • C++
    • Go
    • Python
    • Shell
  • 调试

    • gdb
  • 开发者测试

    • gtest
  • 系统支撑

    • 操作系统
  • 性能优化

    • 编译优化选项
    • perf
    • valgrind
  • 容器

    • Docker
  • 微服务

    • Rancher
  • 其他
  • 随笔
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
关于
GitHub (opens new window)

ZhiBing Zheng

时间会回答成长
首页
  • Linux工具链

    • shell命令
  • 构建

    • CMake
    • Makefile
  • 版本管理

    • Git
    • Github
  • IDE及工具

    • vscode
    • CLion
  • 设计模式

    • 设计原则
  • 编程语言

    • C++
    • Go
    • Python
    • Shell
  • 调试

    • gdb
  • 开发者测试

    • gtest
  • 系统支撑

    • 操作系统
  • 性能优化

    • 编译优化选项
    • perf
    • valgrind
  • 容器

    • Docker
  • 微服务

    • Rancher
  • 其他
  • 随笔
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
关于
GitHub (opens new window)
  • 编程语言

    • C++

      • shared_ptr(智能指针)
      • unique_ptr
      • bind函数
      • cout
      • C++ main函数的传入参数(argc, argv)
      • GLog使用
      • signals2
      • 编译期分派
      • 成员函数的调用效率
      • 返回值优化
      • 函数调用栈
        • 栈帧
        • 函数调用与函数调用栈
        • 实际函数调用栈的阅读
        • 查看调用栈
      • 空类大小
      • 指针和数组的区别
      • 遵守三五原则
      • const限定符
      • static限定符
      • virtual限定符
      • 常用的CXX_FLAGS
      • 模版之AnyType
      • 模版之编译期断言
      • 模版之变参模板
    • Go

    • Python

    • shell

    • Rust

  • 调试

  • 开发者测试

  • 系统支撑

  • 性能优化

  • 通用领域
  • 编程语言
  • C++
zhengzhibing
2022-06-16
目录

函数调用栈

# 函数调用栈

在程序运行过程中,栈用于维护函数调用的上下文,离开了栈,函数调用就无法实现。

# 栈帧

每个函数发生调用时,都会有一块栈空间,这块栈空间称为栈帧。

栈帧的结构如下:

image-20220226161830034

这里要注意的是:栈空间是从高地址到低地址分配的。

rbp:基址指针寄存器(reextended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。

rsp:栈指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。

栈帧保存了一个函数调用所需要的维护信息:

  • 函数的返回地址和参数。
  • 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量。
  • 保存的上下文:包括在函数调用前后需要保持不变的寄存器。

# 函数调用与函数调用栈

写一个程序,通过反汇编,了解真实的函数调用栈。

// demo.cpp

#include <cstdint>
int32_t Func2(int32_t a);

int32_t Func1(int32_t a, int32_t b)
{
    int32_t c = 10;
    int32_t d = 0;
    c = Func2(c + d);
    return c + d + a + b;
}

int32_t Func2(int32_t a)
{
    int b = a * 2;
    return b;
}

int main(int argc, char** argv)
{
    int32_t a = 42;
    int32_t b = 101;
    int32_t c = 0;
    c = Func1(a, b);
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  1. g++ -g demo.cpp -o demo:编译得到可执行文件。
  2. objdump -s -d demo:反汇编得到汇编代码。

函数调用过程如下:

image-20220226165700995

总结一下,当函数 A 调用函数 B 时:

  1. 将函数入参保存要寄存器中。(在函数 A 的调用栈中)
  2. 将函数 B 的返回地址压入栈,即将callq <B>的下一行指令地址压入栈,然后调用函数 B。(在函数 A 的调用栈中)
  3. 函数参数入栈。(在函数 B 的调用栈中)
  4. 局部变量入栈。(在函数 B 的调用栈中)
  5. 执行一些运算指令。(在函数 B 的调用栈中)
  6. 将返回结果保存到寄存器eax中。(在函数 B 的调用栈中)
  7. 从寄存器eax中获得函数 B 的返回值。(在函数 A 的调用栈中)

其实函数调用过程中涉及到栈基指针rbp和栈顶指针rsp的变化。也就是这段汇编:

push %rbp
mov  %rsp,%rbp
sub  $0x20,%rsp
1
2
3

它的作用就是把上一个栈帧的rbp存起来,将当前栈的rbp更新成上一个栈帧的rsp,然后设置当前栈的rsp

。这样就完成了两个指针维护一个栈帧大小的功能。函数调用就是这两个指针的移动,函数调用栈的扩张和收缩。

# 实际函数调用栈的阅读

首先明确一点,栈空间是从高地址到低地址扩张的,阅读也是从高地址开始读。

image-20220226173501291

# 查看调用栈

首先需要有调试信息的二进制文件。

利用gdb查看。

gdb demo
(gdb) b Func1(int, int) # 打个断点
(gdb) r
(gdb) s
(gdb) disassemble # 查看当前函数的汇编,能看到具体执行到哪一条指令
(gdb) ni # 执行一条指令
(gdb) p $rbp
(gdb) p $rsp
(gdb) x/32xg $rbp - 128 # 查看从$rbp - 128的地址开始32的单位(以16进制显示,每个单元8字节)的内存内容
1
2
3
4
5
6
7
8
9
#C++
上次更新: 2022/06/17, 07:22:19
返回值优化
空类大小

← 返回值优化 空类大小→

最近更新
01
HPE gen10 plus 安装ESXI 7
06-12
02
ESXI 7安装黑群晖
06-12
03
ESXI 7安装win10
06-12
更多文章>
Theme by Vdoing | Copyright © 2022-2024 ZhBing Zheng | 粤ICP备2022062743号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式