---
id: 04e03e046bb46ab34aaeeed79279923b
author: Lee Yunjin
title: Go言語の条件分岐(if)におけるAMD64アセンブリコードの生成と分岐命令の分析
description: Goコンパイラがif-else条件文を、CMPQ、JGE、JMPなどのAMD64機械語分岐命令に変換する過程を、C コンパイラ(-O2)による分岐コード生成結果と比較します。
language: ja
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`という構文が入っています。
この場合、プログラムが受け取った引数の数と、16進数の`0x2`(つまり単に2)を比較します。

その後、引数が2より小さいか(つまりプログラム自身だけが引数である場合)を判定し、`JL`によって比較後のジャンプを行います。つまりこれは **J**ump, **L**ess than を略して **JL** となります。
直前の比較演算で引数が2より小さかった場合は`0x47a8b0`番地にジャンプしますが、そこには`JGE`があります。
しかし、この構文で使われているのは`AX`レジスタなので、レジスタに格納された値の正体を知る必要があります。

### MOVQ命令

この次に、実際に'CX'レジスタを使ってデータの開始アドレスを格納し、アドレスを読んだ後に実際のデータをどのように取り出そうとしているかを知る必要があります。

`0x47858`-`0x47863`の範囲を見ると、段階的にこの演算を行っています。

まず、引数配列の開始アドレスを`MOVQ os.Args(SB), CX`命令でCXレジスタに格納します。この時、Goの文字列型を理解しておく必要があります。

Goの`string`は構造体であり、この構造体は8バイトのデータ2つで、合計16バイトから構成されています。

| struct | 8 byte      | 8 byte        |
| ------ | ----------- | ------------- |
| string | メモリアドレス | 文字列の長さ    |

視覚的に描くと上記のようになり、前の8バイトは文字列の開始アドレス、後ろの8バイトは文字列の長さが格納されています。

したがって、文字列のアドレスをAXレジスタに、文字列の長さをBXレジスタに格納します。

### CALL

前の投稿でも`runtime`系の関数を見たとき、CALLという命令が付いていました。
これはGoで使用される関数の前に付いており、ある関数を**呼び出す**という意味で、文字通りCALLです。その後、CALL関数を使って文字列を整数に変換しますが、この時、整数をどこに格納するかは_関数に抽象化されて見えません。_

### CMPQ命令 & JGE命令

再び先ほどのアドレス`0x47a86c`に戻ると、命令は**文字列のアドレスと、数値である`0xa`(10進数で10)を比較しています!**

これは、プログラム内でもはや該当の引数を使わないため、**文字列の位置に上書きして整数型変数`x`の場所を作った、という意味です。**

これがGo言語などで行われる積極的な最適化の正体です。

その後、JGEという命令が登場しますが、これは **J**ump, **G**reater or **E**quals の略です。したがってこの構文は、比較対象と照合したときに大きいか等しいかを問うています。

よって、`x < 10`構文そのままではなく、`x < 10`で**構文の比較方向が逆転しています!**
これは機械語では、条件が一致しないときに先に飛び越す方が、条件が一致するときの比較を1回行った後に、さらに一致しないかを確認するより**直感的であり、命令を1つ節約できる**からです。

このような最適化は非常に古典的であるため、上で見た`strconv.Atoi`の例と異なり、最適化がかなり低水準のコンパイラでも頻繁に現れるパターンです。知っておくと良いでしょう。

したがって、このような点を応用すると、ソースは異なるものの、アセンブリ単位で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が実行する純粋な命令セット(Opcode & Operands)フィールドのみをフィルタリング
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`文について見ていくことにします。
