---
id: 04e03e046bb46ab34aaeeed79279923b
author: Lee Yunjin
title: Go-villkorssatsen (if) i AMD64-assemblerkodgenerering och analys av greninstruktioner
description: Den här artikeln jämför processen genom vilken Go-kompilatorn översätter if-else-villkorssatser till AMD64-maskinkodens greninstruktioner, såsom CMPQ, JGE och JMP, med resultatet av grenkodsgenereringen från C-kompilatorn (-O2).
language: sv
date: 2026-05-17T12:00:00Z
path: /blog/posts/go-if-statement-analysis-3012d30c
category: lowlevel
---

## If-satser i Go

Först och främst har vi valt Go eftersom Go, bland moderna språk, har de mest "vackra" assemblerkoderna, och dess syntax är ibland till och med överlägsen klassiska språk när det gäller effektivitet.

Nu när du förstår hur ett enkelt Go-program fungerar från föregående lektion, låt oss jämföra Go och Assembly rad för rad.

### Källkod

Först av allt, inte bara i Go utan även i moderna kompilatorer, inklusive GCC, optimeras grenar som saknar betydelse automatiskt. GCC och Clang, C-kompilatorer, utför mycket aggressiva optimeringar vid -O2, som är industristandarden, så tiden då programmerare har svårt att fullt ut lita på kompilatorn fullbordades i själva verket redan under den senare delen av 1900-talet.

Därför måste villkoret ges på ett sätt som kompilatorn inte lätt kan förutsäga och ersätta med en annan syntax, annars har det ingen verklig betydelse.

