14
函数调用栈
source link: https://studygolang.com/articles/31267
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
函数调用栈
什么是函数调用栈
函数是每一门编程语言中,不可缺少的部分。函数本质是一片成块的内存指令。而函数调用,除了基本的程序指令跳转外,还需要保存函数相关的上下文,也就是函数的参数,本地变量,返回参数,返回地址等。保存函数上下文的就是我们常说的函数 栈
。函数相互调用的栈结构,就是函数调用栈。
函数调用栈用在何处
- 函数调用栈是函数调用必不可少的组成部分。
- 我们常说的协程,底层的实现原理,都是基于函数调用栈的。协程切换,就是不同的栈帧切换,同时保存相关的上下文,当然这里也有寄存器值的保存。
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
栈帧中的返回值,写入本栈,返回值从右向左入栈
-
有疑问加站长微信联系
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK