函数调用栈
什么是函数调用栈
函数是每一门编程语言中,不可短少的局部。函数实质是一片成块的内存指令。而函数调用,除了根本的程序指令跳转外,还须要保留函数相干的上下文,也就是函数的参数,本地变量,返回参数,返回地址等。保留函数上下文的就是咱们常说的函数栈
。函数互相调用的栈构造,就是函数调用栈。
函数调用栈用在何处
- 函数调用栈是函数调用必不可少的组成部分。
- 咱们常说的协程,底层的实现原理,都是基于函数调用栈的。协程切换,就是不同的栈帧切换,同时保留相干的上下文,当然这里也有寄存器值的保留。
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 mainimport ( "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
栈帧中的返回值,写入本栈,返回值从右向左入栈