---
id: 04e03e046bb46ab34aaeeed79279923b
author: Lee Yunjin
title: Analyse av AMD64-assemblykodegenerering og forgreningsinstruksjoner for Go-betingelsessetninger (if)
description: Go-kompilatoren oversetter if-else-betingelser til AMD64-maskinkodegreninstruksjoner som CMPQ, JGE og JMP, og denne prosessen sammenlignes med resultatet av grenkodegenereringen fra C-kompilatoren (-O2).
language: "no"
date: 2026-05-17T12:00:00Z
path: /blog/posts/go-if-statement-analysis-3012d30c
category: lowlevel
---

## If-setningen i Go

Først er grunnen til at vi valgte Go at Go, blant moderne språk, har den mest «vakre» assemblyen, og at effektiviteten i syntaksen til tider faktisk er overveldende, selv sammenlignet med klassiske språk.

Nå som vi forsto hvordan et enkelt Go-program fungerer i forrige forelesning, skal vi sammenligne Go og Assembly linje for linje.

### Kildekode

For det første optimaliserer moderne kompilatorer, inkludert de i Go og til og med GCC, automatisk grener som ikke har noen nytte. GCC og Clang, som er C-kompilatorer, utfører svært aggressiv optimalisering under bransjestandarden -O2, så tiden da programmerere ikke fullt ut kunne stole på kompilatoren, er endelig blitt en saga blott fra slutten av 1900-tallet.

Derfor får en betingelse først mening i det minste når den er vanskelig for kompilatoren å forutsi og erstatte med en annen konstruksjon.

```go
package main

import (
    "os"
    "strconv"
)

func main() {
    // Hvis man erstatter dette med noe forutsigbart og som kan fjerne grenen, slik som x = 10,
    // optimaliserer kompilatoren det og fjerner grenen.
    // For å se slikt direkte i assembly i C må man derfor bruke f.eks. -O0,
    // eller la kompilatoren bruke en ytre verdi som den ikke kan forutsi,
    // men siden denne delen handler om moderne programmering, brukes ikke
    // metoden der man slår av Go sin binæroptimalisering.
    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")
    }
}
```

I dette tilfellet kan ikke kompilatoren forutsi inndataen, så grenen oversettes uendret til maskinkode.

### Assemblyspråk

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

Ved hjelp av go tool får man den hjelpsomme informasjonen om hvilken syntaks som tilsvarer hvilken assembly, linje for linje.

Siden vi denne gangen skal lære om sammenligninger og if-grener, er det nok å se på noen få linjer.

### CMPQ-instruksjonen og JL-instruksjonen

`CMPQ` er en instruksjon for å sammenligne datatyper på 4 byte (4 ord), og navnet kommer fra **C**o**MP**are**Q**uadword, forkortet til **CMPQ**.

Hvis man ser på minneadressen `0x47a84e`, finnes setningen `CMPQ os.Args+8(SB), $0x2`.
I dette tilfellet sammenligner programmet antall argumenter det har mottatt med heksadesimalt `0x2` (altså bare 2).

Deretter sjekkes det om antall argumenter er mindre enn 2 (altså om programmet selv er det eneste argumentet), og et hopp utføres via `JL` etter sammenligningen. Altså står dette for **J**ump, **L**ess than', forkortet til **JL**.
Hvis argumentantallet var mindre enn 2 i den foregående sammenligningen, hoppes det til adressen `0x47a8b0`, og der finnes `JGE`.
Men siden denne setningen bruker registeret `AX`, må man vite hva verdien lagret i registeret er.

### MOVQ-instruksjonen

Deretter må man forstå hvordan registeret 'CX' brukes til å lagre startadressen til dataene, og hvordan de faktiske dataene hentes ut etter at adressen er lest.

Når man ser på området `0x47858`-`0x47863`, utføres denne operasjonen trinn for trinn.

Først settes startadressen til argumentarrayen inn i registeret CX med instruksjonen `MOVQ os.Args(SB), CX`. Her må man forstå Gos strengtype.

En `string` i Go er en struct, og denne structen består av to datatyper på 8 byte, altså totalt 16 byte.

| struct | 8 byte      | 8 byte        |
| ------ | ----------- | ------------- |
| string | minneadresse | strenglengde |

Visuelt kan dette fremstilles som over: de første 8 byte inneholder strengens startadresse, og de siste 8 byte inneholder strengens lengde.

