---
id: 04e03e046bb46ab34aaeeed79279923b
author: Lee Yunjin
title: Генерация AMD64-ассемблерного кода для условного оператора (if) в Go и анализ команд ветвления
description: Сравнивается процесс, в ходе которого компилятор Go преобразует условный оператор if-else в инструкции условного перехода машинного кода AMD64, такие как CMPQ, JGE и JMP, а также результаты генерации кода ветвлений компилятором C (-O2).
language: ru
date: 2026-05-17T12:00:00Z
path: /blog/posts/go-if-statement-analysis-3012d30c
category: lowlevel
---

## Оператор If в языке Go

Прежде всего, причина, по которой мы выбрали Go, состоит в том, что среди современных языков Go обладает наиболее «изящным» ассемблерным кодом, а по эффективности своего синтаксиса он порой даже превосходит классические языки.

Теперь, когда мы разобрались в принципе работы простой программы на Go на прошлом занятии, давайте сразу сравним Go и Assembly построчно.

### Исходный код

Прежде всего, как и в случае с Go, современные компиляторы, включая GCC, даже автоматически оптимизируют ветвления, которые не несут никакой смысловой нагрузки. Компиляторы языка C, такие как GCC и Clang, при отраслевом стандарте -O2 выполняют весьма агрессивные оптимизации, поэтому эпоха, когда программист с трудом мог полностью доверять компилятору, фактически завершилась ещё со второй половины XX века.

Поэтому, чтобы пример вообще имел смысл, условие должно быть таким, которое компилятору трудно предсказать и заменить другой конструкцией.

```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' (переход, если меньше).
Если по результату предыдущей операции сравнения аргументов было меньше 2, происходит переход по адресу `0x47a8b0`, где находится `JGE`.
Однако в этой инструкции используется регистр `AX`, поэтому необходимо понимать, что именно хранится в этом регистре.

### Команда MOVQ

Далее нужно понять, как с помощью регистра 'CX' сохраняется начальный адрес данных и каким образом из прочитанного по адресу извлекаются фактические данные.

Если рассмотреть диапазон `0x47858`-`0x47863`, видно, что эта операция выполняется поэтапно.

Сначала начальный адрес массива аргументов помещается в регистр CX с помощью инструкции `MOVQ os.Args(SB), CX`. При этом необходимо понимать строковый тип Go.

Тип `string` в Go является структурой, которая состоит из двух 8-байтовых значений, то есть занимает 16 байт.

| struct | 8 байт      | 8 байт        |
| ------ | ----------- | ------------- |
| string | адрес памяти | длина строки |

Визуально это выглядит так, как показано выше: первые 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`: **направление сравнения в инструкции перевёрнуто!**
Это связано с тем, что в машинном коде упреждающий переход при несовпадении условия является **более наглядным и экономит одну инструкцию**, по сравнению с выполнением сравнения при совпадении условия с последующей повторной проверкой на несовпадение.

Такая оптимизация очень классическая, поэтому, в отличие от примера `strconv.Atoi`, рассмотренного выше, она часто встречается даже в компиляторах с довольно низким уровнем оптимизации, и её полезно знать.

Поэтому, применив эту особенность, можно получить исходные коды, которые различаются по тексту, но на уровне ассемблера совпадают на 100%.

### Пример зеркального кода

С помощью приведённого ниже скрипта можно проверить, что bash-скрипт создаёт два зеркальных исходника, и что при рассмотрении только `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"

        // Если сохранить структуру x < 10, взяв 10 за основу, компилятор
        // примет точно такой же механизм JGE и такое же размещение блоков, как в main.go.
        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. Извлечение чистой ассемблерной функции main.main с помощью go tool objdump
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

# Удаление текста виртуальных адресов, смещений и байтов машинного кода
# и фильтрация только полей чистого набора инструкций процессора (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`.
