go 通过汇编分析栈布局和函数栈帧

目录

概要

一、汇编分析

[1.1、go tool compile](#1.1、go tool compile)

[1.2、dlv debug](#1.2、dlv debug)

二、栈布局与函数栈帧


概要

函数栈帧是指函数在被调用时,为该函数在栈上分配的一块内存区域(一般是连续的),主要用于保存函数的上下文信息,包括参数,返回值,局部变量,寄存器值(ebp/rbp)等信息

调试的服务器信息:Centos Linux 7 ,CPU AMD x86_64,Go version 1.24

在go源码runtime/stack.go文件中可以看到go给出的在x86 CPU下栈布局:

// (x86)

// +------------------+

// | args from caller |

// +------------------+ <- frame->argp

// | return address |

// +------------------+

// | caller's BP (*) | (*) if framepointer_enabled && varp > sp

// +------------------+ <- frame->varp

// | locals |

// +------------------+

// | args to callee |

// +------------------+ <- frame->sp

我们结合一下案例,结合汇编进行逐步分析:

Go 复制代码
 1 package main
 2
 3 func main() {
 4     x,y:=int64(1),int64(2)
 5     x=add(x,y)
 6 }
 7
 8 func add(a, b int64) int64 {
 9      c:=a+b
 10     q:=int64(5)
 11     p:=sub(c, q)
 12     return p
 13 }
 14
 15 func sub(x, y int64) int64{
 16     return x -y
 17 }

前置知识:

x86寄存器

x86汇编指令

go汇编

一、汇编分析

我们通过go tool compile和dlv两个工具结合来看。

1.1、go tool compile

bash 复制代码
[root@test gofunc]# go tool compile -S -N -l main.go
main.main STEXT size=66 args=0x0 locals=0x28 funcid=0x0 align=0x0
        0x0000 00000 (/home/gofunc/main.go:3) TEXT    main.main(SB), ABIInternal, $40-0
        0x0000 00000 (/home/gofunc/main.go:3) CMPQ    SP, 16(R14)
        0x0004 00004 (/home/gofunc/main.go:3) PCDATA  $0, $-2 //GC相关
        0x0004 00004 (/home/gofunc/main.go:3) JLS     58
        0x0006 00006 (/home/gofunc/main.go:3) PCDATA  $0, $-1 //GC相关
        0x0006 00006 (/home/gofunc/main.go:3) PUSHQ   BP
        0x0007 00007 (/home/gofunc/main.go:3) MOVQ    SP, BP
        0x000a 00010 (/home/gofunc/main.go:3) SUBQ    $32, SP
        0x000e 00014 (/home/gofunc/main.go:3) FUNCDATA        $0, gclocals·FzY36IO2mY0y4dZ1+Izd/w==(SB) //GC相关
        0x000e 00014 (/home/gofunc/main.go:3) FUNCDATA        $1, gclocals·FzY36IO2mY0y4dZ1+Izd/w==(SB) //GC相关
        0x000e 00014 (/home/gofunc/main.go:4) MOVQ    $1, main.x+24(SP)
        0x0017 00023 (/home/gofunc/main.go:4) MOVQ    $2, main.y+16(SP)
        0x0020 00032 (/home/gofunc/main.go:5) MOVL    $1, AX
        0x0025 00037 (/home/gofunc/main.go:5) MOVL    $2, BX
        0x002a 00042 (/home/gofunc/main.go:5) PCDATA  $1, $0
        0x002a 00042 (/home/gofunc/main.go:5) CALL    main.add(SB)
        0x002f 00047 (/home/gofunc/main.go:5) MOVQ    AX, main.x+24(SP)
        0x0034 00052 (/home/gofunc/main.go:6) ADDQ    $32, SP
        0x0038 00056 (/home/gofunc/main.go:6) POPQ    BP
        0x0039 00057 (/home/gofunc/main.go:6) RET
        0x003a 00058 (/home/gofunc/main.go:6) NOP
        0x003a 00058 (/home/gofunc/main.go:3) PCDATA  $1, $-1
        0x003a 00058 (/home/gofunc/main.go:3) PCDATA  $0, $-2
        0x003a 00058 (/home/gofunc/main.go:3) CALL    runtime.morestack_noctxt(SB)
        0x003f 00063 (/home/gofunc/main.go:3) PCDATA  $0, $-1
        0x003f 00063 (/home/gofunc/main.go:3) NOP
        0x0040 00064 (/home/gofunc/main.go:3) JMP     0
        0x0000 49 3b 66 10 76 34 55 48 89 e5 48 83 ec 20 48 c7  I;f.v4UH..H.. H.
        0x0010 44 24 18 01 00 00 00 48 c7 44 24 10 02 00 00 00  D$.....H.D$.....
        0x0020 b8 01 00 00 00 bb 02 00 00 00 e8 00 00 00 00 48  ...............H
        0x0030 89 44 24 18 48 83 c4 20 5d c3 e8 00 00 00 00 90  .D$.H.. ].......
        0x0040 eb be                                            ..
        rel 43+4 t=R_CALL main.add+0
        rel 59+4 t=R_CALL runtime.morestack_noctxt+0
main.add STEXT size=103 args=0x10 locals=0x38 funcid=0x0 align=0x0
        0x0000 00000 (/home/gofunc/main.go:8) TEXT    main.add(SB), ABIInternal, $56-16
        0x0000 00000 (/home/gofunc/main.go:8) CMPQ    SP, 16(R14)
        0x0004 00004 (/home/gofunc/main.go:8) PCDATA  $0, $-2
        0x0004 00004 (/home/gofunc/main.go:8) JLS     76
        0x0006 00006 (/home/gofunc/main.go:8) PCDATA  $0, $-1
        0x0006 00006 (/home/gofunc/main.go:8) PUSHQ   BP
        0x0007 00007 (/home/gofunc/main.go:8) MOVQ    SP, BP
        0x000a 00010 (/home/gofunc/main.go:8) SUBQ    $48, SP
        0x000e 00014 (/home/gofunc/main.go:8) FUNCDATA        $0, gclocals·FzY36IO2mY0y4dZ1+Izd/w==(SB)
        0x000e 00014 (/home/gofunc/main.go:8) FUNCDATA        $1, gclocals·FzY36IO2mY0y4dZ1+Izd/w==(SB)
        0x000e 00014 (/home/gofunc/main.go:8) FUNCDATA        $5, main.add.arginfo1(SB)
        0x000e 00014 (/home/gofunc/main.go:8) MOVQ    AX, main.a+64(SP)
        0x0013 00019 (/home/gofunc/main.go:8) MOVQ    BX, main.b+72(SP)
        0x0018 00024 (/home/gofunc/main.go:8) MOVQ    $0, main.~r0+16(SP)
        0x0021 00033 (/home/gofunc/main.go:9) ADDQ    BX, AX
        0x0024 00036 (/home/gofunc/main.go:9) MOVQ    AX, main.c+40(SP)
        0x0029 00041 (/home/gofunc/main.go:10)        MOVQ    $5, main.q+24(SP)
        0x0032 00050 (/home/gofunc/main.go:11)        MOVL    $5, BX
        0x0037 00055 (/home/gofunc/main.go:11)        PCDATA  $1, $0
        0x0037 00055 (/home/gofunc/main.go:11)        CALL    main.sub(SB)
        0x003c 00060 (/home/gofunc/main.go:11)        MOVQ    AX, main.p+32(SP)
        0x0041 00065 (/home/gofunc/main.go:12)        MOVQ    AX, main.~r0+16(SP)
        0x0046 00070 (/home/gofunc/main.go:12)        ADDQ    $48, SP
        0x004a 00074 (/home/gofunc/main.go:12)        POPQ    BP
        0x004b 00075 (/home/gofunc/main.go:12)        RET
        0x004c 00076 (/home/gofunc/main.go:12)        NOP
        0x004c 00076 (/home/gofunc/main.go:8) PCDATA  $1, $-1
        0x004c 00076 (/home/gofunc/main.go:8) PCDATA  $0, $-2
        0x004c 00076 (/home/gofunc/main.go:8) MOVQ    AX, 8(SP)
        0x0051 00081 (/home/gofunc/main.go:8) MOVQ    BX, 16(SP)
        0x0056 00086 (/home/gofunc/main.go:8) CALL    runtime.morestack_noctxt(SB)
        0x005b 00091 (/home/gofunc/main.go:8) PCDATA  $0, $-1
        0x005b 00091 (/home/gofunc/main.go:8) MOVQ    8(SP), AX
        0x0060 00096 (/home/gofunc/main.go:8) MOVQ    16(SP), BX
        0x0065 00101 (/home/gofunc/main.go:8) JMP     0
        0x0000 49 3b 66 10 76 54 55 48 89 e5 48 83 ec 28 48 89  I;f.vTUH..H..(H.
        0x0010 44 24 38 48 89 5c 24 40 48 c7 44 24 10 00 00 00  D$8H.\[email protected]$....
        0x0020 00 48 8d 0c 18 48 89 4c 24 20 48 c7 44 24 18 05  .H...H.L$ H.D$..
        0x0030 00 00 00 b8 4b 00 00 00 bb 05 00 00 00 0f 1f 00  ....K...........
        0x0040 e8 00 00 00 00 48 89 44 24 18 48 8b 44 24 20 48  .....H.D$.H.D$ H
        0x0050 89 44 24 10 48 83 c4 28 5d c3 48 89 44 24 08 48  .D$.H..(].H.D$.H
        0x0060 89 5c 24 10 e8 00 00 00 00 48 8b 44 24 08 48 8b  .\$......H.D$.H.
        0x0070 5c 24 10 eb 8b                                   \$...
        rel 65+4 t=R_CALL main.sub+0
        rel 101+4 t=R_CALL runtime.morestack_noctxt+0
main.sub STEXT nosplit size=39 args=0x10 locals=0x10 funcid=0x0 align=0x0
        0x0000 00000 (/home/gofunc/main.go:15)        TEXT    main.sub(SB), NOSPLIT|ABIInternal, $16-16
        0x0000 00000 (/home/gofunc/main.go:15)        PUSHQ   BP
        0x0001 00001 (/home/gofunc/main.go:15)        MOVQ    SP, BP
        0x0004 00004 (/home/gofunc/main.go:15)        SUBQ    $8, SP
        0x0008 00008 (/home/gofunc/main.go:15)        FUNCDATA        $0, gclocals·FzY36IO2mY0y4dZ1+Izd/w==(SB)
        0x0008 00008 (/home/gofunc/main.go:15)        FUNCDATA        $1, gclocals·FzY36IO2mY0y4dZ1+Izd/w==(SB)
        0x0008 00008 (/home/gofunc/main.go:15)        FUNCDATA        $5, main.sub.arginfo1(SB)
        0x0008 00008 (/home/gofunc/main.go:15)        MOVQ    AX, main.x+24(SP)
        0x000d 00013 (/home/gofunc/main.go:15)        MOVQ    BX, main.y+32(SP)
        0x0012 00018 (/home/gofunc/main.go:15)        MOVQ    $0, main.~r0(SP)
        0x001a 00026 (/home/gofunc/main.go:16)        SUBQ    BX, AX
        0x001d 00029 (/home/gofunc/main.go:16)        MOVQ    AX, main.~r0(SP)
        0x0021 00033 (/home/gofunc/main.go:16)        ADDQ    $8, SP
        0x0025 00037 (/home/gofunc/main.go:16)        POPQ    BP
        0x0026 00038 (/home/gofunc/main.go:16)        RET
        0x0000 55 48 89 e5 48 83 ec 08 48 89 44 24 18 48 89 5c  UH..H...H.D$.H.\
        0x0010 24 20 48 c7 04 24 00 00 00 00 48 29 d8 48 89 04  $ H..$....H).H..
        0x0020 24 48 83 c4 08 5d c3                             $H...].
