---
id: 04e03e046bb46ab34aaeeed79279923b
author: Lee Yunjin
title: Generování AMD64 assembleru pro podmínkový příkaz (if) v jazyce Go a analýza větvicích instrukcí
description: Porovnáváme, jak Go kompilátor překládá podmíněný příkaz if-else do strojových větvicích instrukcí AMD64, jako jsou CMPQ, JGE a JMP, a výsledek generování větvicího kódu jazykem C (-O2) s ním srovnáváme.
language: cs
date: 2026-05-17T12:00:00Z
path: /blog/posts/go-if-statement-analysis-3012d30c
category: lowlevel
---

## If příkazy v jazyce Go

Nejprve, důvod, proč jsme pro tento účel vybrali Go, spočívá v tom, že Go je mezi moderními jazyky ten, jehož assembler je nejkrásnější, a jeho syntaktická efektivita je dokonce někdy přesvědčivě vyšší i ve srovnání s klasickými jazyky.

Nyní, když jsme v předchozí lekci pochopili, jak funguje jednoduchý program v Go, porovnejme Go a Assembly řádek po řádku.

### Zdrojový kód

Nejprve, stejně jako v Go, i moderní kompilátory včetně GCC automaticky optimalizují větvení, které nemá žádný smysl. GCC a Clang, tedy kompilátory jazyka C, provádějí při standardní úrovni -O2, což je průmyslový standard, velmi agresivní optimalizace, takže doba, kdy programátor nemohl kompilátoru plně důvěřovat, skončila již koncem 20. století.

Proto musíme z pohledu kompilátoru zadat podmínku, kterou nelze snadno předvídat a nahradit jinou konstrukcí, aby měla alespoň nějaký význam.

```go
package main

import (
    "os"
    "strconv"
)

func main() {
    // Pokud bychom to nahradili něčím předvídatelným, co lze odstranit, například x = 10,
    // kompilátor by větvení optimalizací odstranil.
    // Pokud bychom tedy chtěli takový kód sledovat v assembleru, u C bychom použili například -O0,
    // nebo bychom nechali kompilátor používat nepředvídatelnou externí hodnotu,
    // ale protože tato sekce pojednává o moderním programování, nepoužijeme metodu
    // vypnutí binární optimalizace v 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")
    }
}
```

V tomto případě, protože kompilátor nemůže předvídat vstup, zůstane větvení přeloženo do strojového kódu beze změny.

### Assembler

```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)
```

Pomocí nástroje go tool nám pěkně vysvětlí, které zdrojové řádky odpovídají kterým instrukcím assembleru.

Protože se v této lekci učíme porovnání a větvení if, stačí sledovat několik řádků.

### Instrukce CMPQ a JL

Instrukce `CMPQ` slouží k porovnání datového typu o velikosti 4 bajtů (4 slova); její název vznikl zkrácením z **C**o**MP**are**Q**uadword na **CMPQ**.

Na paměťové adrese `0x47a84e` najdeme příkaz `CMPQ os.Args+8(SB), $0x2`.
V tomto případě program porovnává počet přijatých argumentů se šestnáctkovým číslem `0x2` (tedy prostě 2).

Poté se pomocí `JL` provede skok podle výsledku porovnání, tedy zda je počet argumentů menší než 2 (tj. program má jen sám sebe jako argument). Jde o zkratku z **J**ump, **L**ess than, tedy **JL**.
Pokud byl počet argumentů při předchozím porovnání menší než 2, skáče se na adresu `0x47a8b0`, kde se nachází `JGE`.
Tato instrukce však pracuje s registrem `AX`, proto musíme vědět, jaká hodnota je v registru uložena.

### Instrukce MOVQ

Dále je třeba vědět, jak se pomocí registru 'CX' ukládá počáteční adresa dat a jak se po načtení adresy extrahují skutečná data.

V rozsahu `0x47858`-`0x47863` tuto operaci provádíme postupně.

Nejprve se počáteční adresa pole argumentů vloží do registru CX instrukcí `MOVQ os.Args(SB), CX`. Přitom je nutné porozumět typu string v jazyce Go.

`string` v jazyce Go je struktura, která se skládá ze dvou datových položek po 8 bajtech, tedy celkem z 16 bajtů.

| struct | 8 byte      | 8 byte        |
| ------ | ----------- | ------------- |
| string | adresa v paměti | délka řetězce |

