---
id: 04e03e046bb46ab34aaeeed79279923b
author: Lee Yunjin
title: Go-ehtolauseiden (if) AMD64-assemblykoodin generointi ja haaraohjeiden analyysi
description: Tämä vertailee, miten Go-kääntäjä muuntaa if-else-ehtolauseen AMD64-konekielen haarautumiskäskyiksi, kuten CMPQ, JGE ja JMP, ja asettaa tuloksen rinnalle C-kääntäjän (-O2) tuottaman haarautumiskoodin.
language: fi
date: 2026-05-17T12:00:00Z
path: /blog/posts/go-if-statement-analysis-3012d30c
category: lowlevel
---

## Go-kielen If-lause

Ensinnäkin, syy siihen, että valitsimme Go:n, on se, että Go on moderneista kielistä se, jonka assembly on "kauneinta", ja verrattuna klassisiin kieliin sen syntaksin tehokkuus on joskus jopa ylivoimaista.

Nyt, kun ymmärsimme edellisessä luennossa yksinkertaisen Go-ohjelman toimintatavan, vertaillaan Go:ta ja Assemblyä suoraan rivi riviltä.

### Lähdekoodi

Ensinnäkin, Go:ssa, kuten myös GCC:n mukaan lukien modernit kääntäjät, optimoidaan automaattisesti haarautumat, joilla ei ole käyttöä. GCC:n ja Clangin kaltaiset C-kielen kääntäjät tekevät teollisuusstandardin -O2-asetuksella myös hyvin aggressiivisia optimointeja, joten aika, jolloin ohjelmoija ei voinut täysin luottaa kääntäjään, päättyi lopullisesti jo 1900-luvun loppupuolella.

Siksi kääntäjän näkökulmasta ehdon on oltava sellainen, jota kääntäjän on vaikea ennustaa ja korvata toisella rakenteella, jotta sillä on edes merkitystä.

```go
package main

import (
    "os"
    "strconv"
)

func main() {
    // Jos tämä korvataan ennustettavalla ja haarautuman poistavalla lausekkeella, kuten x = 10,
    // kääntäjä optimoi sen ja poistaa haarautuman.
    // Siksi, jos tätä halutaan tarkastella suoraan assemblynä, C-kielessä käytetään esimerkiksi -O0:aa
    // tai annetaan kääntäjän käyttää ennustamatonta ulkoista arvoa,
    // mutta koska tämä osio käsittelee modernia ohjelmointia,
    // Go:n binäärioptimointien poistamista ei käytetä.
    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")
    }
}
```

Tässä tapauksessa kääntäjä ei voi ennustaa syötettä, joten haarautumalause käännetään konekielelle sellaisenaan.

### Assembly