go:cuinfo.producer.<unlinkable> SDWARFCUINFO dupok size=0
        0x0000 2d 4e 20 2d 6c 20 72 65 67 61 62 69              -N -l regabi
go:cuinfo.packagename.main SDWARFCUINFO dupok size=0
        0x0000 6d 61 69 6e                                      main
main..inittask SNOPTRDATA size=8
        0x0000 00 00 00 00 00 00 00 00                          ........
gclocals·FzY36IO2mY0y4dZ1+Izd/w== SRODATA dupok size=8
        0x0000 01 00 00 00 00 00 00 00                          ........
main.add.arginfo1 SRODATA static dupok size=5
        0x0000 00 08 08 08 ff                                   .....
main.sub.arginfo1 SRODATA static dupok size=5
        0x0000 00 08 08 08 ff                                   .....

我们对add函数汇编结果的前两行最分析(其他的内容用dlv分析,结果更直观):

1) 函数头信息 (STEXT)

main.add STEXT size=103 args=0x10 locals=0x38 funcid=0x0 align=0x0

  • **STEXT**
    表示该段代码属于程序的代码段(Text Segment),用于存放可执行指令。
  • **size=117**
    函数内容编译后的机器码总大小为 103 字节。
  • **args=0x10**
    参数和返回值总大小为 16 字节(对应两个 int64 类型,8+8)。
  • **locals=0x38**
    局部变量和临时存储空间占用 56 字节(包含寄存器值、栈扩展保留空间等)。
  • **funcid=0x0**
    函数标识符,0 表示普通函数(非闭包或方法)。
  • **align=0x0**
    函数入口地址对齐方式,0 表示使用默认对齐(通常为 16 字节)。

