GoSuda

If-setninger i Go-språket

By Lee Yunjin
views ...

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.

struct8 byte8 byte
stringmem addressstring 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.