如何查看 go 执行的汇编语言:从编译到反汇编的实战方法
做Go开发久了,难免会遇到一些“表层优化无效”的场景——比如高频函数执行效率不达预期、并发逻辑出现诡异的性能波动,或是想搞懂sync.Pool这类底层组件的执行细节。这时候,直接查看Go代码对应的汇编语言,往往能戳中问题本质。
很多开发者觉得汇编“高深难懂”,但实际上查看Go汇编的操作很简单,且我们不需要精通所有指令,只要能看懂关键逻辑即可。今天就分享3种实战中最常用的Go汇编查看方法,搭配代码示例一步步演示,再聊聊汇编解读的核心技巧和避坑点。
一、为什么要查看汇编语言?
- 性能优化:比如循环内的变量分配、函数调用开销,表层代码看不出来,汇编能直观显示指令冗余
- 底层原理验证:比如确认Go的
defer是怎么实现的、interface类型断言的指令开销 - 调试诡异问题:比如不同架构(x86/arm)下的执行差异,或是编译优化导致的逻辑偏差
接下来的方法,我们都用这个简单的Go代码作为演示,方便对比效果:
package main
// 简单加法函数,用于演示汇编
func add(a, b int) int {
return a + b
}
func main() {
result := add(10, 20)
println(result)
}二、三种常用方法
Go官方工具链提供了完整的汇编查看能力,不需要额外安装第三方工具。以下方法按“使用频率”排序,新手建议从第一种开始上手。
(一)编译时直接生成汇编代码(最常用)
这种方法是“源头查看”——让Go编译器在编译过程中,直接输出对应的汇编代码文件。优点是能关联源码行号,方便对照解读;缺点是只能看编译后的静态汇编,看不到运行时的动态执行。
操作步骤:
- 基础命令生成汇编:在代码目录执行以下命令,会生成一个
main.s的汇编文件# -S 表示输出汇编代码,后面跟源文件 ``go tool compile -S main.go > main.s - 关键参数:关闭优化(新手必加):Go编译器默认会做优化(比如内联、常量折叠),可能会让汇编代码和源码对应关系不明显。新手建议加
-N -l参数关闭优化:# -N 关闭编译优化,-l 关闭函数内联 ``go tool compile -S -N -l main.go > main.s
结果解读:
打开生成的main.s,找到add函数对应的汇编(搜索“func add”),核心部分如下:
// 对应add函数的汇编
"".add STEXT nosplit size=17 args=0x18 locals=0x0
0x0000 00000 (main.go:4) TEXT "".add(SB), NOSPLIT|ABIInternal, $0-24
0x0000 00000 (main.go:4) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:4) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:5) MOVQ "".a+8(SP), AX // 把参数a加载到AX寄存器
0x0005 00005 (main.go:5) ADDQ "".b+16(SP), AX // 把参数b加到AX寄存器
0x000a 00010 (main.go:5) MOVQ AX, "".~r2+24(SP) // 把结果存到返回值位置
0x000f 00015 (main.go:5) RET // 函数返回这段汇编和源码的对应关系很清晰:先把参数a和b加载到寄存器,执行加法后返回结果。新手不用纠结所有指令,重点看MOVQ(数据移动)、ADDQ(加法)、RET(返回)这些核心操作即可。
(二)反汇编已编译的二进制文件(查线上程序)
如果遇到线上程序性能问题,没法拿到源码重新编译,就可以用这种方法——先把Go程序编译成二进制文件,再对二进制反汇编。优点是能直接分析线上部署的程序;缺点是如果编译时剥离了调试信息,就看不到源码行号关联。
操作步骤:
- 编译二进制文件:
# 编译成名为main的二进制文件(Windows下是main.exe) ``go build -o main main.go如果要保留调试信息(方便关联源码),编译时不要加-ldflags "-w -s"参数,这个参数会剥离调试信息减小体积。 - 反汇编二进制文件:
# -S 表示输出汇编,后面跟二进制文件名 ``go tool objdump -S main > main_dump.s - 过滤特定函数(高效技巧):如果只关心
add函数,用grep过滤更高效:go tool objdump -S main | grep -A 10 -B 5 "add"其中-A 10表示显示匹配行后10行,-B 5表示显示匹配行前5行。
结果解读:
反汇编的结果和方法1类似,但会包含整个程序的汇编(包括Go运行时初始化逻辑)。过滤后add函数的汇编和方法1一致,适合快速定位特定函数的线上执行代码。
(三)实时查看运行中的汇编(调试场景)
如果想看到代码“执行到某一步时”的汇编状态(比如调试分支逻辑),就需要用Go的调试器delve。这种方法最灵活,但操作稍复杂,适合问题排查。
操作步骤:
-
安装delve调试器:
go install github.com/go-delve/delve/cmd/dlv@latest -
启动调试并查看汇编:
启动调试会话
dlv debug main.go在main函数处设置断点
(dlv) break main开始运行程序
(dlv) continue查看当前位置的汇编(默认显示10行)
(dlv) disassemble单步执行汇编指令(按汇编步骤调试) ``(dlv) stepi`
结果解读:
执行disassemble后,会显示当前断点位置的汇编代码,配合stepi(单步执行汇编指令),能清晰看到程序执行的每一步指令变化,比如进入add函数时寄存器的数值变化
三、常见问题
Q1、生成的汇编文件超大,全是看不懂的运行时代码?
这是因为Go汇编包含了运行时(如GC、调度器)的代码。解决方法:用grep过滤特定函数,比如grep -A 20 -B 5 "func add" main.s,只看目标函数的汇编。
Q2、汇编代码和源码对应不上,比如函数不见了?
大概率是Go编译器做了“内联优化”——把小函数直接嵌入调用处,减少函数调用开销。解决方法:编译时加-l参数关闭内联(如方法1中的-N -l)。
Q3、不同电脑生成的汇编不一样?
是的。汇编和CPU架构(x86/arm)、Go版本、编译参数都有关。比如Mac的M系列芯片是arm架构,生成的汇编指令和Intel的x86架构差异很大
Q4、delve调试时,disassemble命令报错?
可能是调试器没找到调试信息。解决方法:编译时不要加-ldflags "-w -s"参数,确保保留调试信息;如果是线上二进制,编译时要加-gcflags "all=-l"保留调试信息。
总结
最后用一张表总结3种方法的适用场景,方便大家快速选择:
| 方法 | 核心命令 | 适用场景 | 优点 |
|---|---|---|---|
| 编译生成汇编 | go tool compile -S -N -l main.go | 日常开发、性能优化 | 关联源码行号,易解读 |
| 二进制反汇编 | go tool objdump -S main | 线上程序分析 | 直接分析部署的二进制 |
| delve实时调试 | dlv debug + disassemble | 分支逻辑调试、问题排查 | 动态查看执行过程 |
最后提供一个可以在线查看执行的汇编结果的工具:Compiler Explorer (godbolt.org)
用起来也是比较简单
- 编写源代码
- 将源代码复制到编译网站
- 右键执行 Compile
- 即可查看 汇编过程
如下图:
查看Go汇编的核心不是“精通汇编指令”,而是“通过汇编理解Go的底层执行逻辑”。刚开始可以从简单函数(比如加法、循环)练手,熟悉后再去分析复杂组件,慢慢就会发现——原来Go的很多“黑魔法”,在汇编层面其实很清晰。
如果大家在实践中遇到特殊场景的汇编解读问题,欢迎在评论区留言交流!
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!
本文原文链接: https://fiveyoboy.com/articles/go-assembly-language/
备用原文链接: https://blog.fiveyoboy.com/articles/go-assembly-language/