Derfor lagres strengens adresse i registeret AX, og strengens lengde i registeret BX.

### CALL

Også i forrige innlegg, når vi så på funksjonene i `runtime`-pakken, var instruksjonen CALL knyttet til dem.
Den står foran funksjonene som brukes i Go, og betyr bokstavelig talt at en bestemt funksjon **kalles**, altså CALL.
Deretter brukes en CALL til å konvertere strengen til et heltall, men hvor heltallet lagres, er _skjult i funksjonen og er ikke synlig._

### CMPQ-instruksjonen og JGE-instruksjonen

Hvis vi går tilbake til adressen `0x47a86c` fra før, sammenligner instruksjonen **strengens adresse med tallet `0xa` (10 i desimal)!**

Det betyr at programmet ikke lenger bruker det aktuelle argumentet, og derfor **har overskrevet strengens plass for å lage plass til den heltallsvariable `x`.**

Dette er essensen av den aggressive optimaliseringen som utføres i blant annet Go.

Deretter dukker instruksjonen JGE opp, som er forkortelsen for **J**ump, **G**reater or **E**quals. Derfor spør denne setningen om verdien er større enn eller lik sammenligningsverdien.

Derfor er det ikke slik at setningen er `x < 10` akkurat som den står, men med `x < 10` er **sammenligningsretningen i setningen snudd!**
Dette er fordi det i maskinkode er mer intuitivt å hoppe over på forhånd når betingelsen ikke stemmer, enn å utføre en sammenligning når betingelsen stemmer og deretter sjekke om den ikke stemmer, og det **sparer én instruksjon**.

Denne optimaliseringen er svært klassisk, og i motsetning til `strconv.Atoi`-eksemplet ovenfor er det et mønster som ofte dukker opp selv i kompilatorer med betydelig lavere optimaliseringsnivå, så det er nyttig å kjenne til.

Ved å utnytte dette kan man derfor få kildekode som er forskjellig, men som gir 100% identisk assembly.

### Speilkode-eksempel

Ved hjelp av skriptet nedenfor kan man verifisere at bash-skriptet lager to 100% speilvendte kildefiler, og at man, når man kun ser på `main` og ser bort fra metadata som varierer fra gang til gang, får nøyaktig samme assembly.

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

# 1. Fullstendig tilbakestilling av eksisterende rester av filer og kataloger
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. Skriv kildekoden for originalversjonen (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. Skriv kildekoden for speilversjonen (main_from_asm.go)
# Synkroniser operatorstrukturen fullstendig symmetrisk, slik at kompilatoren bruker optimaliseringsmalen (JGE) uendret
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"

        // 10를 기준으로 삼아 x < 10 구조를 유지하면 컴파일러는
        // main.go와 정확히 동일한 JGE 매커니즘 및 블록 배치를 채택합니다.
        switch {
        case x < 10:
                println(s1)
        default:
                println(s2)
        }
}
EOF

# 4. Bygg hver av dem i samme katalogbane og med samme filnavn
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. Trekk ut den rene assembly-funksjonen main.main ved hjelp av 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

# Fjern den virtuelle adressen, offsetten og teksten med maskinkodebytes,
# og filtrer ut kun feltene for den rene instruksjonssettet (Opcode & Operands) som CPU-en skal utføre
awk '{print $4, $5, $6, $7}' orig.asm > orig_pure.asm
awk '{print $4, $5, $6, $7}' asm.asm > asm_pure.asm

# 6. Verifiser strukturen til maskinkodeinstruksjonene med 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 "------------------------------------------------------------"
```

Når man faktisk kjører kildekoden, får man informasjonen nedenfor.

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

### Konklusjon

Programmeringsspråk tilbyr mange abstraksjoner, men bak abstraksjonene skjuler det seg svært interessante og aggressive optimaliseringer. Ved å utnytte dette kunne vi også lage speilvendt kode, der kildekoden er forskjellig, men assemblyen er identisk. Hvis man er interessert i lavnivå og støter på proprietær programvare skrevet i Go, er det kanskje ikke umulig å gjenopprette kildekoden ved å dekompilere og analysere assemblyen direkte.

## Neste forelesning

Neste gang skal vi se nærmere på if-setningen og på `select-case`-setningen, som også er interessant.