2)函数定义 (TEXT)

0x0000 00000 (/home/gofunc/main.go:8) TEXT main.add(SB), ABIInternal, $56-16

  • **main.add(SB)**
    定义函数 main.addSB 是虚拟的静态基址寄存器,表示符号的全局地址。
  • **ABIInternal**
    使用 Go 1.17+ 的内部调用约定(Internal ABI),通过寄存器传递参数,提升性能。
  • **$56-16**
    • 56:函数栈帧总大小(单位字节)。
    • 16:参数和返回值的总大小(由调用者在栈上分配)。

PS:这里可能会疑惑为什么传参和返回值怎么才16字节,不应该34字节吗?这是因为go编译器编译时让返回值复用一个参数的内存了。

1.2、dlv debug

go汇编,汇编指令解析放在每一行最后。

bash 复制代码
[root@test gofunc]# dlv debug main.go
Type 'help' for list of commands.
(dlv) b main.go:3
Breakpoint 1 set at 0x470aea for main.main() ./main.go:3
(dlv) b main.go:8
Breakpoint 2 set at 0x470b4a for main.add() ./main.go:8
(dlv) b main.go:15
Breakpoint 3 set at 0x470bc4 for main.sub() ./main.go:15
(dlv) c
> [Breakpoint 1] main.main() ./main.go:3 (hits goroutine(1):1 total:1) (PC: 0x470aea)
     1: package main
     2:
