目录

如何查看 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编译器在编译过程中,直接输出对应的汇编代码文件。优点是能关联源码行号,方便对照解读;缺点是只能看编译后的静态汇编,看不到运行时的动态执行。

操作步骤

  1. 基础命令生成汇编:在代码目录执行以下命令,会生成一个main.s的汇编文件 # -S 表示输出汇编代码,后面跟源文件 ``go tool compile -S main.go > main.s
  2. 关键参数:关闭优化(新手必加):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                 // 函数返回

这段汇编和源码的对应关系很清晰:先把参数ab加载到寄存器,执行加法后返回结果。新手不用纠结所有指令,重点看MOVQ(数据移动)、ADDQ(加法)、RET(返回)这些核心操作即可。

(二)反汇编已编译的二进制文件(查线上程序)

如果遇到线上程序性能问题,没法拿到源码重新编译,就可以用这种方法——先把Go程序编译成二进制文件,再对二进制反汇编。优点是能直接分析线上部署的程序;缺点是如果编译时剥离了调试信息,就看不到源码行号关联。

操作步骤

  1. 编译二进制文件# 编译成名为main的二进制文件(Windows下是main.exe) ``go build -o main main.go如果要保留调试信息(方便关联源码),编译时不要加-ldflags "-w -s"参数,这个参数会剥离调试信息减小体积。
  2. 反汇编二进制文件# -S 表示输出汇编,后面跟二进制文件名 ``go tool objdump -S main > main_dump.s
  3. 过滤特定函数(高效技巧):如果只关心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。这种方法最灵活,但操作稍复杂,适合问题排查。

操作步骤

  1. 安装delve调试器go install github.com/go-delve/delve/cmd/dlv@latest

  2. 启动调试并查看汇编

    启动调试会话 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)

用起来也是比较简单

  1. 编写源代码
  2. 将源代码复制到编译网站
  3. 右键执行 Compile
  4. 即可查看 汇编过程

如下图:

/img/go-assembly-language/0201.png
在线查看go执行的汇编语言

查看Go汇编的核心不是“精通汇编指令”,而是“通过汇编理解Go的底层执行逻辑”。刚开始可以从简单函数(比如加法、循环)练手,熟悉后再去分析复杂组件,慢慢就会发现——原来Go的很多“黑魔法”,在汇编层面其实很清晰。

如果大家在实践中遇到特殊场景的汇编解读问题,欢迎在评论区留言交流!

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-assembly-language/

备用原文链接: https://blog.fiveyoboy.com/articles/go-assembly-language/