函数调用栈

什么是函数调用栈

函数是每一门编程语言中,不可短少的局部。函数实质是一片成块的内存指令。而函数调用,除了根本的程序指令跳转外,还须要保留函数相干的上下文,也就是函数的参数,本地变量,返回参数,返回地址等。保留函数上下文的就是咱们常说的函数。函数互相调用的栈构造,就是函数调用栈。

函数调用栈用在何处

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