go 汇编

Created

2024-10-28 13:24:16

Updated

2024-10-28 13:24:23

1 查看汇编代码1

Important
  • 查看go底层执行的代码时, 可以通过查看汇编来找到源码位置
  • 下面几个查看的方式,结果格式略有不同
方式一
# -l 禁止内联优化
# -N Disable optimizations.
# 编译的二进制文件 禁止内联优化
go build -o main -gcflags "-N -l" main.go
# -S Print assembly listing to standard output (code only).
# 编译成二进制文件,并打印汇编代码
go build -o main -gcflags="-N -l -S" main.go

# 从可执行文件 反编译出汇编代码
# 表示只输出 main 包中相关的汇编
go tool objdump -s "main." main
# 如果使用 "main.main" 则表示 只输出 main.main方法相关的汇编
go tool objdump -s "main.main" main
#  -S   print Go code alongside assembly
# 会打印对应的go代码
go tool objdump -s "main.main" -S main
方式二
#  会生成main.o 文件, -S 输出汇编代码
go tool compile -S -N -l main.go
# 直接看代码的第几行的汇编代码
go tool compile -S -N -l main.go |grep "main.go:8"
方式三
# dlv  disassemble
# 查看汇编代码
(dlv) disassemble -l main.main

2 汇编分析go代码

Tip

更详细的汇编说明后续会专门写教程,有些很多都忘记了…,得重新花时间再看看

2.1 函数调用为啥使用栈

package main

func C() {
    println(4)
}
func B() {
    C()
    println(3)
}
func A() {
    B()
    println(2)
}

func main() {
    A()
    println(1)
}
思考
  1. 函数的执行是后来的先执行,这个和栈的后进先出特征一致
  2. 执行一个函数需要的一些内存: 参数, 返回值, 自己的局部变量
    1. 参数和返回值 由调用者分配 (理由后面讲)
    2. 局部变量所在的内存, 如果在函数执行完毕后,没有用了,那直接在函数栈上即可,返回时,弹出,移动栈顶指针即可,(就是内存释放)
    3. 局部变量所在的内存, 如果在函数执行完毕后,还要用到,就不能在函数栈上分配了,在其他地方分配,然后返回值返回它地址即可
  3. 这样函数自己分配的栈空间在调用完毕后可以完全释放了,符合入栈出栈, 可以用来管理函数
  4. 还有一点可以得出,就是在栈上分配内存的变量它的大小是必须确定的
    1. 比如定义一个局部变量 是字符串, 然后在后续代码的执行过程中, 字符串做了增加, 这说明你的字符串占用的内存大小不固定,你肯定不能将它完全直接分配在栈上了
    2. 所以这类型的变量,必定需要在其他地方分配 (堆上分配, 栈上存它的指针,指针大小是确定的)

2.2 要分析的go代码

package main

func add(c, d int64) (sum int64) {
    var add_local_var int64 = 3
    sum = c + d + add_local_var
    return sum
}

func main() {
    var r int64
    var a, b int64 = 1, 2
    r = add(a, b)
    println(r)
}
注意

go 的版本不一样看到的结果可能不一样

OS go version
Ubuntu 20.04.3 LTS go1.20.4
go tool compile -S -N -l main.go

2.3 main.main

item description
caller 调用者
callee 被调用者
SP 栈顶指针
$数字 立即数: 就是直接是指令来进行数据赋值,而不是从哪块内存读取数据的操作
  • args: 参数+返回值, main()的调用者传递给main的参数,没有,显示 0
  • locals: 包含如下
    • main函数本地使用的局部变量
    • 调用其他所有函数时传递的参数以及它的返回值 (=callee 所有 args之和) ,参数和返回值由caller负责分配
为什么callee的参数和返回值要由caller分配内存呢?
  • 如果在 callee 栈中分配, 那么每次调用 callee 都要进行内存分配和释放的操作,比如你在 main函数里多次 调用 add(),每次调用都要分配一次内存和释放
  • 如果在 caller 栈中分配内存,caller 调用多次 callee, 也只需要一次内存分配操作
  • 局部变量 a和b 申请的栈空间直接作为 add 的参数(c ,d)不行吗?
    • 如果add 里对 参数做修改, 汇编代码怎么处理?
      • 直接修改 caller的a,b 所在的栈内存吗? 那肯定不行
      • 在 callee 中申请内存? 那又变成多次分配释放的问题了,也不行的
    • 处理方式就是在 caller 栈中申请参数 ! ==>可以说明函数是值转递的
  • 返回值的额外理由
    • callee 的返回值, 肯定是 caller 需要用到,所以 在caller直接申请内存空间就行了,callee 直接将结果写入caller的函数栈内存即可
    • 如果你callee申请 ,最后函数返回会释放内存, 你可能还需要将返回值复制到 caller 的函数栈内,这样会导致额外的内存拷贝操作
