---
id: 04e03e046bb46ab34aaeeed79279923b
author: Lee Yunjin
title: Go 条件语句(if)的 AMD64 汇编代码生成与分支指令分析
description: Go 编译器将 if-else 条件语句转换为 CMPQ、JGE、JMP 等 AMD64 机器码分支指令的过程，并将其与 C 编译器（-O2）生成的分支代码结果进行比较。
language: zh
date: 2026-05-17T12:00:00Z
path: /blog/posts/go-if-statement-analysis-3012d30c
category: lowlevel
---

## Go 语言中的 If 语句

首先，我们选择 Go，是因为在现代语言中，Go 的汇编代码是最"优美"的，并且与经典语言相比，其语法的效率有时甚至是压倒性的。

现在，既然你已经理解了上一讲中简单 Go 程序的运行方式，那就让我们逐行比较 Go 与 Assembly 吧。

### 源代码

首先，不仅是 Go，甚至包括 GCC 在内的现代编译器，都会自动优化那些没有意义的分支语句。GCC、Clang 等 C 语言编译器在业界标准的 -O2 下也会进行非常激进的优化，因此程序员难以完全信任编译器的时代，最终早已在 20 世纪后半叶成为过去。

因此，从编译器的角度看，如果要让分支语句真正有意义，就必须给予编译器难以预测并改写为其他语句的条件。

```go
package main

import (
    "os"
    "strconv"
)

func main() {
    // 如果用 x = 10 这样可预测且能够消除分支的内容来凑数，
    // 编译器会进行优化并消除分支。
    // 因此，若想直接通过汇编观察这种情况，在 C 语言中需要使用 -O0 等选项，
    // 或者让编译器使用无法预测的外部值，
    // 但由于本节讨论的是现代编程，我们不采用关闭 Go 二进制优化的方式。
    if len(os.Args) < 2 {
        return
    }
    x, _ := strconv.Atoi(os.Args[1])

    if x < 10 {
        println("X is smaller than 10")
    } else {
        println("X is larger or same as 10")
    }
}
```

在这种情况下，由于编译器无法预测输入，分支语句会原样被翻译为机器码。

### 汇编语言

```asm
TEXT main.main(SB) /home/yjlee/introduction-to-golang/learn-golang/if-and-switch/golang-if/main.go
  main.go:8             0x47a840                493b6610                CMPQ SP, 0x10(R14)
  main.go:8             0x47a844                7670                    JBE 0x47a8b6
  main.go:8             0x47a846                55                      PUSHQ BP
  main.go:8             0x47a847                4889e5                  MOVQ SP, BP
  main.go:8             0x47a84a                4883ec10                SUBQ $0x10, SP
  main.go:15            0x47a84e                48833d12fb0a0002        CMPQ os.Args+8(SB), $0x2
  main.go:15            0x47a856                7c58                    JL 0x47a8b0
  main.go:15            0x47a858                488b0d01fb0a00          MOVQ os.Args(SB), CX
  main.go:18            0x47a85f                488b4110                MOVQ 0x10(CX), AX
  main.go:18            0x47a863                488b5918                MOVQ 0x18(CX), BX
  main.go:18            0x47a867                e834e8ffff              CALL strconv.Atoi(SB)
  main.go:20            0x47a86c                4883f80a                CMPQ AX, $0xa
  main.go:20            0x47a870                7d1d                    JGE 0x47a88f
  main.go:21            0x47a872                e809befbff              CALL runtime.printlock(SB)
  main.go:21            0x47a877                488d0519f50100          LEAQ 0x1f519(IP), AX
  main.go:21            0x47a87e                bb15000000              MOVL $0x15, BX
  main.go:21            0x47a883                e878c6fbff              CALL runtime.printstring(SB)
  main.go:21            0x47a888                e853befbff              CALL runtime.printunlock(SB)
  main.go:21            0x47a88d                eb1b                    JMP 0x47a8aa
  main.go:23            0x47a88f                e8ecbdfbff              CALL runtime.printlock(SB)
  main.go:23            0x47a894                488d05c8040200          LEAQ 0x204c8(IP), AX
  main.go:23            0x47a89b                bb1a000000              MOVL $0x1a, BX
  main.go:23            0x47a8a0                e85bc6fbff              CALL runtime.printstring(SB)
  main.go:23            0x47a8a5                e836befbff              CALL runtime.printunlock(SB)
  main.go:25            0x47a8aa                4883c410                ADDQ $0x10, SP
  main.go:25            0x47a8ae                5d                      POPQ BP
  main.go:25            0x47a8af                c3                      RET
  main.go:16            0x47a8b0                4883c410                ADDQ $0x10, SP
  main.go:16            0x47a8b4                5d                      POPQ BP
  main.go:16            0x47a8b5                c3                      RET
  main.go:8             0x47a8b6                e845f0feff              CALL runtime.morestack_noctxt.abi0(SB)
  main.go:8             0x47a8bb                eb83                    JMP main.main(SB)
```

