GoSuda

If-statements in de Go-taal

By Lee Yunjin
views ...

If-statements in Go

Ten eerste hebben we voor Go gekozen omdat Go, onder de moderne talen, de meest 'esthetische' assembly genereert en de efficiëntie van de syntaxis in vergelijking met klassieke talen soms zelfs overweldigend superieur is.

Nu we in de vorige les de basiswerking van een eenvoudig Go-programma hebben begrepen, gaan we direct over tot het regel-voor-regel vergelijken van Go en Assembly.

Broncode

Allereerst, zoals in Go het geval is, optimaliseren zelfs moderne compilers (inclusief GCC) automatisch vertakkingsinstructies (branch statements) die geen nut hebben. C-compilers zoals GCC en Clang voeren in de industriestandaard -O2 ook zeer agressieve optimalisaties uit, waardoor het tijdperk waarin een programmeur de compiler volledig kon vertrouwen, sinds de late 20e eeuw definitief voltooid is.

Daarom heeft het alleen zin om voorwaarden te stellen die voor de compiler moeilijk te voorspellen zijn en die hij lastig kan omzetten naar andere structuren.

 1package main
 2
 3import (
 4    "os"
 5    "strconv"
 6)
 7
 8func main() {
 9    // Als we dit invullen met iets voorspelbaars zoals x = 10,
10    // waarbij de vertakking verwijderd kan worden, zal de compiler 
11    // deze optimaliseren en de vertakking verwijderen.
12    // Om dit direct in assembly te kunnen observeren, hanteert men in C
13    // vaak -O0, of men gebruikt externe waarden die onvoorspelbaar zijn 
14    // voor de compiler. Aangezien deze sectie moderne programmering behandelt,
15    // gebruiken we geen methode om de binaire optimalisatie van Go uit te schakelen.
16    if len(os.Args) < 2 {
17        return
18    }
19    x, _ := strconv.Atoi(os.Args[1])
20
21    if x < 10 {
22        println("X is smaller than 10")
23    } else {
24        println("X is larger or same as 10")
25    }
26}

In dit geval wordt de vertakkingsinstructie letterlijk vertaald naar machinetaal, omdat de compiler de invoer niet kan voorspellen.

Assembly-taal

 1TEXT main.main(SB) /home/yjlee/introduction-to-golang/learn-golang/if-and-switch/golang-if/main.go
 2  main.go:8             0x47a840                493b6610                CMPQ SP, 0x10(R14)
 3  main.go:8             0x47a844                7670                    JBE 0x47a8b6
 4  main.go:8             0x47a846                55                      PUSHQ BP
 5  main.go:8             0x47a847                4889e5                  MOVQ SP, BP
 6  main.go:8             0x47a84a                4883ec10                SUBQ $0x10, SP
 7  main.go:15            0x47a84e                48833d12fb0a0002        CMPQ os.Args+8(SB), $0x2
 8  main.go:15            0x47a856                7c58                    JL 0x47a8b0
 9  main.go:15            0x47a858                488b0d01fb0a00          MOVQ os.Args(SB), CX
10  main.go:18            0x47a85f                488b4110                MOVQ 0x10(CX), AX
11  main.go:18            0x47a863                488b5918                MOVQ 0x18(CX), BX
12  main.go:18            0x47a867                e834e8ffff              CALL strconv.Atoi(SB)
13  main.go:20            0x47a86c                4883f80a                CMPQ AX, $0xa
14  main.go:20            0x47a870                7d1d                    JGE 0x47a88f
15  main.go:21            0x47a872                e809befbff              CALL runtime.printlock(SB)
16  main.go:21            0x47a877                488d0519f50100          LEAQ 0x1f519(IP), AX
17  main.go:21            0x47a87e                bb15000000              MOVL $0x15, BX
18  main.go:21            0x47a883                e878c6fbff              CALL runtime.printstring(SB)
19  main.go:21            0x47a888                e853befbff              CALL runtime.printunlock(SB)
20  main.go:21            0x47a88d                eb1b                    JMP 0x47a8aa
21  main.go:23            0x47a88f                e8ecbdfbff              CALL runtime.printlock(SB)
22  main.go:23            0x47a894                488d05c8040200          LEAQ 0x204c8(IP), AX
23  main.go:23            0x47a89b                bb1a000000              MOVL $0x1a, BX
24  main.go:23            0x47a8a0                e85bc6fbff              CALL runtime.printstring(SB)
25  main.go:23            0x47a8a5                e836befbff              CALL runtime.printunlock(SB)
26  main.go:25            0x47a8aa                4883c410                ADDQ $0x10, SP
27  main.go:25            0x47a8ae                5d                      POPQ BP
28  main.go:25            0x47a8af                c3                      RET
29  main.go:16            0x47a8b0                4883c410                ADDQ $0x10, SP
30  main.go:16            0x47a8b4                5d                      POPQ BP
31  main.go:16            0x47a8b5                c3                      RET
32  main.go:8             0x47a8b6                e845f0feff              CALL runtime.morestack_noctxt.abi0(SB)
33  main.go:8             0x47a8bb                eb83                    JMP main.main(SB)

