函数调用栈
什么是函数调用栈
函数是每一门编程语言中,不可缺少的部分。函数本质是一片成块的内存指令。而函数调用,除了基本的程序指令跳转外,还需要保存函数相关的上下文,也就是函数的参数,本地变量,返回参数,返回地址等。保存函数上下文的就是我们常说的函数栈
。函数相互调用的栈结构,就是函数调用栈。
函数调用栈用在何处
- 函数调用栈是函数调用必不可少的组成部分。
- 我们常说的协程,底层的实现原理,都是基于函数调用栈的。协程切换,就是不同的栈帧切换,同时保存相关的上下文,当然这里也有寄存器值的保存。
C语言实现
#include <stdio.h>
int sum(int a, int b, int c) {
int sum_loc1 = 7;
int sum_loc2 = 8;
int sum_loc3 = 9;
return sum_loc1 + sum_loc2 + sum_loc3 + a + b + c;
}
int main(int argc, const char *argv[]) {
int loc1 = 1;
int loc2 = 2;
int loc3 = 3;
int ret = sum(4,5,6);
printf("ret:%d\n", ret);
}
- C语言对应的
x86_64
汇编代码
0000000000400530 <sum>:
400530: 55 push %rbp # rbp 入栈
400531: 48 89 e5 mov %rsp,%rbp # rbp = rsp
400534: 89 7d ec mov %edi,-0x14(%rbp) # 第一个参数入栈
400537: 89 75 e8 mov %esi,-0x18(%rbp) # 第二个参数入栈
40053a: 89 55 e4 mov %edx,-0x1c(%rbp) # 第三个参数入栈
40053d: c7 45 fc 07 00 00 00 movl $0x7,-0x4(%rbp) # 本地变量1:7
400544: c7 45 f8 08 00 00 00 movl $0x8,-0x8(%rbp) # 本地变量2:8
40054b: c7 45 f4 09 00 00 00 movl $0x9,-0xc(%rbp) # 本地变量3:9
400552: 8b 45 f8 mov -0x8(%rbp),%eax
400555: 8b 55 fc mov -0x4(%rbp),%edx
400558: 01 c2 add %eax,%edx # 8 + 7
40055a: 8b 45 f4 mov -0xc(%rbp),%eax
40055d: 01 c2 add %eax,%edx # 9 + 15 = 24
40055f: 8b 45 ec mov -0x14(%rbp),%eax # 4 + 24 = 28
400562: 01 c2 add %eax,%edx
400564: 8b 45 e8 mov -0x18(%rbp),%eax # 5 + 28 = 33
400567: 01 c2 add %eax,%edx
400569: 8b 45 e4 mov -0x1c(%rbp),%eax # 6 + 33 = 39
40056c: 01 d0 add %edx,%eax # eax 作为返回值存储寄存器
40056e: 5d pop %rbp # 弹出rbp
40056f: c3 retq
0000000000400570 <main>:
400570: 55 push %rbp # rbp 压栈
400571: 48 89 e5 mov %rsp,%rbp # rbp = rsp
400574: 48 83 ec 20 sub $0x20,%rsp # rsp = 32 ; 字节
400578: 89 7d ec mov %edi,-0x14(%rbp) #
40057b: 48 89 75 e0 mov %rsi,-0x20(%rbp) #
40057f: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp) # 本地变量从右到左入栈
400586: c7 45 f8 02 00 00 00 movl $0x2,-0x8(%rbp) #
40058d: c7 45 f4 03 00 00 00 movl $0x3,-0xc(%rbp) #
400594: ba 06 00 00 00 mov $0x6,%edx # 第三个参数
400599: be 05 00 00 00 mov $0x5,%esi # 第二个参数
40059e: bf 04 00 00 00 mov $0x4,%edi # 第一个参数
4005a3: e8 88 ff ff ff callq 400530 <sum> # 调用sum指令
4005a8: 89 45 f0 mov %eax,-0x10(%rbp) # 返回值放入预先的栈空间
4005ab: 8b 45 f0 mov -0x10(%rbp),%eax
4005ae: 89 c6 mov %eax,%esi # 放入printf第二个参数
4005b0: bf 60 06 40 00 mov $0x400660,%edi
4005b5: b8 00 00 00 00 mov $0x0,%eax
4005ba: e8 51 fe ff ff callq 400410 <printf@plt>
4005bf: c9 leaveq
4005c0: c3 retq
4005c1: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
4005c8: 00 00 00
4005cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
# call :
# 1. 将call下一条指令入栈;也就是ip入栈
# 2. 将sum指令入ip
- 对应的C语言栈帧
1. 本地变量从左到右依次入栈,返回值保存的变量是第四个本地变量
2. 函数参数由被调用者`callee`负责维护,从左到右依次入栈
3. 最后返回地址入栈
4. `rbp`入栈
Go语言实现
package main
import (
"fmt"
)
func sum(a, b, c int) (ret1, ret2 int) {
loc1 := 1
loc2 := 2
loc3 := 3
return a + b + loca1, c + loc2 + loc3
}
func main() {
l1 := 4
l2 := 5
l3 := 6
r1,r2 := sum(7,8,9)
fmt.Printf("r1:%d, r2%d\n", r1, r2)
}
func sum(a, b, c int) (ret1, ret2 int) {
0x49aa80 4883ec30 SUBQ $0x30, SP # 栈扩展 48字节
0x49aa84 48896c2428 MOVQ BP, 0x28(SP) # bp 入栈
0x49aa89 488d6c2428 LEAQ 0x28(SP), BP # 重新设置bp的值
0x49aa8e 48c744245000000000 MOVQ $0x0, 0x50(SP) #
0x49aa97 48c744245800000000 MOVQ $0x0, 0x58(SP)
loc1 := 1
0x49aaa0 48c744241001000000 MOVQ $0x1, 0x10(SP) # 本地变量1
loc2 := 2
0x49aaa9 48c744240802000000 MOVQ $0x2, 0x8(SP)
loc3 := 3
0x49aab2 48c7042403000000 MOVQ $0x3, 0(SP)
return a + b + loc1, c + loc2 + loc3
0x49aaba 488b442438 MOVQ 0x38(SP), AX # caller保存参数
0x49aabf 4803442440 ADDQ 0x40(SP), AX #
0x49aac4 4803442410 ADDQ 0x10(SP), AX # AX = loc1 + (a+b)
0x49aac9 4889442420 MOVQ AX, 0x20(SP) # r1 第一个返回参数
0x49aace 488b442448 MOVQ 0x48(SP), AX
0x49aad3 4803442408 ADDQ 0x8(SP), AX
0x49aad8 48030424 ADDQ 0(SP), AX
0x49aadc 4889442418 MOVQ AX, 0x18(SP) # r2 第二个返回参数
0x49aae1 488b442420 MOVQ 0x20(SP), AX
0x49aae6 4889442450 MOVQ AX, 0x50(SP)
0x49aaeb 488b442418 MOVQ 0x18(SP), AX
0x49aaf0 4889442458 MOVQ AX, 0x58(SP)
0x49aaf5 488b6c2428 MOVQ 0x28(SP), BP
0x49aafa 4883c430 ADDQ $0x30, SP
0x49aafe c3 RET
func main() {
0x49ab00 64488b0c25f8ffffff MOVQ FS:0xfffffff8, CX
0x49ab09 488d842448ffffff LEAQ 0xffffff48(SP), AX
0x49ab11 483b4110 CMPQ 0x10(CX), AX
0x49ab15 0f8619030000 JBE 0x49ae34
0x49ab1b 4881ec38010000 SUBQ $0x138, SP
0x49ab22 4889ac2430010000 MOVQ BP, 0x130(SP)
0x49ab2a 488dac2430010000 LEAQ 0x130(SP), BP
l1 := 4
0x49ab32 48c744246004000000 MOVQ $0x4, 0x60(SP) # 本地变量1
l2 := 5
0x49ab3b 48c744245805000000 MOVQ $0x5, 0x58(SP) # 本地变量2
l3 := 6
0x49ab44 48c744245006000000 MOVQ $0x6, 0x50(SP) # 本地变量3
r1, r2 := sum(7, 8, 9)
0x49ab4d 48c7042407000000 MOVQ $0x7, 0(SP) # 参数
0x49ab55 48c744240808000000 MOVQ $0x8, 0x8(SP) # 参数2
0x49ab5e 48c744241009000000 MOVQ $0x9, 0x10(SP) # 参数3
0x49ab67 e814ffffff CALL main.sum(SB)
0x49ab6c 488b442418 MOVQ 0x18(SP), AX
0x49ab71 4889442470 MOVQ AX, 0x70(SP) # r2返回值拷贝
0x49ab76 488b442420 MOVQ 0x20(SP), AX
0x49ab7b 4889442468 MOVQ AX, 0x68(SP) # r2返回值拷贝
0x49ab80 488b442470 MOVQ 0x70(SP), AX
0x49ab85 4889442448 MOVQ AX, 0x48(SP)
0x49ab8a 488b442468 MOVQ 0x68(SP), AX
0x49ab8f 4889442440 MOVQ AX, 0x40(SP)
总结
C
语言和Go
语言栈帧,最大的不同就是:
函数参数保存方式,位置不同
C
使用寄存器传递函数参数,函数参数保存在callee
栈帧中- Go
直接使用栈传递参数,函数参数保存在
caller`栈帧中
函数参数排列顺序不同
C
函数参数从左到右入栈排列Go
函数参数从右到左入栈排列
函数返回值处理不同
C
通过寄存器ax
将返回值传给caller
,caller
当作本地变量Go
:caller
拷贝callee
栈帧中的返回值,写入本栈,返回值从右向左入栈
有疑问加站长微信联系(非本文作者)