使用 go tool 时，它会贴心地逐一告诉你哪些语法与哪些汇编指令相对应。

由于这次要学习的是比较语句和 if 分支语句，只需关注其中几行即可。

### CMPQ 指令与 JL 指令

`CMPQ` 指令是用于比较 4 字节（4 字）数据类型的指令，其词源为 **C**o**MP**are**Q**uadword，因此缩写为 **CMPQ**。

观察内存地址 `0x47a84e`，其中包含 `CMPQ os.Args+8(SB), $0x2` 语句。
在这种情况下，程序会比较输入参数的个数与十六进制 `0x2`（即十进制的 2）。

随后，通过 `JL` 判断参数是否小于 2（即程序自身是唯一参数的情况），并进行比较后的跳转。也就是说，**J**ump, **L**ess than 的缩写即为 **JL**。
对于前面的比较运算，如果参数小于 2，则会跳转到地址 `0x47a8b0`，那里有一条 `JGE` 指令。
然而，该语句使用的是 `AX` 寄存器，因此必须弄清寄存器中所保存的值究竟是什么。

### MOVQ 指令

接下来，需要了解如何利用 `CX` 寄存器保存数据的起始地址，并在读取该地址后如何提取实际数据。

观察 `0x47858`-`0x47863` 的范围，可以看到分阶段执行这一操作。

首先，通过 `MOVQ os.Args(SB), CX` 指令将参数数组的起始地址放入 CX 寄存器。此时需要理解 Go 的字符串类型。

Go 的 `string` 是一个结构体，该结构体由两个 8 字节数据组成，共 16 字节。

| struct | 8 byte      | 8 byte        |
| ------ | ----------- | ------------- |
| string | mem address | string length |

从图示上看就如上表所示，前 8 字节保存字符串的起始地址，后 8 字节保存字符串的长度。

因此，将字符串的地址存入 AX 寄存器，将字符串的长度存入 BX 寄存器。

### CALL

在之前的文章中，观察 `runtime` 相关函数时，也能看到 CALL 这一指令。
它出现在 Go 所使用的函数前面，表示**调用**某个函数，字面上就是 CALL。之后，通过 CALL 函数将字符串转换为整数，而这时整数被保存在哪里，**对函数进行了抽象，因此无法直接看到。**

### CMPQ 指令与 JGE 指令

回到之前的地址 `0x47a86c`，该指令**正在将字符串的地址与数字 `0xa`（十进制的 10）进行比较！**

这意味着，由于程序中不再使用该参数，**编译器便覆盖了字符串的位置，以此腾出整型变量 `x` 的空间。**

这就是 Go 等语言中所进行的激进优化的真实面目。

随后出现的 JGE 指令，是 **J**ump, **G**reater or **E**quals 的缩写。因此，该语句询问的是与比较对象相比是否大于或等于。

所以，它并不是原样的 `x < 10` 语句，而是**比较方向被颠倒的 `x < 10`！**
这是因为在机器码中，当条件不成立时提前跳过，比起条件成立时先执行一次比较、再确认是否不成立，**更加直观，并且能节省一条指令**。

这种优化非常经典，因此即使在优化水平较低的编译器中，也经常出现这种模式，值得掌握。

基于这一点加以应用，就能得到源代码虽不同、但在汇编层面 100% 一致的源代码。

### 镜像代码示例

运行下面的脚本，Bash 脚本会生成两份 100% 镜像的源代码，并在排除每次都会变化的元数据后，验证仅看 `main` 时能得到完全相同的汇编。