=>   3: func main() {
     4:     x,y:=int64(1),int64(2)
     5:     x=add(x,y)
     6: }
     7:
     8: func add(a, b int64) int64 {
(dlv) disass
TEXT main.main(SB) /home/gofunc/main.go
        main.go:3       0x470ae0        493b6610                cmp rsp, qword ptr [r14+0x10] //比较栈顶(rsp)和协程栈预警值(g.stackguard0)大小,小于则要栈扩容
        main.go:3       0x470ae4        7634                    jbe 0x470b1a//小于成立,则跳到0x470b9a指令处,进行栈扩容
        main.go:3       0x470ae6        55                      push rbp //入栈
        main.go:3       0x470ae7        4889e5                  mov rbp, rsp
=>      main.go:3       0x470aea*       4883ec20                sub rsp, 0x20//SP寄存器值减去32,表示栈顶向下减小32字节,即为main函数申请32字节的栈内存保存其上下文
        main.go:4       0x470aee        48c744241801000000      mov qword ptr [rsp+0x18], 0x1 //设置x变量的值,占用8字节,即其占用rsp(栈顶)向上[0x18,0x20]之间的内存,第24到32字节之间
        main.go:4       0x470af7        48c744241002000000      mov qword ptr [rsp+0x10], 0x2  //设置y变量的值,占用8字节,即其占用rsp(栈顶)向上[0x10,0x18]之间的内存,第16到24字节之间
        main.go:5       0x470b00        b801000000              mov eax, 0x1 //设置AX寄存器值为1,即调用add的参数x
        main.go:5       0x470b05        bb02000000              mov ebx, 0x2 //设置BX寄存器值为2,即调用add的参数y
        main.go:5       0x470b0a        e831000000              call $main.add //调用add函数
        main.go:5       0x470b0f        4889442418              mov qword ptr [rsp+0x18], rax //将add返回值从AX寄存器中取出来,赋值给x变量
        main.go:6       0x470b14        4883c420                add rsp, 0x20//SP寄存器值加上32,表示栈顶向上增加32字节,即将main函数申请32字节的栈内存归还
        main.go:6       0x470b18        5d                      pop rbp //出栈
        main.go:6       0x470b19        c3                      ret
        main.go:3       0x470b1a        e8c1adffff              call $runtime.morestack_noctxt
        main.go:3       0x470b1f        90                      nop
        main.go:3       0x470b20        ebbe                    jmp $main.main
(dlv) c
> [Breakpoint 2] main.add() ./main.go:8 (hits goroutine(1):1 total:1) (PC: 0x470b4a)
     3: func main() {
     4:     x,y:=int64(1),int64(2)
     5:     x=add(x,y)
     6: }
     7:
=>   8: func add(a, b int64) int64 {
     9:     c:=a+b
    10:     q:=int64(5)
    11:     p:=sub(c, q)
    12:     return p
    13: }
(dlv) disass
TEXT main.add(SB) /home/gofunc/main.go
        main.go:8       0x470b40        493b6610                cmp rsp, qword ptr [r14+0x10]  
        main.go:8       0x470b44        7654                    jbe 0x470b9a 
        main.go:8       0x470b46        55                      push rbp //将BP寄存器的值入栈,此时BP寄存器存的是main函数栈帧起始位置的地址,此时SP值等于SP值减去8字节,用于存储BP寄存器的值
        main.go:8       0x470b47        4889e5                  mov rbp, rsp//将SP寄存器值赋给BP寄存器,作为add函数栈帧的栈底
=>      main.go:8       0x470b4a*       4883ec28                sub rsp, 0x30//SP寄存器值减去48,表示栈顶向下减少48字节,即为add函数申请48字节的栈内存保存其上下文
        main.go:8       0x470b4e        4889442438              mov qword ptr [rsp+0x40], rax //从AX寄存器取参数x的值,放到rsp+0x38地址处,使用的是main函数的栈
        main.go:8       0x470b53        48895c2440              mov qword ptr [rsp+0x48], rbx//从BX寄存器取参数y的值,放到rsp+0x40地址处,使用的是main函数的栈
        main.go:8       0x470b38        48c744241000000000      mov qword ptr [rsp+0x10], 0x0 //令rsp+0x10地址处的值为0,即rsp+[0x10,0x18]之间的内存处存储的值清空
        main.go:9       0x470b41        4801d8                  add rax, rbx//令AX寄存器值等于AX寄存器值+BX寄存器值,即c=a+b的操作
        main.go:9       0x470b44        4889442428              mov qword ptr [rsp+0x28], rax //将AX寄存器值放到rsp+0x28地址处
        main.go:10      0x470b6a        48c744241805000000      mov qword ptr [rsp+0x18], 0x5 //令rsp+0x18地址处值为5,即q:=int64(5)
        main.go:11      0x470b78        bb05000000              mov ebx, 0x5//令AX寄存器值等与0x5(5),为调用sub函数第二个参数
        main.go:11      0x470b7d        0f1f00                  nop dword ptr [rax], eax
        main.go:11      0x470b80        e83b000000              call $main.sub //调用sub本函数
        main.go:11      0x470b5c        4889442420              mov qword ptr [rsp+0x20], rax //将sub函数返回结果放到rsp+0x20地址处
        main.go:12      0x470b8f        4889442410              mov qword ptr [rsp+0x10], rax //令rsp+0x10地址处的值等与AX寄存器的值(莫名其妙的操作,大神知道这个操作做啥吗,评论区见)
        main.go:12      0x470b94        4883c428                add rsp, 0x30//SP寄存器值加上48,表示栈顶向上增加48字节,即将add函数申请48字节的栈内存归还
        main.go:12      0x470b98        5d                      pop rbp //出栈,即将栈顶值(此时其值是main函数栈帧起始位置的地址)设置给BP寄存器
        main.go:12      0x470b99        c3                      ret//返回到main函数调用add函数处继续执行
        main.go:8       0x470b9a        4889442408              mov qword ptr [rsp+0x8], rax //栈扩容前保存下AX寄存器的值,即参数a,因为栈扩容会覆盖AX寄存器的值
        main.go:8       0x470b9f        48895c2410              mov qword ptr [rsp+0x10], rbx//栈扩容前保存下BX寄存器的值,即参数b,因为栈扩容会覆盖BX寄存器的值
        main.go:8       0x470ba4        e837adffff              call $runtime.morestack_noctxt  //栈扩容
        main.go:8       0x470ba9        488b442408              mov rax, qword ptr [rsp+0x8] //扩容后重新设置参数a到AX寄存器
        main.go:8       0x470bae        488b5c2410              mov rbx, qword ptr [rsp+0x10] //扩容后重新设置参数b到BX寄存器
        main.go:8       0x470bb3        eb8b                    jmp $main.add //扩容后重新跳到add函数代码段(不是调用add函数),继续执行add函数
(dlv) c
> [Breakpoint 3] main.sub() ./main.go:15 (hits goroutine(1):1 total:1) (PC: 0x470bc4)
    10:     q:=int64(5)
    11:     q=sub(75, q)
    12:     return c
    13: }
    14:
