GoSuda

Instrucțiunea If în limbajul Go

By Lee Yunjin
views ...

Instrucțiunea If în limbajul Go

Mai întâi, am ales Go deoarece, dintre limbajele moderne, Go are cel mai „estetic” assembly, iar eficiența sintaxei sale este adesea copleșitoare chiar și în comparație cu limbajele clasice.

Acum că am înțeles în cursul anterior modul de operare al unui program simplu în Go, să comparăm direct Go și Assembly linie cu linie.

Cod sursă

În primul rând, așa cum se întâmplă și în Go, chiar și compilatoarele moderne, inclusiv GCC, optimizează automat instrucțiunile de ramificare care nu au o utilitate practică. Compilatoarele de limbaj C, precum GCC și Clang, efectuează optimizări foarte agresive la standardul industrial -O2, astfel încât epoca în care programatorul putea să nu aibă încredere deplină în compilator s-a încheiat, în cele din urmă, încă de la sfârșitul secolului al XX-lea.

Prin urmare, pentru a avea cel puțin o semnificație, trebuie să oferim condiții pe care compilatorul să le poată anticipa cu dificultate și să le transforme în alte instrucțiuni.

 1package main
 2
 3import (
 4    "os"
 5    "strconv"
 6)
 7
 8func main() {
 9    // Dacă completăm acest lucru cu ceva predictibil, cum ar fi x = 10,
10    // care permite eliminarea ramificației, compilatorul va optimiza și va șterge ramificația.
11    // Prin urmare, pentru a observa acest lucru direct în assembly, în limbajul C
12    // s-ar folosi -O0 sau s-ar utiliza valori externe imprevizibile pentru compilator,
13    // însă, deoarece această secțiune tratează programarea modernă, nu vom folosi
14    // metoda dezactivării optimizării binare în 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}

În acest caz, deoarece compilatorul nu poate prezice input-ul, instrucțiunea de ramificare este tradusă direct în limbaj mașină.

Limbaj de asamblare (Assembly)

 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)

Dacă folosim go tool, acesta ne indică amabil ce sintaxă corespunde cărei instrucțiuni assembly.

Deoarece ceea ce vom învăța acum sunt instrucțiunile de comparare și ramificarea if, trebuie să acordăm atenție câtorva linii.

Instrucțiunea CMPQ & Instrucțiunea JL

Instrucțiunea CMPQ este utilizată pentru a compara tipuri de date de 4 octeți (4 cuvinte), iar etimologia sa provine de la CoMPareQuadword, fiind abreviată ca CMPQ.

Dacă privim adresa de memorie 0x47a84e, observăm sintaxa CMPQ os.Args+8(SB), $0x2. În acest caz, programul compară numărul de argumente primite cu valoarea hexazecimală 0x2 (adică pur și simplu 2).

Ulterior, prin JL, se efectuează o comparație și un salt dacă numărul de argumente este mai mic decât 2 (adică dacă programul primește doar el însuși ca argument). Aceasta provine de la Jump, Less than, fiind abreviată ca JL. Dacă, în urma operației de comparare anterioare, argumentul a fost mai mic decât 2, se sare la adresa 0x47a8b0, unde se află JGE. Totuși, deoarece această sintaxă utilizează registrul AX, trebuie să înțelegem natura valorii stocate în registru.

Instrucțiunea MOVQ

După aceasta, trebuie să știm cum se stochează adresa de început a datelor folosind registrul 'CX' și cum se încearcă extragerea datelor reale după citirea adresei.

Dacă observăm intervalul 0x47858-0x47863, vedem că această operație este efectuată pas cu pas.

Mai întâi, adresa de început a tabloului de argumente este introdusă în registrul CX prin instrucțiunea MOVQ os.Args(SB), CX. În acest moment, trebuie înțeles tipul de date string din Go.

string în Go este o structură, iar această structură este compusă din 16 octeți, fiind formată din două date de câte 8 octeți.

struct8 byte8 byte
stringmem addressstring length

Dacă o reprezentăm vizual, arată ca mai sus, unde primii 8 octeți stochează adresa de început a string-ului, iar ultimii 8 octeți stochează lungimea acestuia.

Prin urmare, adresa string-ului este stocată în registrul AX, iar lungimea string-ului în registrul BX.

CALL