```bash
#!/usr/bin/env bash

# 1. 彻底清理原有残留文件及目录
echo "[1/6] Cleaning up old artifacts..."
rm -rf test_dir main_orig main_asm orig.asm asm.asm orig_pure.asm asm_pure.asm
mkdir -p test_dir

# 2. 编写原始版本源代码 (main.go)
echo "[2/6] Generating main.go..."
cat << 'EOF' > main.go
package main

import (
        "os"
        "strconv"
)

func main() {
        if len(os.Args) < 2 {
                return
        }
        x, _ := strconv.Atoi(os.Args[1])

        s1 := "X is smaller than 10"
        s2 := "X is larger or same as 10"

        if x < 10 {
                println(s1)
        } else {
                println(s2)
        }
}
EOF

# 3. 编写镜像版本源代码 (main_from_asm.go)
# 使编译器采用优化模板(JGE)，使运算符结构完全对称同步
echo "[3/6] Generating main_from_asm.go..."
cat << 'EOF' > main_from_asm.go
package main

import (
        "os"
        "strconv"
)

func main() {
        if len(os.Args) < 2 {
                return
        }
        x, _ := strconv.Atoi(os.Args[1])

        s1 := "X is smaller than 10"
        s2 := "X is larger or same as 10"

        // 以 10 为基准并保持 x < 10 的结构，编译器将
        // 采用与 main.go 完全相同的 JGE 机制及块布局。
        switch {
        case x < 10:
                println(s1)
        default:
                println(s2)
        }
}
EOF

# 4. 在相同目录路径和文件名环境下分别执行构建
echo "[4/6] Compiling both sources inside 'test_dir'..."
cp main.go test_dir/main.go
cd test_dir && go build -o ../main_orig main.go && cd ..
rm test_dir/main.go

cp main_from_asm.go test_dir/main.go
cd test_dir && go build -o ../main_asm main.go && cd ..

# 5. 使用 go tool objdump 提取纯粹的 main.main 汇编函数
echo "[5/6] Extracting main.main assembly sections..."
go tool objdump -s "main\.main" main_orig > orig.asm
go tool objdump -s "main\.main" main_asm > asm.asm

# 去除虚拟地址、偏移量和机器码字节文本，
# 仅过滤出 CPU 实际执行的纯指令集（操作码与操作数）字段
awk '{print $4, $5, $6, $7}' orig.asm > orig_pure.asm
awk '{print $4, $5, $6, $7}' asm.asm > asm_pure.asm

# 6. 通过 diff 验证两个机器指令结构
echo "[6/6] Verifying assembly structural integrity via diff..."
echo "------------------------------------------------------------"
if diff orig_pure.asm asm_pure.asm > /dev/null; then
    echo "===> [成功] 两个二进制文件的 main.main 机器指令逻辑 100% 一致！ <==="
    echo "通过将编译器的优化流水线指引完全同步，我们获得了相同的汇编。"
else
    echo "===> [失败] 汇编指令结构中发现差异。 <==="
    diff -u orig_pure.asm asm_pure.asm
fi
echo "------------------------------------------------------------"
```

实际运行该脚本，可以得到如下信息。

```text
[1/6] Cleaning up old artifacts...
[2/6] Generating main.go...
[3/6] Generating main_from_asm.go...
[4/6] Compiling both sources inside 'test_dir'...
[5/6] Extracting main.main assembly sections...
[6/6] Verifying assembly structural integrity via diff...
------------------------------------------------------------
===> [成功] 两个二进制文件的 main.main 机器指令逻辑 100% 一致！ <===
通过将编译器的优化流水线指引完全同步，我们获得了相同的汇编。
------------------------------------------------------------
```

### 结论

编程语言提供了许多抽象，但在抽象之下隐藏着非常有趣且激进的优化。此外，利用这一点，我们还能编写出源代码不同、但汇编完全相同的镜像代码。如果你对底层感兴趣，并遇到以 Go 编写的专有软件，那么直接反汇编并分析汇编以恢复源代码，看来也并非不可能的事情。

## 下一讲

下一次，我们将学习 If 语句以及另一个同样有趣的 `select-case` 语句。