=>  15: func sub(x, y int64) int64{
    16:   return x -y
    17: }
(dlv) disass
TEXT main.sub(SB) /home/gofunc/main.go
        main.go:15      0x470bc0        55                      push rbp
        main.go:15      0x470bc1        4889e5                  mov rbp, rsp
=>      main.go:15      0x470bc4*       4883ec08                sub rsp, 0x8//申请8字节栈内存
        main.go:15      0x470bc8        4889442418              mov qword ptr [rsp+0x18], rax
        main.go:15      0x470bcd        48895c2420              mov qword ptr [rsp+0x20], rbx
        main.go:15      0x470bd2        48c7042400000000        mov qword ptr [rsp], 0x0//令rsp地址处的值为0
        main.go:16      0x470bda        4829d8                  sub rax, rbx //计算x-y的值并将结果存到AX寄存器中,这样add函数从AX寄存器取sub函数返回值
        main.go:16      0x470bdd        48890424                mov qword ptr [rsp], rax//令rsp地址处的值为AX寄存器的值,也就是x-y的值
        main.go:16      0x470be1        4883c408                add rsp, 0x8 //归还8字节栈内存
        main.go:16      0x470be5        5d                      pop rbp//出栈,BP寄存器值恢复到add函数栈帧起始位置地址
        main.go:16      0x470be6        c3                      ret