```go
package main

import (
    "os"
    "strconv"
)

func main() {
    // Om man här använder något förutsägbart, till exempel x = 10, som kan tas bort från grenen,
    // så optimerar kompilatorn bort grenen.
    // För att se assemblerkoden direkt i C används därför till exempel -O0,
    // eller så låter man kompilatorn använda ett externt värde som den inte kan förutsäga.
    // Eftersom detta avsnitt handlar om modern programmering använder vi inte
    // metoden att stänga av Gos binäroptimeringar.
    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 det här fallet kan kompilatorn inte förutsäga indata, så if-satsen översätts till maskinkod precis som den är.

### Assemblerkod

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

Verktyget go tool visar mycket vänligt vilken syntax som motsvarar vilken assemblerkod, rad för rad.

Eftersom det vi ska lära oss nu är jämförelsesatser och if-grenar behöver vi bara uppmärksamma några rader.

### CMPQ-instruktionen och JL-instruktionen

`CMPQ`-instruktionen är en instruktion för att jämföra 4-byte (4-word) datatyper, och dess ursprung är **C**o**MP**are**Q**uadword, förkortat till **CMPQ**.

Vid minnesadressen `0x47a84e` finns satsen `CMPQ os.Args+8(SB), $0x2`.
I det här fallet jämför programmet antalet argument som tagits emot med det hexadecimala talet `0x2` (det vill säga bara 2).

Därefter utför `JL` ett hopp baserat på jämförelsen, om antalet argument är mindre än 2 (det vill säga om programmet endast har sig självt som argument). `JL` är förkortningen av **J**ump, **L**ess than.
Om antalet argument var mindre än 2 hoppar programmet till adressen `0x47a8b0`, där det finns en `JGE`. Men den instruktion som används här är registret `AX`, så vi måste känna till vilket värde som är lagrat i registret.

### MOVQ-instruktionen

Därefter måste vi förstå hur startadressen för datat lagras i registret `CX`, hur data läses från den adressen och hur det faktiska datat extraheras.

Intervallet `0x47858`-`0x47863` utför denna operation steg för steg.

Först placeras startadressen för argumentarrayen i registret CX med instruktionen `MOVQ os.Args(SB), CX`. Här måste vi förstå hur Gos strängtyp fungerar.

En `string` i Go är en struktur, och denna struktur består av två 8-byte-datavärden, totalt 16 byte.

| struct | 8 byte      | 8 byte        |
| ------ | ----------- | ------------- |
| string | mem address | string length |

Visuellt ser det ut som ovan: de första 8 byten innehåller strängens startadress och de andra 8 byten innehåller strängens längd.

Därför lagras strängens adress i registret AX och strängens längd i registret BX.

### CALL

I det föregående inlägget, när vi tittade på `runtime`-funktioner, fanns instruktionen CALL framför dem.
Den används före funktioner i Go, och betyder att en viss funktion **anropas**, vilket är precis vad CALL innebär. Därefter används en CALL-funktion för att omvandla strängen till ett heltal, och var heltalet lagras är **abstraherat bort i funktionen och syns inte.**

### CMPQ-instruktionen och JGE-instruktionen

Om vi återvänder till adressen `0x47a86c` från tidigare, jämför instruktionen **strängens adress med talet `0xa` (tio i decimal)!**

Detta innebär att eftersom argumentet inte längre används i programmet, **skrev man över strängens position för att skapa en plats för den heltalsvariabla `x`.**

Detta är den verkliga bilden av den aggressiva optimering som sker i språk som Go.

Därefter kommer instruktionen JGE, som är förkortningen av **J**ump, **G**reater or **E**quals. Den här satsen frågar därför om värdet är större än eller lika med jämförelsevärdet.

Alltså är jämförelseriktningen omvänd jämfört med satsen `x < 10`, så den är inte exakt `x < 10`, utan **jämförelsens riktning är vänd!**
Detta beror på att det på maskinkodsnivå är mer intuitivt och sparar en instruktion att hoppa över förväg när villkoret inte stämmer, jämfört med att utföra jämförelsen en gång när villkoret stämmer och sedan kontrollera igen om det inte stämmer.

Eftersom denna optimering är mycket klassisk förekommer mönstret ofta även i kompilatorer med betydligt lägre optimeringsnivå än exemplet `strconv.Atoi` ovan, så det är bra att känna till.

Genom att tillämpa dessa principer kan man alltså få källkod som skiljer sig åt men ger identisk assemblerkod på 100 %.

### Exempel på spegelkod

Med hjälp av skriptet nedan kan man, med ett Bash-skript, skapa två källkodsvarianter som är 100 % spegelvända och sedan verifiera att exakt samma assemblerkod erhålls för `main`, med undantag för metadata som varierar från gång till gång.

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

# 1. Fullständig återställning av kvarvarande filer och 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 källkod för den ursprungliga versionen (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 källkod för spegelversionen (main_from_asm.go)
# Strukturen av operatorerna synkroniseras helt symmetriskt så att kompilatorn använder samma optimeringsmall (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"

        // Om man utgår från 10 och behåller strukturen x < 10 använder kompilatorn
        // exakt samma JGE-mekanism och blockplacering som i main.go.
        switch {
        case x < 10:
                println(s1)
        default:
                println(s2)
        }
}
EOF

# 4. Bygg båda källkoderna i samma katalog och med samma filnamn
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. Extrahera den rena assemblerfunktionen main.main med 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

# Ta bort virtuella adresser, offset och text med maskinkodsbyte
# och filtrera endast de rena instruktionsfälten (Opcode & Operands) som CPU:n kör
awk '{print $4, $5, $6, $7}' orig.asm > orig_pure.asm
awk '{print $4, $5, $6, $7}' asm.asm > asm_pure.asm

# 6. Verifiera strukturen hos de två maskinkodsinstruktionerna 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 du kör källkoden i praktiken kan du få följande information.

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

### Slutsats

Programmeringsspråk erbjuder många abstraktioner, men bakom abstraktionerna döljer sig mycket intressanta och aggressiva optimeringar. Genom att utnyttja detta kunde vi även skapa spegelbildskod som har olika källkod men identisk assemblerkod. Om du är intresserad av lågnivåprogrammering och stöter på proprietär mjukvara skriven i Go, verkar det inte vara omöjligt att bryta ner assemblerkoden och analysera den för att återskapa källkoden.

## Nästa lektion

I nästa lektion ska vi titta på `select-case`-satsen, som är ännu en rolig del, tillsammans med if-satsen.
