If-setninger i Go-språket
If-setninger i Go
For det første valgte vi Go fordi språket har den «vakreste» assembly-koden blant moderne språk, og fordi effektiviteten i syntaksen ofte er overlegen selv sammenlignet med klassiske programmeringsspråk.
Nå som vi i forrige forelesning har forstått hvordan et enkelt Go-program fungerer, skal vi umiddelbart sammenligne Go og assembly linje for linje.
Kildekode
For det første, som i Go, vil moderne kompilatorer – inkludert GCC – automatisk optimalisere bort forgreninger (branching) som ikke tjener noe formål. C-kompilatorer som GCC og Clang utfører svært aggressive optimaliseringer ved industristandarden -O2, så det har vært en realitet siden slutten av det 20. århundre at programmereren ikke lenger kan stole blindt på kompilatoren.
Derfor gir det bare mening å gi kompilatoren betingelser som er vanskelige å forutse eller endre til andre uttrykk.
1package main
2
3import (
4 "os"
5 "strconv"
6)
7
8func main() {
9 // Hvis vi erstatter dette med noe forutsigbart som x = 10,
10 // der forgreningen kan fjernes, vil kompilatoren optimalisere den bort.
11 // For å studere dette direkte i assembly, må man i C bruke flagg som -O0,
12 // eller bruke eksterne verdier som kompilatoren ikke kan forutse.
13 // Siden denne seksjonen tar for seg moderne programmering,
14 // benytter vi ikke metoder for å deaktivere binær optimalisering i Go.
15 if len(os.Args) < 2 {
16 return
17 }
18 x, _ := strconv.Atoi(os.Args[1])
19
20 if x < 10 {
21 println("X is smaller than 10")
22 } else {
23 println("X is larger or same as 10")
24 }
25}
I dette tilfellet oversettes forgreningen direkte til maskinkode fordi kompilatoren ikke kan forutse inndataene.
Assembly-kode
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)
Ved å bruke go tool får vi på en informativ måte vite nøyaktig hvilken syntaks som samsvarer med hvilken assembly-instruksjon.
Siden vi denne gangen skal lære om sammenligningsuttrykk og if-forgreninger, er det noen få linjer vi bør legge merke til.
CMPQ-instruksjon & JL-instruksjon
CMPQ-instruksjonen brukes til å sammenligne 4-byte (4-word) datatyper, og navnet er en forkortelse for CoMPare Quadword.
Hvis vi ser på minneadressen 0x47a84e, finner vi instruksjonen CMPQ os.Args+8(SB), $0x2.
I dette tilfellet sammenligner programmet antall mottatte argumenter med heksadesimaltallet 0x2 (som ganske enkelt er 2).
Deretter utføres et hopp via JL dersom argumentet er mindre enn 2 (det vil si hvis programmet kun har seg selv som argument). Dette er en forkortelse for Jump, Less than.
Ved den foregående sammenligningen, dersom argumentet var mindre enn 2, hoppes det til adressen 0x47a8b0, hvor vi finner en JGE-instruksjon.
Siden denne instruksjonen benytter AX-registeret, må vi forstå hva verdien som er lagret i registeret representerer.
MOVQ-instruksjon
Deretter må vi vite hvordan dataene hentes etter at adressen er lest, ved bruk av CX-registeret for å lagre startadressen til dataene.
Ser vi på området 0x47858-0x47863, utføres denne operasjonen trinnvis.
Først settes startadressen til argumenttabellen inn i CX-registeret med instruksjonen MOVQ os.Args(SB), CX. Her må man forstå Go sin string-type.
En string i Go er en struktur (struct) som består av to 8-byte verdier, totalt 16 bytes.
| struct | 8 byte | 8 byte |
|---|---|---|
| string | mem address | string length |
Visuelt sett er dette representert som over, hvor de første 8 bytene inneholder startadressen til strengen, og de siste 8 bytene inneholder strengens lengde.
Følgelig lagres adressen til strengen i AX-registeret, og lengden på strengen i BX-registeret.
CALL
I tidligere innlegg så vi at når vi ser på runtime-funksjoner, følger instruksjonen CALL med.
Dette prefikset brukes for funksjoner i Go, og betyr bokstavelig talt å kalle en funksjon. Senere brukes CALL-funksjonen til å konvertere strengen til et heltall, men hvor heltallet lagres, er abstrahert bort og ikke direkte synlig i funksjonen.
CMPQ-instruksjon & JGE-instruksjon
Går vi tilbake til adressen 0x47a86c, ser vi at instruksjonen sammenligner adressen til strengen med tallet 0xa (10 i desimaltall)!
Dette betyr at siden argumentet ikke lenger brukes i programmet, har kompilatoren overskrevet strengens plassering for å opprette variabelen x av heltallstype.
Dette er kjernen i de aggressive optimaliseringene som utføres i språk som Go.
Deretter dukker JGE-instruksjonen opp, som er en forkortelse for Jump, Greater or Equals. Derfor spør dette uttrykket om verdien er større enn eller lik sammenligningsgrunnlaget.
Dermed er ikke uttrykket x < 10 bevart direkte, men sammenligningsretningen i uttrykket er snudd til x < 10!
Dette skyldes at det i maskinkode er mer intuitivt og sparer én instruksjon å hoppe over koden når betingelsen ikke er oppfylt, fremfor å utføre sammenligningen og så sjekke på nytt om den ikke er oppfylt.
Siden denne optimaliseringen er svært klassisk, er det et mønster som ofte dukker opp selv i kompilatorer med lavt optimaliseringsnivå, i motsetning til strconv.Atoi-eksempelet over, så det er nyttig å kjenne til.
Ved å anvende dette prinsippet kan man oppnå kildekode som er forskjellig, men som produserer identisk assembly-kode.
Eksempel på speilvendt kode
Ved å bruke skriptet nedenfor kan man verifisere at bash-skriptet produserer to kildekoder som er «speilbilder» av hverandre, og at man, når man ser bort fra metadata som endres, oppnår nøyaktig samme assembly-kode for main-funksjonen.
1#!/usr/bin/env bash
2
3# 1. Fullstendig initialisering av eksisterende filer og kataloger
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. Opprettelse av den originale kildekoden (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. Opprettelse av speilvendt kildekode (main_from_asm.go)
36# Strukturen er symmetrisk synkronisert for at kompilatoren skal bruke samme optimalisering (JGE)
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 // Ved å bruke 10 som referanse og opprettholde x < 10 strukturen,
56 // velger kompilatoren samme JGE-mekanisme og blokkplassering som i main.go.
57 if x < 10 {
58 println(s1)
59 } else {
60 println(s2)
61 }
62}
63EOF
64
65# 4. Utfør kompilering i samme katalogmiljø
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. Bruk go tool objdump for å trekke ut ren main.main assembly-funksjon
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# Filtrer bort virtuelle adresser, offset og maskinkodedata,
80# for kun å sitte igjen med instruksjonssettet (Opcode & Operands)
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. Verifiser diff i maskinkodestrukturen
85echo "[6/6] Verifying assembly structural integrity via diff..."
86echo "------------------------------------------------------------"
87
88if diff orig_pure.asm asm_pure.asm > /dev/null; then
89 echo "===> [Suksess] Maskinkodelogikken i main.main er 100% identisk for begge binærfiler! <==="
90 echo "Vi har oppnådd identisk assembly ved å synkronisere kompilatorens optimaliserings-pipeline."
91else
92 echo "===> [Feil] Forskjeller oppdaget i assembly-instruksjonsstrukturen. <==="
93 diff -u orig_pure.asm asm_pure.asm
94fi
95echo "------------------------------------------------------------"
Ved å kjøre skriptet får man følgende informasjon:
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===> [Suksess] Maskinkodelogikken i main.main er 100% identisk for begge binærfiler! <===
9Vi har oppnådd identisk assembly ved å synkronisere kompilatorens optimaliserings-pipeline.
10------------------------------------------------------------
Konklusjon
Programmeringsspråk tilbyr mye abstraksjon, men bak denne abstraksjonen skjuler det seg svært interessante og aggressive optimaliseringer. Ved å utnytte dette, kan man lage «speilvendt» kode som har ulik kildekode, men identisk assembly. Hvis man er interessert i lavnivå-programmering og møter på lukket programvare skrevet i Go, er det ikke umulig å dekompilere og analysere assembly-koden for å gjenopprette kildekoden.
Neste forelesning
I neste forelesning skal vi se nærmere på select-case-setninger, som har sine egne fascinerende egenskaper i tillegg til if-setninger.