main.main STEXT size=109 args=0x0 locals=0x30 funcid=0x0 align=0x0
    # SB  伪寄存器,保存静态基地址(static-base) 指针,即我们程序地址空间的开始地址
    0x0000 00000 (main.go:9)    TEXT    main.main(SB), ABIInternal, $48-0 
    0x0000 00000 (main.go:9)    CMPQ    SP, 16(R14)
    # PCDATA  垃圾回收相关, 由go 编译器加入
    0x0004 00004 (main.go:9)    PCDATA  $0, $-2
    0x0004 00004 (main.go:9)    JLS 102
    0x0006 00006 (main.go:9)    PCDATA  $0, $-1
    # "main 申请的48个字节"
    # `SUBQ $48, SP`  SP=SP-48 ,栈顶指针往下移动48个字节, 就是main函数 申请 48个字节的栈空间
    # main的局部变量 a b 共16个字节
    # 调用add ,需要 传递的参数 16个字节, r 接收add返回值 占8个字节  共 24个字节 都由caller(即main) 分配
    # 保存BP  占8个字节
    # 调用println()  传参 直接使用的r,我估计是编译器明确知道println 不会修改参数的原因?
    0x0006 00006 (main.go:9)    SUBQ    $48, SP 
    # BP 伪寄存器, 调用者(这里就是main的调用者)函数栈的起始位置 仅作为一个指示作用
    # 保存BP 伪寄存器 到栈 SP+40 的位置
    0x000a 00010 (main.go:9)    MOVQ    BP, 40(SP)
    # 将 SP+40 这个地址 (栈地址)  设置给 BP寄存器
    # 现在 BP寄存器指向的栈内存 存储的是 原来BP的值
    0x000f 00015 (main.go:9)    LEAQ    40(SP), BP
    # FUNCDATA  垃圾回收相关, 由go 编译器加入
    0x0014 00020 (main.go:9)    FUNCDATA    $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
    0x0014 00020 (main.go:9)    FUNCDATA    $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
    # 将 SP+16 的栈内存 设置 立即数 0 ==> r变量
    0x0014 00020 (main.go:10)   MOVQ    $0, main.r+16(SP)
    # 将 SP+32 的栈内存 设置 立即数 1 ==> a变量
    0x001d 00029 (main.go:11)   MOVQ    $1, main.a+32(SP)
    # 将 SP+24 的栈内存 设置 立即数 2 ==> b变量
    0x0026 00038 (main.go:11)   MOVQ    $2, main.b+24(SP)
    # 将 立即数 1 设置到 寄存器 AX
    0x002f 00047 (main.go:12)   MOVL    $1, AX
    # 将 立即数 2 设置到 寄存器 BX
    0x0034 00052 (main.go:12)   MOVL    $2, BX
    0x0039 00057 (main.go:12)   PCDATA  $1, $0
  • 调用 add 函数, 去看main.add
  • CALL 指令
    • 将ip 压栈 就是将下一条指令(调用函数后的下一个要执行的指令)的地址 压栈
    • 压栈: 先将栈顶指针下移, SP=SP-8 (内存地址占8个字节), 然后将ip设置到SP指向的栈上
    • 调用的add 函数 最后会有一个RET 操作, 是弹栈. (call 和 ret)
    0x0039 00057 (main.go:12)   CALL    main.add(SB)
Diagram

CALL 的add 最后的RET后, SP又重新变成一开始的 SP-48的位置了.
将AX寄存器里的值设置到 r的栈内存位置

    0x003e 00062 (main.go:12)   MOVQ    AX, main.r+16(SP)
Diagram
    0x0043 00067 (main.go:13)   CALL    runtime.printlock(SB)
    0x0048 00072 (main.go:13)   MOVQ    main.r+16(SP), AX
    0x004d 00077 (main.go:13)   CALL    runtime.printint(SB)
    0x0052 00082 (main.go:13)   CALL    runtime.printnl(SB)
    0x0057 00087 (main.go:13)   CALL    runtime.printunlock(SB)

复原BP,SP

    0x005c 00092 (main.go:14)   MOVQ    40(SP), BP
    0x0061 00097 (main.go:14)   ADDQ    $48, SP
    0x0065 00101 (main.go:14)   RET

