---
id: 04e03e046bb46ab34aaeeed79279923b
author: Lee Yunjin
title: Analýza generovania AMD64 assembleru pre podmienky (if) v jazyku Go a inštrukcií vetvenia
description: Porovnávame, ako Go kompilátor prekladá podmienené príkazy if-else do strojových inštrukcií vetvenia architektúry AMD64, ako sú CMPQ, JGE a JMP, a výsledky generovania kódu pre vetvenie v C kompilátore (-O2).
language: sk
date: 2026-05-17T12:00:00Z
path: /blog/posts/go-if-statement-analysis-3012d30c
category: lowlevel
---

## If príkazy v jazyku Go

Najprv treba uviesť, že sme si Go vybrali preto, lebo medzi modernými jazykmi má jeho assembler najkrajšiu podobu a efektivita jeho syntaxe môže byť v porovnaní s klasickými jazykmi dokonca presvedčivá.

Teraz, keď sme v predchádzajúcej prednáške pochopili, ako funguje jednoduchý program v jazyku Go, porovnajme priamo Go a Assembly riadok po riadku.

### Zdrojový kód

Najprv, rovnako ako v jazyku Go, aj moderné kompilátory vrátane GCC automaticky optimalizujú vetvenia, ktoré nemajú zmysel. Kompilátory jazyka C, ako GCC a Clang, pri štandardnej úrovni -O2 vykonávajú veľmi agresívne optimalizácie, takže éra, v ktorej programátor kompilátoru nemohol plne dôverovať, sa vlastne definitívne skončila už koncom 20. storočia.

Preto má podmienka zmysel len vtedy, keď je zadaná tak, že ju kompilátor nedokáže predvídať a nahradiť iným výrazom.

```go
package main

import (
    "os"
    "strconv"
)

func main() {
    // Ak túto podmienku nahradíme niečím predvídateľným, ako napríklad x = 10, ktorým možno vetvenie odstrániť,
    // kompilátor optimalizuje kód a vetvenie odstráni.
    // Ak to chceme vidieť priamo v assembleri, v jazyku C použijeme napríklad -O0,
    // alebo necháme kompilátor pracovať s externou hodnotou, ktorú nedokáže predvídať,
    // keďže táto časť sa venuje modernému programovaniu, optimalizáciu binárneho kódu v jazyku Go
    // vypínať nebudeme.
    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 prípade kompilátor nedokáže predvídať vstup, preto sa príkaz vetvenia preloží do strojového kódu nezmenený.

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

Pomocou nástroja go tool nám ochotne ukáže, ktorý príkaz zodpovedá ktorým inštrukciám assemblera.

Keďže sa v tomto diele budeme venovať porovnávacím príkazom a vetveniu if, stačí sledovať niekoľko riadkov.

### Inštrukcie CMPQ a JL

Inštrukcia `CMPQ` slúži na porovnanie dátových typov s veľkosťou 4 bajty (4 slová) a jej názov pochádza z **C**o**MP**are**Q**uadword, odtiaľ skratka **CMPQ**.

Na pamäťovej adrese `0x47a84e` nájdeme príkaz `CMPQ os.Args+8(SB), $0x2`.
V tomto prípade program porovnáva počet prijatých argumentov so šestnástkovou hodnotou `0x2` (teda jednoducho s číslom 2).

Potom sa pomocou `JL` zisťuje, či je počet argumentov menší ako 2 (teda ak je jediným argumentom samotný program), a podľa výsledku sa vykoná skok. Ide o skratku z **J**ump, **L**ess than', teda **JL**.
Ak je argumentov menej ako 2, skáče sa na adresu `0x47a8b0`, kde sa nachádza `JGE`.
Keďže sa však v tejto časti používa register `AX`, musíme poznať, aká hodnota je v registri uložená.

### Inštrukcia MOVQ

Ďalej treba pochopiť, ako sa pomocou registra 'CX' ukladá začiatočná adresa dát a ako sa z nej po jej načítaní získavajú skutočné údaje.

Z rozsahu `0x47858`-`0x47863` vidno, že táto operácia prebieha postupne.

Najprv sa začiatočná adresa poľa argumentov vloží do registra CX pomocou príkazu `MOVQ os.Args(SB), CX`. Pritom je potrebné rozumieť typu string v jazyku Go.

Typ `string` v jazyku Go je štruktúra, ktorá sa skladá z dvoch 8-bajtových údajov, teda má celkovo 16 bajtov.

| struct | 8 bajtov        | 8 bajtov        |
| ------ | --------------- | --------------- |
| string | adresa v pamäti | dĺžka reťazca   |

Vizuálne to vyzerá ako vyššie, pričom prvých 8 bajtov obsahuje začiatočnú adresu reťazca a nasledujúcich 8 bajtov jeho dĺžku.

Adresa reťazca sa teda uloží do registra AX a dĺžka reťazca do registra BX.

### CALL

Aj v predchádzajúcom príspevku sme pri pohľade na funkcie z balíka `runtime` videli pridanú inštrukciu CALL.
Táto inštrukcia sa nachádza pred funkciami používanými v jazyku Go a znamená, že sa volá určitá funkcia; doslova teda ide o CALL. Následne sa pomocou volania CALL reťazec prevedie na celé číslo, pričom kam sa toto číslo uloží, _nie je viditeľné, pretože je abstrahované vo funkcii._

### Inštrukcie CMPQ a JGE

Ak sa vrátime k adrese `0x47a86c`, inštrukcia **porovnáva adresu reťazca s číslom `0xa` (v desiatkovej sústave 10)!**

To znamená, že keďže program daný argument ďalej nepoužíva, **prepísal ho na mieste reťazca a vytvoril tak miesto pre celočíselnú premennú `x`.**

Toto je podstata agresívnej optimalizácie, ktorá prebieha v jazyku Go a podobných jazykoch.

Potom sa objaví inštrukcia JGE, ktorá je skratkou z **J**ump, **G**reater or **E**quals. Tento príkaz teda zisťuje, či je hodnota väčšia alebo rovná porovnávanej hodnote.

Preto nejde o priamo zapísaný výraz `x < 10`, ale **smer porovnania v tomto výraze je obrátený!**
V strojovom kóde je to preto, lebo preskočiť vykonávanie vopred, keď podmienka neplatí, je **intuitívnejšie a ušetrí jednu inštrukciu** v porovnaní s tým, aby sa po jednom porovnaní pri splnenej podmienke ešte raz overovalo, či neplatí.

Táto optimalizácia je veľmi klasická, a preto sa na rozdiel od príkladu `strconv.Atoi` uvedeného vyššie objavuje často aj v kompilátoroch s oveľa nižšou úrovňou optimalizácie, takže je dobré ju poznať.

Ak sa tieto poznatky využijú, možno získať zdrojové kódy, ktoré sa síce líšia, ale na úrovni assemblera sú zhodné na 100 %.

### Príklad zrkadlového kódu

Pomocou nižšie uvedeného skriptu môžeme overiť, že bash skript vytvorí dva zrkadlové zdrojové kódy na 100 % a že po vynechaní metadát, ktoré sa zakaždým menia, sa pri pohľade iba na `main` získa presne rovnaký assembler.

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

# 1. Úplné vymazanie existujúcich zvyšných súborov a adresárov
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 zdrojového kódu pôvodnej verzie (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 zdrojového kódu zrkadlovej verzie (main_from_asm.go)
# Dokonalá symetrická synchronizácia štruktúry operátorov, aby kompilátor použil optimalizačnú šablónu (JGE) bez zmeny
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"

        // Ak sa za základ zoberie 10 a zachová sa štruktúra x < 10, kompilátor
        // použije presne rovnaký mechanizmus JGE a rozloženie blokov ako main.go.
        switch {
        case x < 10:
                println(s1)
        default:
                println(s2)
        }
}
EOF

# 4. Samostatné zostavenie v rovnakom adresári a s rovnakým názvom súboru
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. Extrakcia čistej assembler funkcie main.main pomocou 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

# Odstránenie virtuálnych adries, offsetov a textových údajov strojových bajtov
# a filtrovanie iba polí čistej sady inštrukcií pre CPU (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. Overenie štruktúry strojových inštrukcií oboch programov pomocou 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 spustení skriptu získame nasledujúce informácie.

```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áver

Programovacie jazyky poskytujú veľa abstrakcií, no za nimi sa skrývajú veľmi zaujímavé a agresívne optimalizácie. Tieto poznatky sme navyše mohli využiť na vytvorenie zrkadlového kódu, ktorý sa líši v zdrojovom texte, ale má rovnaký assembler. Ak sa zaujímate o nízkoúrovňové programovanie a narazíte na proprietárny softvér napísaný v jazyku Go, môže byť priama analýza assemblera a rekonštrukcia zdrojového kódu aspoň čiastočne realizovateľná.

## Nasledujúca prednáška

V budúcej hodine sa pozrieme na príkazy if a na ďalší zaujímavý príkaz, `select-case`.