Door de go tool te gebruiken, wordt vriendelijk aangegeven welke syntaxis correspondeert met welke assembly-instructie.

Omdat we in deze les vergelijkingen en if-vertakkingen bestuderen, moeten we op een paar regels letten.

CMPQ-instructie & JL-instructie

De CMPQ-instructie wordt gebruikt voor het vergelijken van 4-byte (4-word) datatypen; de naam is afgeleid van CoMPare Quadword, afgekort tot CMPQ.

Kijkend naar het geheugenadres 0x47a84e, zien we de instructie CMPQ os.Args+8(SB), $0x2. In dit geval vergelijkt het programma het aantal ontvangen argumenten met het hexadecimale getal 0x2 (oftewel 2).

Vervolgens wordt via JL een vergelijking uitgevoerd om te zien of het aantal argumenten kleiner is dan 2 (oftewel als alleen het programma zelf wordt uitgevoerd). Dit staat voor Jump Less than. Als het argument kleiner is dan 2, springt het naar het adres 0x47a8b0, waar een JGE-instructie staat. Echter, omdat deze instructie het AX-register gebruikt, moeten we weten wat de aard is van de opgeslagen waarde in dit register.

MOVQ-instructie

Daarna moeten we begrijpen hoe de data wordt geëxtraheerd nadat de beginlocatie van de data is opgeslagen met behulp van het 'CX'-register.

In het bereik 0x47858-0x47863 wordt deze bewerking stapsgewijs uitgevoerd.

Eerst wordt het beginadres van de argumentenlijst in het CX-register geplaatst met de instructie MOVQ os.Args(SB), CX. Hierbij is het noodzakelijk om het string-type van Go te begrijpen.

Een string in Go is een structuur die bestaat uit 16 bytes, opgebouwd uit twee 8-byte datavelden.

struct8 byte8 byte
stringmem addressstring length

Visueel weergegeven ziet dit er als volgt uit: de eerste 8 bytes slaan het startadres van de string op, en de laatste 8 bytes slaan de lengte van de string op.

Daarom wordt het adres van de string in het AX-register opgeslagen en de lengte van de string in het BX-register.

CALL

In voorgaande berichten, bij het bekijken van de runtime-functies, zagen we de instructie CALL. Deze staat voor functies die in Go worden gebruikt en betekent letterlijk het aanroepen van een functie. Daarna wordt de functie CALL gebruikt om de string naar een integer te converteren, waarbij het niet geabstraheerd in de functie zelf zichtbaar is waar de integer wordt opgeslagen.

CMPQ-instructie & JGE-instructie

Terugkerend naar het adres 0x47a86c, vergelijkt de instructie het adres van de string met het getal 0xa (10 in decimaal)!

Dit betekent dat, aangezien het programma het argument niet meer gebruikt, de locatie van de string is overschreven om een plek te maken voor de integer-variabele x.

Dit is de essentie van de agressieve optimalisatie die plaatsvindt in talen als Go.

Vervolgens verschijnt de instructie JGE, wat een afkorting is voor Jump Greater or Equals. Deze instructie vraagt dus of de waarde groter of gelijk is aan het vergelijkingsobject.

De instructie is dus niet x < 10 zoals in de broncode, maar de vergelijkingsrichting is omgekeerd naar x >= 10! Dit komt omdat het in machinetaal efficiënter is om voorwaardelijk over te slaan als de voorwaarde niet waar is, dan om na een vergelijking opnieuw te controleren of de voorwaarde niet waar is; het is intuïtiever en bespaart één instructie.

Dergelijke optimalisaties zijn zeer klassiek en vormen een patroon dat vaak voorkomt, zelfs bij compilers met een relatief laag optimalisatieniveau, in tegenstelling tot het strconv.Atoi-voorbeeld hierboven.

Door dit principe toe te passen, is het mogelijk om broncode te verkrijgen die, hoewel de broncode zelf verschilt, op assembly-niveau 100% overeenkomt.

Voorbeeld van spiegelbeeldcode

Met het onderstaande script kan worden geverifieerd dat het Bash-script twee broncodes creëert die 100% elkaars spiegelbeeld zijn, waarbij, afgezien van de veranderlijke metadata, exact dezelfde assembly wordt verkregen wanneer men enkel naar main kijkt.

 1#!/usr/bin/env bash
 2
 3# 1. Volledige initialisatie van bestaande restbestanden en mappen
 4echo "[1/6] Cleaning up old artifacts..."
 5rm -rf test_dir main_orig main_asm orig.asm asm.asm orig_pure.asm asm_pure.asm
 6mkdir -p test_dir
 7
 8# 2. Schrijven van de originele versie broncode (main.go)
 9echo "[2/6] Generating main.go..."