2.4 main.add

  • args = 0x10=16, 使用了caller的栈内存 (参数+返回值)
  • $24-16
    • 24表示add 申请的栈内存空间,
    • 16表示 用到了 caller的栈内存空间大小 = args
    • 你可能会觉得 不应该是 参数+返回值= 24个字节吗?? 怎么是16,后面说
main.add STEXT nosplit size=63 args=0x10 locals=0x18 funcid=0x0 align=0x0
    0x0000 00000 (main.go:3)    TEXT    main.add(SB), NOSPLIT|ABIInternal, $24-16
  • BP+局部变量(sum+add_local_var) 共24个字节?
  • 你可能又会觉得 这个sum 不就是 main栈空间的r吗? 不直接使用它而在add栈内分配了内存呢? 后面解答
    0x0000 00000 (main.go:3)    SUBQ    $24, SP
    0x0004 00004 (main.go:3)    MOVQ    BP, 16(SP)
    0x0009 00009 (main.go:3)    LEAQ    16(SP), BP
    0x000e 00014 (main.go:3)    FUNCDATA    $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
    0x000e 00014 (main.go:3)    FUNCDATA    $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
    0x000e 00014 (main.go:3)    FUNCDATA    $5, main.add.arginfo1(SB)
  1. 将AX( 即 1) 设置到 SP+32 =SP+24(add申请的栈空间)+8(call add 压栈) 这个位置就是 main一开始申请栈内存后的 sp的位置, 参数c
  2. 将BX( 即 2) 参数d
    0x000e 00014 (main.go:3)    MOVQ    AX, main.c+32(SP)
    0x0013 00019 (main.go:3)    MOVQ    BX, main.d+40(SP)
  • 初始化 sum变量设置为0
  • 设置 局部变量 add_local_var =3
    0x0018 00024 (main.go:3)    MOVQ    $0, main.sum(SP)
    0x0020 00032 (main.go:4)    MOVQ    $3, main.add_local_var+8(SP)
  • 计算后将结果设置到AX 以及sum变量,我们发现这里并没有操作main的栈内存分配的返回值r
  • 所以 $24-16 写的是16
$24-16的问题, 知道即可,无需太过在意
  • 我在看笔记的时候,发现以前的显示的args是24, 以前的看起来符合我们前面说的.
  • 为什么不一样 我估计是go 版本的问题, 下面是我试了几个go版本后,一个符合的
go1.15.4版本的汇编情况
"".add STEXT nosplit size=60 args=0x18 locals=0x10
0x0000 00000 (main.go:3)    TEXT    "".add(SB), NOSPLIT|ABIInternal, $16-24
0x0000 00000 (main.go:3)    SUBQ    $16, SP
0x0004 00004 (main.go:3)    MOVQ    BP, 8(SP)
0x0009 00009 (main.go:3)    LEAQ    8(SP), BP
0x000e 00014 (main.go:3)    FUNCDATA    $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (main.go:3)    FUNCDATA    $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (main.go:3)    MOVQ    $0, "".sum+40(SP)
0x0017 00023 (main.go:4)    MOVQ    $3, "".add_local_var(SP)
0x001f 00031 (main.go:5)    MOVQ    "".c+24(SP), AX
0x0024 00036 (main.go:5)    ADDQ    "".d+32(SP), AX
0x0029 00041 (main.go:5)    ADDQ    $3, AX
0x002d 00045 (main.go:5)    MOVQ    AX, "".sum+40(SP)
0x0032 00050 (main.go:6)    MOVQ    8(SP), BP
0x0037 00055 (main.go:6)    ADDQ    $16, SP
0x003b 00059 (main.go:6)    RET

我们可以发现 8行和13行, add函数是直接使用main的栈内存作为sum的,所以add它申请的栈内存只有16,而使用的caller的栈内存是24个字节

    0x0029 00041 (main.go:5)    LEAQ    (AX)(BX*1), AX
    0x002d 00045 (main.go:5)    LEAQ    3(AX), AX
    0x0031 00049 (main.go:5)    MOVQ    AX, main.sum(SP)
Diagram

BP和SP归位

    0x0035 00053 (main.go:6)    MOVQ    16(SP), BP
    0x003a 00058 (main.go:6)    ADDQ    $24, SP
  • RET
    • =POP IP, 将之前main call add的时候 压栈的ip,弹出 写入到 ip寄存器,这样 cpu就知道重新执行哪一条指令了
    • 先读取栈数据 写入ip寄存器, 然后 SP=SP+8 往上移动, 这样就回到了 call add 之前的 sp位置了.
    0x003e 00062 (main.go:6)    RET

3 go调用汇编代码写的函数

Back to top