函数调用栈

函数调用栈

什么是函数调用栈

函数是每一门编程语言中,不可缺少的部分。函数本质是一片成块的内存指令。而函数调用,除了基本的程序指令跳转外,还需要保存函数相关的上下文,也就是函数的参数,本地变量,返回参数,返回地址等。保存函数上下文的就是我们常说的函数。函数相互调用的栈结构,就是函数调用栈。

函数调用栈用在何处

  1. 函数调用栈是函数调用必不可少的组成部分。
  2. 我们常说的协程,底层的实现原理,都是基于函数调用栈的。协程切换,就是不同的栈帧切换,同时保存相关的上下文,当然这里也有寄存器值的保存。

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当作本地变量
    • Gocaller拷贝callee栈帧中的返回值,写入本栈,返回值从右向左入栈
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值