通过观察三个函数的汇编,可以看到【栈的申请与归还操作】和【函数栈帧的恢复】:

栈申请:

push rbp //入栈操作等价于【sub rsp 0x8; mov rsp, rbp】,即申请8字节栈内存用于保存caller的函数栈帧起始位置地址

mov rbp, rsp //保存callee的函数栈帧起始位置地址

sub rsp, 0x28 //栈申请

栈归还:

add rsp, 0x28 //栈归还

pop rbp //出栈操作等价于【mov rbp, rsp; add rsp 0x8】,恢复caller的函数栈帧起始位置地址

二、栈布局与函数栈帧

通过第一章对示例的汇编进行分析,我们可以画出其栈布局,假设main函数被调用时的起始地址是ox3e8(1000),那么我们可以得到下图:

可以看到整体还是符合go源码给出的栈布局的:

// (x86)

// +------------------+

// | args from caller |

// +------------------+ <- frame->argp

// | return address |

// +------------------+

// | caller's BP (*) | (*) if framepointer_enabled && varp > sp (暂存调用者函数栈帧起始位置地址)

// +------------------+ <- frame->varp (函数栈帧起始位置地址 BP寄存器)

// | locals |

// +------------------+

// | args to callee |

// +------------------+ <- frame->sp (栈顶,SP寄存器)

一个函数栈帧由BP寄存器和SP寄存器确定。

C中函数栈帧是逐步扩张的(每定义一个变量就扩张一次), 但是go里面函数栈帧的扩张是一次性分配一大块(直接将栈指针移动到所需最大栈空间的位置,即栈顶),然后通过栈顶指针加偏移量这种相对寻址方式使用函数栈帧。

函数栈帧大小在编译期就可以确定!!! 对于栈消耗较大的函数,编译器会在函数头部插入检测代码,如果发现需要进行"栈扩容",就会调用runtime.morestack相关函数重新分配一块足够大的栈空间, 将原来的数据拷过来并释放原来的空间。