If-statements in de Go-taal
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.
| struct | 8 byte | 8 byte |
|---|---|---|
| string | mem address | string 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.