环境依据:
Ubuntu2204 x64
gcc version 11.4.0 (Ubuntu 11.4.0-1ubuntu1~22.04)

记录 C 中栈对函数的管理

DEMO

示例代码

int test1(int a, int b) {
return a - b;
}

int test(int a, int b) {
int aa = 2;
int bb = 3;
return a + b + test1(aa, bb);
}

int main() {
int a = 4;
int b = 5;
int count = test(4, 5);
int count1 = test(a, b);
}

反汇编结果

0000000000001149 <test1>:
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 48 89 e5 mov %rsp,%rbp
1151: 89 7d fc mov %edi,-0x4(%rbp)
1154: 89 75 f8 mov %esi,-0x8(%rbp)
1157: 8b 45 fc mov -0x4(%rbp),%eax
115a: 2b 45 f8 sub -0x8(%rbp),%eax
115d: 5d pop %rbp
115e: c3 ret

000000000000115f <test>:
115f: f3 0f 1e fa endbr64
1163: 55 push %rbp
1164: 48 89 e5 mov %rsp,%rbp
1167: 53 push %rbx
1168: 48 83 ec 18 sub $0x18,%rsp
116c: 89 7d e4 mov %edi,-0x1c(%rbp)
116f: 89 75 e0 mov %esi,-0x20(%rbp)
1172: c7 45 f0 02 00 00 00 movl $0x2,-0x10(%rbp)
1179: c7 45 f4 03 00 00 00 movl $0x3,-0xc(%rbp)
1180: 8b 55 e4 mov -0x1c(%rbp),%edx
1183: 8b 45 e0 mov -0x20(%rbp),%eax
1186: 8d 1c 02 lea (%rdx,%rax,1),%ebx
1189: 8b 55 f4 mov -0xc(%rbp),%edx
118c: 8b 45 f0 mov -0x10(%rbp),%eax
118f: 89 d6 mov %edx,%esi
1191: 89 c7 mov %eax,%edi
1193: e8 b1 ff ff ff call 1149 <test1>
1198: 01 d8 add %ebx,%eax
119a: 48 8b 5d f8 mov -0x8(%rbp),%rbx
119e: c9 leave
119f: c3 ret

00000000000011a0 <main>:
11a0: f3 0f 1e fa endbr64
11a4: 55 push %rbp
11a5: 48 89 e5 mov %rsp,%rbp
11a8: 48 83 ec 10 sub $0x10,%rsp
11ac: c7 45 f0 04 00 00 00 movl $0x4,-0x10(%rbp)
11b3: c7 45 f4 05 00 00 00 movl $0x5,-0xc(%rbp)
11ba: be 05 00 00 00 mov $0x5,%esi
11bf: bf 04 00 00 00 mov $0x4,%edi
11c4: e8 96 ff ff ff call 115f <test>
11c9: 89 45 f8 mov %eax,-0x8(%rbp)
11cc: 8b 55 f4 mov -0xc(%rbp),%edx
11cf: 8b 45 f0 mov -0x10(%rbp),%eax
11d2: 89 d6 mov %edx,%esi
11d4: 89 c7 mov %eax,%edi
11d6: e8 84 ff ff ff call 115f <test>
11db: 89 45 fc mov %eax,-0x4(%rbp)
11de: b8 00 00 00 00 mov $0x0,%eax
11e3: c9 leave
11e4: c3 ret

分析

1. main()

00000000000011a0 <main>:
...
11a4: 55 push %rbp
11a5: 48 89 e5 mov %rsp,%rbp
11a8: 48 83 ec 10 sub $0x10,%rsp
11ac: c7 45 f0 04 00 00 00 movl $0x4,-0x10(%rbp)
11b3: c7 45 f4 05 00 00 00 movl $0x5,-0xc(%rbp)
...

从这几步指令可以看出对应的执行步骤:

  1. 首先向栈中了保存了 %rbp 上的数据,然后将栈顶指针做为当前函数内存空间开始的基址
  2. 栈顶指针偏移 -10H 为当前函数中的局部变量开辟空间
  3. 将变量的值赋予到开辟的新空间中
    前两步对于栈的处理流程可以这样展示

从这里也很容易看出为什么 %rbp 也称为栈帧基址指针(Base Pointer)

2. main() 开始调用 test()

00000000000011a0 <main>:
...
11ba: be 05 00 00 00 mov $0x5,%esi
11bf: bf 04 00 00 00 mov $0x4,%edi
11c4: e8 96 ff ff ff call 115f <test>
11c9: 89 45 f8 mov %eax,-0x8(%rbp)
...

执行步骤:

  1. 将参数赋予到对应的传参寄存器
  2. 调用 test 函数,同时将下一条指令的地址压入栈中,*%rsp* 向下增长

3. 类推

那么以此类推,则不难看出进入到 test1() 后所形成的栈:

4. 函数返回

0000000000001149 <test1>:
...
115d: 5d pop %rbp
115e: c3 ret

执行步骤:

  1. 从栈中弹出 %rbp 的数据,并恢复其原有的数据,同时 %rsp 指针向上增长
  2. 调用 ret 指令,将栈中存储的下一条指令地址传递给 %rip ,同时 %rsp 指针向上增长
    这样函数整体调用就完成了,转换成流程图更容易理解:

test() 中的 leave 指令也是做了和上面一样的流程,只不过是将多个指令合并成了一个

递归和迭代的区别

从上面可以看出,当函数调用时形成对应的栈空间用于数据保存,那么实际上递归隐性的使用了栈进行数据管理,所以这是迭代和递归最关键的区别。因此任何一个递归都可以转换成迭代的方式进行处理,在循环次数少的情况下运用递归是个不错的选择,反之建议使用迭代的方式进行处理

参考

  1. CSAPP-Ch3
  2. x86_64架构中rbp/rsp配合实现函数调用