```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 -työkalun avulla saadaan mukavasti selville, mikä lause vastaa mitäkin assembly-koodia.

Käsiteltävänä ovat vertailu ja if-haarautumalause, joten riittää, että tarkastellaan muutamia rivejä.

### CMPQ-käsky & JL-käsky

`CMPQ`-käsky on käsky, jolla vertaillaan 4 tavun (4 sanan) tietotyyppejä, ja sen nimi tulee sanoista **C**o**MP**are**Q**uadword, joka on lyhennetty muotoon **CMPQ**.

Muistiosoitteessa `0x47a84e` on lause `CMPQ os.Args+8(SB), $0x2`.
Tässä tapauksessa verrataan ohjelman vastaanottamien argumenttien määrää heksadesimaalilukuun `0x2` (eli käytännössä lukuun 2).

Tämän jälkeen, jos argumentteja on alle kaksi (eli ohjelma on ainoa argumentti), suoritetaan ehdollinen hyppy `JL`-käskyllä. Eli tämä on **J**ump, **L**ess than' lyhennettynä **JL**.
Edellisen vertailun perusteella, jos argumentteja oli alle kaksi, hypätään osoitteeseen `0x47a8b0`, jossa on `JGE`.
Tässä käytetään kuitenkin `AX`-rekisteriä, joten on tiedettävä, mikä arvo rekisterissä on.

### MOVQ-käsky

Tämän jälkeen on ymmärrettävä, miten `CX`-rekisteriä käytetään varsinaisesti datan aloitusosoitteen tallentamiseen ja miten varsinainen data luetaan osoitteesta.

Alueella `0x47858`-`0x47863` tämä toimenpide suoritetaan vaiheittain.

Ensin argumenttitaulukon aloitusosoite lisätään CX-rekisteriin `MOVQ os.Args(SB), CX`-käskyllä. Tässä on ymmärrettävä Go:n merkkijonotyyppi.

Go:n `string` on rakenne, ja tämä rakenne koostuu kahdesta 8 tavun tietoalkiosta, eli yhteensä 16 tavusta.

| struct | 8 tavua     | 8 tavua            |
| ------ | ----------- | ------------------ |
| string | muistiosoite | merkkijonon pituus |

Visuaalisesti se näyttää yllä olevalta: ensimmäiset 8 tavua sisältävät merkkijonon aloitusosoitteen, ja seuraavat 8 tavua merkkijonon pituuden.

Siksi merkkijonon osoite tallennetaan AX-rekisteriin ja merkkijonon pituus BX-rekisteriin.

### CALL

Edellisessä kirjoituksessa, kun tarkasteltiin `runtime`-funktioita, niiden edessä oli CALL-käsky.
Tämä esiintyy Go:ssa käytettävien funktioiden edessä, ja se tarkoittaa sanatarkasti, että jokin funktio **kutsutaan**, eli CALL. Tämän jälkeen CALL-funktiolla muutetaan merkkijono kokonaisluvuksi, mutta sitä, mihin kokonaisluku tallennetaan, ei näy, sillä se on abstrahoitu funktion sisälle.

### CMPQ-käsky & JGE-käsky

Palataan takaisin edelliseen osoitteeseen `0x47a86c`: käsky vertaa **merkkijonon osoitetta lukuun `0xa` (kymmenen kymmenjärjestelmässä)!**

Tämä tarkoittaa, että koska ohjelma ei enää käytä kyseistä argumenttia, **se on ylikirjoitettu merkkijonon paikalle, jolloin kokonaislukumuuttujan `x` paikka on luotu.**

Tämä on Go-kielessä ja muissa vastaavissa kielissä tehtävän aggressiivisen optimoinnin todellinen luonne.

Sen jälkeen esiintyy JGE-käsky, joka on lyhennys sanoista **J**ump, **G**reater or **E**quals. Tämä lause kysyy siis, onko verrattava arvo suurempi tai yhtä suuri kuin vertailukohde.

Näin ollen lause ei ole suoraan `x < 10`, vaan **vertailun suunta on käännetty päinvastaiseksi verrattuna `x < 10`:een!**
Konekielessä tämä on intuitiivisempaa ja säästää yhden käskyn, koska ehdon ollessa epätosi hyppääminen ohi tapahtuu ennakoivasti. Sen sijaan, että ehdon ollessa tosi suoritettaisiin vertailu kerran ja tarkistettaisiin uudelleen, onko se epätosi, hyppy tehdään heti.

Tämä optimointi on hyvin klassinen, ja kuten yllä nähdyssä `strconv.Atoi`-esimerkissä, sitä esiintyy usein myös kääntäjissä, joiden optimointi on huomattavasti vaatimattomampaa, joten sen tunteminen on hyödyllistä.

Tätä periaatetta soveltamalla voidaan saada lähdekoodi, joka on erilainen, mutta joka tuottaa assemblyssä 100 % saman tuloksen.

### Peilikuvakoodiesimerkki

Alla olevan skriptin avulla Bash-skripti voi luoda kaksi täysin peilikuvaa lähdekoodista (100 %) ja varmistaa, että kun tarkastellaan pelkästään `main`-funktiota ja jätetään pois vaihtelevat metatiedot, saadaan täsmälleen sama assembly.

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

# 1. Tyhjennä täysin vanhat jäännöstiedostot ja hakemistot
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. Kirjoita alkuperäisen version lähdekoodi (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. Kirjoita peilikuvaversion lähdekoodi (main_from_asm.go)
# Synkronoi operaattorien rakenne täydellisesti symmetriseksi, jotta kääntäjä käyttää
# optimointimallia (JGE) sellaisenaan
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"

        // Käyttämällä vertailukohtana lukua 10 ja säilyttämällä rakenteen x < 10 kääntäjä ottaa
        // käyttöön täsmälleen saman JGE-mekanismin ja lohkojärjestyksen kuin main.go:ssa.
        switch {
        case x < 10:
                println(s1)
        default:
                println(s2)
        }
}
EOF

# 4. Käännä molemmat lähdekoodit samassa hakemisto- ja tiedostonimiympäristössä
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. Pura pelkkä main.main-assembly-funktio go tool objdumpilla
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

# Poista virtuaaliset osoitteet, offsetit ja konekielen tavudatateksti
# ja suodata vain suoritettavien käskyjen (Opcode & Operands) kentät
awk '{print $4, $5, $6, $7}' orig.asm > orig_pure.asm
awk '{print $4, $5, $6, $7}' asm.asm > asm_pure.asm

# 6. Varmista kahden konekielikäskyrakenteen eheys diff-vertailulla
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 "------------------------------------------------------------"
```

Kun ajat lähdekoodit, saat seuraavanlaisen tulosteen.

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

### Johtopäätös

Ohjelmointikielet tarjoavat paljon abstraktiota, mutta abstraktion takana piilee mielenkiintoisia ja aggressiivisia optimointeja. Tätä periaatetta voi myös hyödyntää käänteisesti: luoda lähdekoodeja, jotka ovat erilaisia mutta joilla on sama assembly, eli peilikuvakoodia. Jos olet kiinnostunut matalan tason ohjelmoinnista ja kohtaat Go:lla kirjoitettua suljettua ohjelmistoa, assemblyn purkaminen ja analysointi lähdekoodin palauttamiseksi ei välttämättä ole mahdotonta.

## Seuraava luento

Seuraavalla kerralla tutustumme If-lauseeseen ja toiseen mielenkiintoiseen rakenteeseen, `select-case`-lauseeseen.
