GoSuda

If-sætninger i Go-sproget

By Lee Yunjin
views ...

If-sætninger i Go-sproget

Først og fremmest skyldes vores valg af Go, at Go har den mest "smukke" assembly blandt moderne sprog, og sammenlignet med klassiske sprog er effektiviteten af dens syntaks ofte overvældende.

Da vi nu i den forrige lektion har forstået, hvordan et simpelt Go-program fungerer, skal vi straks sammenligne Go og Assembly linje for linje.

Kildekode

Som det er tilfældet i Go, optimerer moderne compiler-løsninger, herunder GCC, automatisk forgreningssætninger (branching statements), som ikke tjener noget formål. Da C-sprog-compilere som GCC og Clang også udfører meget aggressive optimeringer ved industristandarden -O2, kan man sige, at en æra, hvor programmøren ikke længere fuldt ud kan stole på compileren, endegyldigt blev fuldendt i slutningen af det 20. århundrede.

Derfor giver det kun mening at give compileren betingelser, som den ikke let kan forudsige og omdanne til en anden syntaks.

 1package main
 2
 3import (
 4    "os"
 5    "strconv"
 6)
 7
 8func main() {
 9    // Hvis man udfylder dette med noget forudsigeligt som x = 10,
10    // hvor forgreningen kan fjernes, vil compileren optimere og fjerne forgreningen.
11    // Derfor, for at betragte dette direkte i assembly, ville man i C-sprog
12    // anvende -O0 eller lignende, eller lade compileren bruge eksterne værdier,
13    // der er uforudsigelige. Da denne sektion omhandler moderne programmering,
14    // benytter vi ikke metoder til at deaktivere Go's binære optimering.
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 tilfælde oversættes forgreningssætningen direkte til maskinkode, da compileren ikke kan forudsige inputtet.

Assembly-sprog

 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 at benytte go tool får man venligt oplyst, hvilken syntaks der matcher hvilken assembly-kode.

Da vi i denne lektion skal lære om sammenligningssætninger og if-forgreningssætninger, er der et par linjer, vi skal være særligt opmærksomme på.

CMPQ-instruktion & JL-instruktion

CMPQ-instruktionen er beregnet til at sammenligne 4-byte (4-word) datatyper, og dens etymologi stammer fra CoMPare Quadword, forkortet til CMPQ.

Hvis vi ser på hukommelsesadressen 0x47a84e, indeholder den syntaksen CMPQ os.Args+8(SB), $0x2. I dette tilfælde sammenligner programmet antallet af modtagne argumenter med den hexadecimale værdi 0x2 (hvilket blot er 2).

Derefter udføres et hop via JL efter sammenligningen for at se, om argumenterne er færre end 2 (det vil sige, hvis programmet kun er sig selv som argument). Dette er en forkortelse for Jump, Less than. Ved den forudgående sammenligningsoperation, hvis argumentet var mindre end 2, hopper den til adressen 0x47a8b0, hvor der findes en JGE. Da denne syntaks dog benytter AX-registret, må vi forstå, hvad den værdi, der er gemt i registret, repræsenterer.

MOVQ-instruktion

Herefter skal vi forstå, hvordan den faktiske startadresse for data gemmes ved brug af 'CX'-registret, og hvordan data ekstraheres efter læsning af adressen.

Ved at betragte området 0x47858-0x47863 udføres denne operation trin for trin.

Først indsættes startadressen for argument-arrayet i CX-registret med instruktionen MOVQ os.Args(SB), CX. Her må man forstå Go's string-type.

Go's string er en struct, og denne struct består af 16 bytes, fordelt på to 8-byte datafelter.

struct8 byte8 byte
stringmem addressstring length

Hvis vi visualiserer dette, svarer det til ovenstående, hvor de første 8 bytes gemmer stringens startadresse, og de efterfølgende 8 bytes gemmer stringens længde.

Derfor gemmes stringens adresse i AX-registret, og stringens længde gemmes i BX-registret.

CALL

I det forrige indlæg, da vi betragtede runtime-funktionerne, var der også en CALL-instruktion knyttet til dem. Dette er knyttet til funktioner, der anvendes i Go, og betyder bogstaveligt talt at kalde (call) en given funktion. Derefter konverteres stringen til et heltal ved hjælp af CALL-funktionen, men hvor heltallet gemmes, fremgår ikke direkte i abstraktionen af funktionen.

CMPQ-instruktion & JGE-instruktion

Hvis vi vender tilbage til den tidligere adresse 0x47a86c, sammenligner instruktionen adressen på stringen med tallet 0xa (10 i decimaltal)!

Dette betyder, at da programmet ikke længere benytter det pågældende argument, er pladsen for stringen blevet overskrevet for at skabe plads til heltallet x.

Dette er kernen i den aggressive optimering, der foregår i sprog som Go.

Derefter optræder instruktionen JGE, som er en forkortelse for Jump, Greater or Equals. Derfor spørger denne syntaks, om værdien er større end eller lig med sammenligningsgrundlaget.

Det betyder, at syntaksen x < 10 ikke er direkte oversat, men at sammenligningsretningen for syntaksen er vendt om! Dette skyldes, at det i maskinkode er mere intuitivt at springe over på forhånd, når betingelsen ikke er opfyldt, frem for at udføre sammenligningen én gang, når betingelsen er opfyldt, for derefter at tjekke igen, om den ikke er opfyldt; det sparer desuden 1 instruktion.

Da denne form for optimering er meget klassisk, er det et mønster, der ofte optræder selv i compilere med en betydeligt lavere grad af optimering end i eksemplet med strconv.Atoi, så det er værd at kende til.

Ved at anvende dette punkt kan man opnå kildekode, der er 100 % identisk på assembly-niveau, selvom kilden er forskellig.

Eksempel på spejlbillede-kode

Ved at benytte nedenstående script kan man verificere, at bash-scriptet skaber to kilder, der er 100 % spejlbilleder af hinanden, og at de producerer nøjagtig samme assembly, når man ser udelukkende på main, bortset fra metadata, der ændrer sig fra gang til gang.

 1#!/usr/bin/env bash
 2
 3# 1. Fuldstændig initialisering af eksisterende resterende filer og mapper
 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. Skrivning af kildekode til original version (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. Skrivning af kildekode til spejlbillede-version (main_from_asm.go)
36# For at sikre at compileren bruger optimeringsskabelonen (JGE) direkte, synkroniseres operatorstrukturen perfekt symmetrisk
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 at tage udgangspunkt i 10 og bevare x < 10 strukturen, 
56        // anvender compileren den præcis samme JGE-mekanisme og blok-placering som i main.go.
57        if x < 10 {
58                println(s1)
59        } else {
60                println(s2)
61        }
62}
63EOF
64
65# 4. Udførelse af build i samme mappesti og filnavn-miljø
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. Udtrækning af ren main.main assembly-funktion ved brug af 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# Fjernelse af virtuel adresse, offset og maskinkode-byte-data,
80# og filtrering af kun de rene instruktionssæt (Opcode & Operands), som CPU'en skal eksekvere
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. Verificering af diff for de to maskinkode-instruktionsstrukturer
85echo "[6/6] Verifying assembly structural integrity via diff..."
86echo "------------------------------------------------------------"
87
88if diff orig_pure.asm asm_pure.asm > /dev/null; then
89    echo "===> [Succes] main.main maskinkodelogikken for de to binære filer er 100 % identisk! <==="
90    echo "Vi har perfekt synkroniseret compilerens optimerings-pipeline-retningslinjer for at opnå den samme assembly."
91else
92    echo "===> [Fejl] Der blev fundet forskelle i assembly-instruktionsstrukturen. <==="
93    diff -u orig_pure.asm asm_pure.asm
94fi
95echo "------------------------------------------------------------"

Ved faktisk at køre koden kan man opnå følgende information.

 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===> [Succes] main.main maskinkodelogikken for de to binære filer er 100 % identisk! <===
 9Vi har perfekt synkroniseret compilerens optimerings-pipeline-retningslinjer for at opnå den samme assembly.
10------------------------------------------------------------

Konklusion

Selvom programmeringssprog tilbyder mange abstraktioner, har vi set, at der bag abstraktionen gemmer sig meget interessante og aggressive optimeringer. Ved at udnytte dette kunne vi desuden skabe spejlbilled-kode, der har forskellig kildetekst, men identisk assembly. Hvis man har interesse for lav-niveau programmering og støder på proprietær software skrevet i Go, burde det ikke være umuligt at analysere ved at dekompilere assembly-koden for at rekonstruere kilden.

Næste lektion

I næste lektion vil vi undersøge select-case-sætningen, som har sin egen unikke charme i forhold til if-sætningen.