10cat << 'EOF' > main.go
11package main
12
13import (
14        "os"
15        "strconv"
16)
17
18func main() {
19        if len(os.Args) < 2 {
20                return
21        }
22        x, _ := strconv.Atoi(os.Args[1])
23
24        s1 := "X is smaller than 10"
25        s2 := "X is larger or same as 10"
26
27        if x < 10 {
28                println(s1)
29        } else {
30                println(s2)
31        }
32}
33EOF
34
35# 3. Schrijven van de spiegelbeeldversie broncode (main_from_asm.go)
36# De operatorstructuur is perfect symmetrisch gesynchroniseerd zodat de compiler de optimalisatiesjabloon (JGE) ongewijzigd overneemt
37echo "[3/6] Generating main_from_asm.go..."
38cat << 'EOF' > main_from_asm.go
39package main
40
41import (
42        "os"
43        "strconv"
44)
45
46func main() {
47        if len(os.Args) < 2 {
48                return
49        }
50        x, _ := strconv.Atoi(os.Args[1])
51
52        s1 := "X is smaller than 10"
53        s2 := "X is larger or same as 10"
54
55        // Door 10 als referentie te nemen en de x < 10 structuur te behouden, 
56        // adopteert de compiler exact hetzelfde JGE-mechanisme en blokindeling als in main.go.
57        if x < 10 {
58                println(s1)
59        } else {
60                println(s2)
61        }
62}
63EOF
64
65# 4. Uitvoeren van de build in dezelfde map en bestandsnaam
66echo "[4/6] Compiling both sources inside 'test_dir'..."
67cp main.go test_dir/main.go
68cd test_dir && go build -o ../main_orig main.go && cd ..
69
70rm test_dir/main.go
71cp main_from_asm.go test_dir/main.go
72cd test_dir && go build -o ../main_asm main.go && cd ..
73
74# 5. Extractie van de pure main.main assembly-functie met go tool objdump
75echo "[5/6] Extracting main.main assembly sections..."
76go tool objdump -s "main\.main" main_orig > orig.asm
77go tool objdump -s "main\.main" main_asm > asm.asm
78
79# Verwijderen van virtuele adressen, offsets en machinetaal-byte-data 
80# en filteren van alleen de pure instructieset (Opcode & Operands) voor de CPU
81awk '{print $4, $5, $6, $7}' orig.asm > orig_pure.asm
82awk '{print $4, $5, $6, $7}' asm.asm > asm_pure.asm
83
84# 6. Verificatie van de diff tussen de twee machinetaalstructuren
85echo "[6/6] Verifying assembly structural integrity via diff..."
86echo "------------------------------------------------------------"
87
88if diff orig_pure.asm asm_pure.asm > /dev/null; then
89    echo "===> [Success] De main.main machinetaallogica van beide binaries is 100% identiek! <==="
90    echo "Door de optimalisatie-pipeline-richtlijnen van de compiler perfect te synchroniseren, is dezelfde assembly verkregen."
91else
92    echo "===> [Failure] Er zijn verschillen gevonden in de assembly-instructiestructuur. <==="
93    diff -u orig_pure.asm asm_pure.asm
94fi
95echo "------------------------------------------------------------"

Bij het uitvoeren van de broncode kunt u de volgende informatie verkrijgen:

 1[1/6] Cleaning up old artifacts...
 2[2/6] Generating main.go...
 3[3/6] Generating main_from_asm.go...
 4[4/6] Compiling both sources inside 'test_dir'...
 5[5/6] Extracting main.main assembly sections...
 6[6/6] Verifying assembly structural integrity via diff...
 7------------------------------------------------------------
 8===> [Success] De main.main machinetaallogica van beide binaries is 100% identiek! <===
 9Door de optimalisatie-pipeline-richtlijnen van de compiler perfect te synchroniseren, is dezelfde assembly verkregen.
10------------------------------------------------------------

Conclusie

We hebben kunnen vaststellen dat programmeertalen veel abstractie bieden, maar dat er achter deze abstractie zeer interessante en agressieve optimalisaties verborgen liggen. Bovendien konden we dit in ons voordeel gebruiken om spiegelbeeldcode te creëren die, hoewel de broncode verschilt, identiek is op assembly-niveau. Als u geïnteresseerd bent in low-level zaken en u komt propriëtaire software in Go tegen, dan lijkt het niet onmogelijk om de broncode te herstellen door de assembly te decompileren en te analyseren.

Volgende les

In de volgende les zullen we kijken naar de select-case-instructie, die net zo interessant is als de if-instructie.