14

函数调用栈

 3 years ago
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.
neoserver,ios ssh client

函数调用栈

什么是函数调用栈

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

函数调用栈用在何处

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

IVZVzuR.png!mobile

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)

ziYjYb3.png!mobile

总结

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

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

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

    C
    Go
    
  • 函数返回值处理不同

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

有疑问加站长微信联系

iiUfA3j.png!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK