---
id: 04e03e046bb46ab34aaeeed79279923b
author: Lee Yunjin
title: Generowanie kodu assemblera AMD64 dla instrukcji warunkowej (if) w języku Go oraz analiza instrukcji skoku
description: Porównujemy proces, w którym kompilator Go tłumaczy instrukcję warunkową if-else na instrukcje warunkowego skoku maszynowego AMD64, takie jak CMPQ, JGE i JMP, z wynikiem generowania kodu rozgałęzień przez kompilator C (-O2).
language: pl
date: 2026-05-17T12:00:00Z
path: /blog/posts/go-if-statement-analysis-3012d30c
category: lowlevel
---

## Instrukcja If w języku Go

Najpierw powód wyboru Go: spośród nowoczesnych języków Go ma najbardziej „piękny” kod assembly, a efektywność jego składni bywa wręcz przytłaczająca nawet w porównaniu z językami klasycznymi.

Skoro na poprzednim wykładzie zrozumieliśmy, jak działa prosty program w Go, porównajmy teraz Go i Assembly linia po linii.

### Kod źródłowy

Przede wszystkim, nie tylko w Go, ale także w nowoczesnych kompilatorach, w tym GCC, gałęzie, które nie mają znaczenia dla wyniku, są automatycznie optymalizowane. Kompilatory języka C, takie jak GCC i Clang, stosują bardzo agresywne optymalizacje już na poziomie -O2, będącym standardem branżowym, więc era, w której programista nie mógł w pełni ufać kompilatorowi, ostatecznie dobiegła końca już w drugiej połowie XX wieku.

Dlatego, z punktu widzenia kompilatora, warunki, które trudno przewidzieć i zastąpić inną konstrukcją, muszą być sformułowane tak, aby miały sens.

```go
package main

import (
    "os"
    "strconv"
)

func main() {
    // Jeśli zastąpimy to czymś przewidywalnym, co można usunąć z gałęzi, np. x = 10,
    // kompilator zoptymalizuje kod i usunie gałąź.
    // Dlatego, aby zobaczyć to bezpośrednio w assembly, w C używa się np. -O0,
    // albo pozwala kompilatorowi użyć nieprzewidywalnej zewnętrznej wartości,
    // ale ponieważ ten rozdział dotyczy nowoczesnego programowania, nie stosujemy
    // metody wyłączania optymalizacji binarnej w 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")
    }
}
```

W tym przypadku, ponieważ kompilator nie może przewidzieć danych wejściowych, instrukcja rozgałęzienia zostaje przetłumaczona na kod maszynowy bez zmian.

### Język asemblera

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

Narzędzie go tool wskazuje bardzo przystępnie, która konstrukcja języka odpowiada któremu fragmentowi assembly.

Ponieważ w tym wykładzie uczymy się porównań i instrukcji warunkowych if, wystarczy zwrócić uwagę na kilka linii.

### Instrukcje CMPQ i JL

Instrukcja `CMPQ` służy do porównywania typów danych o rozmiarze 4 bajtów (4 słowa), a jej nazwa pochodzi od **C**o**MP**are**Q**uadword, skróconego do **CMPQ**.

Pod adresem pamięci `0x47a84e` znajduje się instrukcja `CMPQ os.Args+8(SB), $0x2`.
W tym przypadku program porównuje liczbę przekazanych argumentów z szesnastkową wartością `0x2` (czyli po prostu 2).

Następnie, jeśli argumentów jest mniej niż 2 (czyli program sam jest jedynym argumentem), za pomocą `JL` wykonywany jest skok po porównaniu. Oznacza to **J**ump, **L**ess than', skrócone do **JL**.
Jeśli w poprzednim porównaniu argumentów było mniej niż 2, skok prowadzi pod adres `0x47a8b0`, gdzie znajduje się `JGE`.
Jednak ta instrukcja operuje na rejestrze `AX`, więc trzeba poznać, co dokładnie jest przechowywane w tym rejestrze.

### Instrukcja MOVQ

Następnie trzeba zrozumieć, jak rejestr `CX` jest wykorzystywany do przechowywania adresu początkowego danych oraz jak po odczytaniu adresu wydobywane są rzeczywiste dane.

Zakres `0x47858`-`0x47863` pokazuje krok po kroku wykonanie tej operacji.

Najpierw adres początkowy tablicy argumentów jest wstawiany do rejestru CX za pomocą instrukcji `MOVQ os.Args(SB), CX`. Wtedy trzeba zrozumieć typ string w Go.

String w Go jest strukturą złożoną z dwóch 8-bajtowych wartości, czyli z 16 bajtów.

| struktura | 8 bajtów    | 8 bajtów      |
| --------- | ----------- | ------------- |
| string    | adres pamięci | długość stringa |

Wizualnie wygląda to jak powyżej: pierwsze 8 bajtów zawiera adres początkowy stringa, a kolejne 8 bajtów zawiera jego długość.

Dlatego adres stringa zapisywany jest w rejestrze AX, a jego długość w rejestrze BX.

### CALL

We wcześniejszym poście, gdy oglądaliśmy funkcje `runtime`, także pojawiała się instrukcja CALL.
Występuje ona przed funkcjami używanymi w Go i oznacza, że funkcja jest **wywoływana**, dosłownie CALL. Następnie, za pomocą wywołania funkcji, string jest konwertowany na liczbę całkowitą, przy czym _to, gdzie zapisywana jest ta liczba, nie jest widoczne, ponieważ zostało ukryte w funkcji._

### Instrukcje CMPQ i JGE

Wracając do wcześniejszego adresu `0x47a86c`, instrukcja **porównuje adres stringa z liczbą `0xa` (dziesiętnie 10)!**

Oznacza to, że ponieważ program nie używa już tego argumentu, **nadpisano nim miejsce stringa, tworząc w ten sposób przestrzeń dla zmiennej całkowitej `x`.**

To właśnie jest istota agresywnych optymalizacji, jakie stosuje się m.in. w języku Go.

Następnie pojawia się instrukcja JGE, będąca skrótem od **J**ump, **G**reater or **E**quals. Ta instrukcja sprawdza więc, czy wartość jest większa lub równa wartości porównywanej.

Zatem zamiast dokładnie `x < 10` mamy **odwrócony kierunek porównania!**
W kodzie maszynowym opłaca się bowiem z góry pominąć blok, gdy warunek nie jest spełniony, zamiast wykonywać porównanie w przypadku spełnienia warunku, a następnie ponownie sprawdzać, czy warunek nie jest spełniony. Rozwiązanie to jest bardziej bezpośrednie i **pozwala zaoszczędzić jedną instrukcję**.

Ta optymalizacja jest bardzo klasyczna, dlatego w przeciwieństwie do przykładu `strconv.Atoi` omówionego wyżej często pojawia się nawet w kompilatorach o znacznie niższym poziomie optymalizacji, więc warto ją znać.

Dzięki zastosowaniu tej zasady można uzyskać źródła różniące się od siebie, a mimo to dające na poziomie assembly w 100% identyczny wynik.

### Przykład kodu lustrzanego

Korzystając z poniższego skryptu, skrypt bash tworzy dwa w 100% lustrzane źródła, a następnie weryfikuje, że po pominięciu metadanych, które się zmieniają, sama funkcja `main` daje dokładnie identyczny assembly.

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

# 1. Całkowite czyszczenie pozostałych plików i katalogów z poprzednich uruchomień
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. Zapis oryginalnego kodu źródłowego (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. Zapis kodu lustrzanego (main_from_asm.go)
# Struktura operatorów jest symetrycznie dopasowana, aby kompilator użył tego samego szablonu optymalizacji (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"

        // Jeśli zachowamy strukturę x < 10, używając 10 jako punktu odniesienia, kompilator
        // przyjmie dokładnie ten sam mechanizm JGE i ten sam układ bloków co w main.go.
        switch {
        case x < 10:
                println(s1)
        default:
                println(s2)
        }
}
EOF

# 4. Kompilacja obu źródeł w tym samym katalogu i pod tą samą nazwą pliku
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. Wyodrębnienie czystego assembly funkcji main.main za 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

# Usunięcie adresów wirtualnych, przesunięć i tekstu bajtów maszynowych,
# filtrowanie wyłącznie pól właściwych instrukcjom procesora (Opcode i Operands)
awk '{print $4, $5, $6, $7}' orig.asm > orig_pure.asm
awk '{print $4, $5, $6, $7}' asm.asm > asm_pure.asm

# 6. Weryfikacja integralności struktury instrukcji za 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 uruchomieniu skryptu otrzymujemy następujące informacje.

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

### Wnioski

Języki programowania oferują wiele abstrakcji, ale za nimi kryją się bardzo ciekawe i agresywne optymalizacje. Co więcej, wykorzystując tę zależność w odwrotny sposób, można stworzyć lustrzane kody, które mają różne źródła, ale identyczny assembly. Jeśli interesujesz się niskim poziomem i natrafisz na oprogramowanie własnościowe napisane w Go, dekompilacja assembly i odtworzenie źródła nie wydaje się czymś niemożliwym.

## Następny wykład

Na następnym wykładzie omówimy instrukcję warunkową If oraz równie ciekawą instrukcję `select-case`.
