go 汇编
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方式二
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)
}
思考
- 函数的执行是后来的先执行,这个和栈的后进先出特征一致
- 执行一个函数需要的一些内存: 参数, 返回值, 自己的局部变量
- 参数和返回值 由调用者分配 (理由后面讲)
- 局部变量所在的内存, 如果在函数执行完毕后,没有用了,那直接在函数栈上即可,返回时,弹出,移动栈顶指针即可,(就是内存释放)
- 局部变量所在的内存, 如果在函数执行完毕后,还要用到,就不能在函数栈上分配了,在其他地方分配,然后返回值返回它地址即可
- 这样函数自己分配的栈空间在调用完毕后可以完全释放了,符合入栈出栈, 可以用来管理函数
- 还有一点可以得出,就是在栈上分配内存的变量它的大小是必须确定的
- 比如定义一个局部变量 是字符串, 然后在后续代码的执行过程中, 字符串做了增加, 这说明你的字符串占用的内存大小不固定,你肯定不能将它完全直接分配在栈上了
- 所以这类型的变量,必定需要在其他地方分配 (堆上分配, 栈上存它的指针,指针大小是确定的)
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 |
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 栈中申请参数 ! ==>可以说明函数是值转递的
- 如果add 里对 参数做修改, 汇编代码怎么处理?
- 返回值的额外理由
- 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)CALL 的add 最后的RET后, SP又重新变成一开始的 SP-48的位置了.
将AX寄存器里的值设置到 r的栈内存位置
0x003e 00062 (main.go:12) MOVQ AX, main.r+16(SP) 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) RET2.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)- 将AX( 即 1) 设置到 SP+32 =SP+24(add申请的栈空间)+8(call add 压栈) 这个位置就是 main一开始申请栈内存后的 sp的位置, 参数c
- 将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)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