如何查看 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/