函数调用栈

tvelve代码家 · · 805 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

函数调用栈

什么是函数调用栈

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

函数调用栈用在何处

  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语言栈帧

image

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)

image

总结

C语言和Go语言栈帧,最大的不同就是:

  • 函数参数保存方式,位置不同

    • C使用寄存器传递函数参数,函数参数保存在callee栈帧中
    • Go直接使用栈传递参数,函数参数保存在caller`栈帧中
  • 函数参数排列顺序不同

    • C函数参数从左到右入栈排列
    • Go函数参数从右到左入栈排列
  • 函数返回值处理不同

    • C通过寄存器ax将返回值传给caller, caller当作本地变量
    • Gocaller拷贝callee栈帧中的返回值,写入本栈,返回值从右向左入栈

有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:tvelve代码家

查看原文:函数调用栈

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

805 次点击  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传