Vizuálně to vypadá jako výše: prvních 8 bajtů obsahuje počáteční adresu řetězce a dalších 8 bajtů jeho délku.

Adresa řetězce se tedy uloží do registru AX a jeho délka do registru BX.

### CALL

I v předchozím příspěvku se u funkcí z balíku `runtime` objevovala instrukce CALL.
Ta se objevuje před funkcemi používanými v Go a znamená, že se volá určitá funkce, což odpovídá jejímu doslovnému významu CALL. Následně se pomocí volání CALL převádí řetězec na celé číslo, přičemž místo, kam se celé číslo ukládá, _není v assembleru vidět, protože je skryto ve funkci._

### Instrukce CMPQ a JGE

Vraťme se k adrese `0x47a86c`: instrukce **porovnává adresu řetězce s číslem `0xa` (v desítkové soustavě 10)!**

To znamená, že protože program daný argument dál nepoužívá, **byla na jeho místě přepsána hodnota, čímž vznikl prostor pro celočíselnou proměnnou `x`.**

Toto je podstata agresivní optimalizace, ke které dochází mimo jiné v jazyce Go.

Poté se objeví instrukce JGE, což je zkratka z **J**ump, **G**reater or **E**quals. Tato instrukce tedy zjišťuje, zda je hodnota větší nebo rovna hodnotě, s níž byla porovnána.

Proto se nejedná o přesně takový zápis `x < 10`, ale **směr porovnání je obrácen!**
V strojovém kódu je totiž přirozenější a o jednu instrukci úspornější podmínku, která neplatí, předem přeskočit, než provést porovnání, když podmínka platí, a poté ověřovat, zda neplatí.

Tato optimalizace je velmi klasická, a proto se na rozdíl od příkladu `strconv.Atoi` výše objevuje často i u kompilátorů s podstatně nižší úrovní optimalizace, takže je dobré ji znát.

Pokud tuto skutečnost využijeme, můžeme získat zdrojové kódy, které se liší, ale na úrovni assembleru se shodují na 100 %.

### Příklad zrcadlového kódu

Pomocí níže uvedeného skriptu lze ověřit, že bash skript vytvoří dva zrcadlové zdrojové kódy a že po vyloučení metadat, která se mezi sestaveními liší, se při pohledu pouze na `main` získá přesně stejný assembler.

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

# 1. Úplné vyčištění zbylých souborů a adresářů z předchozích běhů
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. Zápis původní verze zdrojového kódu (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. Zápis zrcadlové verze zdrojového kódu (main_from_asm.go)
# Symetrická synchronizace struktury operátorů tak, aby kompilátor použil stejnou optimalizační šablonu (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"

        // Pokud zachováme strukturu x < 10 se základem 10, kompilátor
        // přijme přesně stejný mechanismus JGE a rozložení bloků jako u main.go.
        switch {
        case x < 10:
                println(s1)
        default:
                println(s2)
        }
}
EOF

# 4. Sestavení obou verzí ve stejné cestě k adresáři a se stejným názvem souboru
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. Extrakce čisté assembler funkce main.main pomocí 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

# Odstranění virtuálních adres, offsetů a textových dat strojových bajtů
# a filtrování pouze polí čisté sady instrukcí, které CPU provede (Opcode a Operands)
awk '{print $4, $5, $6, $7}' orig.asm > orig_pure.asm
awk '{print $4, $5, $6, $7}' asm.asm > asm_pure.asm

# 6. Ověření shody struktury strojových instrukcí pomocí 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 "------------------------------------------------------------"
```

Po spuštění skriptu získáme následující informace.

```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% 일치합니다! <===
컴파일러의 최적화 파이프라인 가이드라인을 완벽하게 동기화하여 동일한 어셈블리를 얻었습니다.
------------------------------------------------------------
```

### Závěr

Programovací jazyky nabízejí mnoho abstrakcí, avšak za touto abstrakcí se skrývají velmi zajímavé a agresivní optimalizace. Také jsme díky tomuto poznatku dokázali vytvořit zrcadlové kódy, které se liší ve zdrojovém kódu, ale mají stejný assembler. Pokud vás zajímá nízkoúrovňové programování a narazíte na proprietární software napsaný v Go, možná nebude nemožné rozebrat assembler a analýzou obnovit zdrojový kód.

## Příští lekce

V příští lekci se podíváme na příkaz `select-case`, který je také zajímavý, a to vedle příkazu If.