Și în postările anterioare, când analizam funcțiile din runtime, era prezentă instrucțiunea CALL. Aceasta apare în fața funcțiilor utilizate în Go și înseamnă, literal, că se apelează o funcție. Ulterior, folosind funcția CALL, string-ul este convertit într-un număr întreg, însă locul în care este stocat acest număr întreg nu este vizibil, fiind abstractizat în funcție.

Instrucțiunea CMPQ & Instrucțiunea JGE

Revenind la adresa 0x47a86c de mai devreme, instrucțiunea compară adresa string-ului cu numărul 0xa (10 în sistem zecimal)!

Acest lucru înseamnă că, deoarece variabila respectivă nu mai este utilizată în program, compilatorul a suprascris locația string-ului pentru a crea spațiu pentru variabila întreagă x.

Aceasta este esența optimizării agresive care are loc în limbaje precum Go.

Ulterior, apare instrucțiunea JGE, care este abrevierea pentru Jump, Greater or Equals. Prin urmare, această sintaxă întreabă dacă valoarea este mai mare sau egală cu obiectul comparat.

Așadar, nu este exact sintaxa x < 10, ci direcția de comparare a sintaxei este inversată în x < 10! Acest lucru se întâmplă deoarece, în limbajul mașină, săritura preventivă atunci când condiția nu este îndeplinită este mai intuitivă și economisește o instrucțiune față de efectuarea comparației o dată când condiția este îndeplinită și verificarea ulterioară a neconcordanței.

Această optimizare este foarte clasică, așa că, spre deosebire de exemplul strconv.Atoi de mai sus, este un model care apare frecvent chiar și în compilatoarele cu un nivel de optimizare destul de scăzut, deci este util de reținut.

Prin urmare, aplicând aceste aspecte, se pot obține surse care, deși diferă, sunt 100% identice la nivel de assembly.

Exemplu de cod în oglindă

Folosind scriptul de mai jos, se poate verifica faptul că scriptul Bash creează două surse care sunt oglinzi 100% una față de cealaltă și că, ignorând metadatele care se schimbă de la caz la caz, se obține exact același assembly atunci când privim doar main.

 1#!/usr/bin/env bash
 2
 3# 1. Inițializarea completă a fișierelor și directoarelor reziduale existente
 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. Scrierea codului sursă al versiunii originale (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. Scrierea codului sursă al versiunii în oglindă (main_from_asm.go)
36# Structura operatorilor este sincronizată simetric pentru ca compilatorul să folosească șablonul de optimizare (JGE) exact așa cum este
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        // Luând 10 ca punct de referință și menținând structura x < 10, compilatorul
56        // va adopta exact același mecanism JGE și aceeași dispunere a blocurilor ca în main.go.
57        if x < 10 {
58                println(s1)
59        } else {
60                println(s2)
61        }
62}
63EOF
64
65# 4. Efectuarea compilării în același mediu de director și nume de fișier
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. Extragerea funcției assembly pure main.main folosind 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# Eliminarea adreselor virtuale, offset-urilor și datelor binare ale limbajului mașină,
80# filtrând doar câmpurile setului de instrucțiuni pure (Opcode & Operands) pe care le va executa CPU-ul
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. Verificarea diff a structurii celor două instrucțiuni mașină
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] Logica limbajului mașină main.main a celor două binare coincide 100%! <==="
90    echo "Am obținut același assembly prin sincronizarea perfectă a ghidurilor pipeline-ului de optimizare al compilatorului."
91else
92    echo "===> [Eșec] S-au descoperit diferențe în structura instrucțiunilor assembly. <==="
93    diff -u orig_pure.asm asm_pure.asm
94fi
95echo "------------------------------------------------------------"

Dacă rulați efectiv sursa, puteți obține următoarele informații.

 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] Logica limbajului mașină main.main a celor două binare coincide 100%! <===
 9Am obținut același assembly prin sincronizarea perfectă a ghidurilor pipeline-ului de optimizare al compilatorului.
10------------------------------------------------------------

Concluzie

Limbajele de programare oferă multe abstractizări, însă am putut observa că, în spatele acestora, se ascund optimizări foarte interesante și agresive. De asemenea, folosind aceste aspecte, am putut crea cod în oglindă care, deși diferit ca sursă, este identic la nivel de assembly. Dacă sunteți interesați de nivelul scăzut și întâlniți software proprietar scris în Go, recuperarea sursei prin dezasamblarea și analizarea directă a codului assembly nu pare a fi un lucru imposibil.

Următorul curs

În cursul următor, vom analiza instrucțiunea select-case, care are un alt farmec față de instrucțiunea If.