GoSuda

Analýza runtime štruktúry spravovaných jazykov a assembly kódu AMD64 binárnych súborov Go

By Lee Yunjin views ...

Čo sú spravované jazyky?

Spravované jazyky sú jazyky, ktoré na rozdiel od nespravovaných jazykov, t. j. jazykov, ktoré iba vykonávajú logiku napísanú programátorom bez väčšej odchýlky od nej, v runtime vykonávajú GC, runtime optimalizácie, zelené vlákna, spracovanie súbežnosti a ďalšie úlohy, a tým umožňujú používateľovi vyhnúť sa nebezpečnej nízkoúrovňovej správe pamäte a zdrojov. Pri takýchto jazykoch má vývojár tú výhodu, že sa môže sústrediť výlučne na biznis logiku a ponoriť sa do vývoja, avšak na druhej strane sa môže stať, že skutočné správanie programu sa líši od intuície programátora, a preto sú niekedy potrebné precízne úpravy runtime. Najprv sa pozrieme na jazyk Go, ktorý je medzi spravovanými jazykmi najvernejší minimalistickej filozofii a jeho assembly je priamočiare.

Binárna štruktúra jazyka Go

.text.data.gopclntab, .typelink atď.
Strojový kód na vykonanieDáta na uloženieSekcie jazykového runtime

Keďže jazyk Go neprekladá vstup používateľa do strojového kódu 1:1, logika v sekcii .text úzko súvisí aj so sekciami jazykového runtime. Okrem toho sa do assembly sekcie .text pridávajú funkcie, ktoré používateľ nenapísal, napríklad runtime.printnl(). Vďaka takémuto automatickému vkladaniu kódu Go pomáha vývojárovi oslobodiť sa od manuálnej správy.

Pohľad na funkciu main v jazyku Go

Najprv vytvoríme jednoduchý príkladový zdrojový súbor main.go a pozrieme sa na main na stroji AMD64.

 1package main
 2
 3func sayHello(msg string) {
 4    println(msg)
 5}
 6
 7func main() {
 8    sayHello("Hello World")
 9}
10

Potom ho zostavíme takto.

1go build main.go
2

Go na jednoduché nízkoúrovňové ladenie podporuje nástroj go tool. Aby sme v nástroji go tool zobrazili iba assembly funkcie main z hlavného balíka, zadáme tento príkaz.

1go tool objdump -s "main\.main" ./main
2

Assembly

 1TEXT main.main(SB) /home/yjlee/compare-assembly/go/main.go
 2  main.go:7             0x468f60                493b6610                CMPQ SP, 0x10(R14)
 3  main.go:7             0x468f64                762f                    JBE 0x468f95
 4  main.go:7             0x468f66                55                      PUSHQ BP
 5  main.go:7             0x468f67                4889e5                  MOVQ SP, BP
 6  main.go:7             0x468f6a                4883ec10                SUBQ $0x10, SP
 7  main.go:8             0x468f6e                90                      NOPL
 8  main.go:4             0x468f6f                e8cca3fcff              CALL runtime.printlock(SB)
 9  main.go:4             0x468f74                488d05da290100          LEAQ 0x129da(IP), AX
10  main.go:4             0x468f7b                bb0b000000              MOVL $0xb, BX
11  main.go:4             0x468f80                e83bacfcff              CALL runtime.printstring(SB)
12  main.go:4             0x468f85                e8f6a5fcff              CALL runtime.printnl(SB)
13  main.go:4             0x468f8a                e811a4fcff              CALL runtime.printunlock(SB)
14  main.go:9             0x468f8f                4883c410                ADDQ $0x10, SP
15  main.go:9             0x468f93                5d                      POPQ BP
16  main.go:9             0x468f94                c3                      RET
17  main.go:7             0x468f95                e8e6afffff              CALL runtime.morestack_noctxt.abi0(SB)
18  main.go:7             0x468f9a                ebc4                    JMP main.main(SB)
19
  • Porovná sa stráž zásobníka v registri ovládacieho bloku goroutine (R14) s aktuálnym ukazovateľom zásobníka (SP) pomocou CMPQ, aby sa zistilo, či je dostatok miesta pre rámec zásobníka aktuálnej goroutine. Ak miesto nestačí, skok (JBE) smeruje na adresu 0x468f95, ktorá je vstupným bodom pre rozšírenie zásobníka.
  • Pomocou PUSHQ BP sa na zásobník uloží predchádzajúci ukazovateľ bázy.
  • Do registra ukazovateľa bázy (BP) sa skopíruje aktuálny ukazovateľ zásobníka (SP), čím sa pevne určí referenčný bod zásobníka na začiatku funkcie.
  • Potom sa alokuje 16 bajtov priestoru pre lokálne premenné zásobníka (SUBQ $0x10, SP) a pomocou NOPL sa vyplnia virtuálne inštrukcie, aby sa zabezpečilo zarovnanie pre CPU cache.
  • Na synchronizáciu štandardného výstupu reťazcov v runtime Go sa volaním runtime.printlock(SB) získa zámok.
  • Pomocou inštrukcie LEAQ sa do registra AX, ktorý podľa ABI jazyka Go slúži ako prvý parameter, uloží začiatočná adresa konštantného reťazca ("Hello World").
  • Následne sa dĺžka reťazca uloží do registra BX, ktorý slúži ako druhý parameter (MOVL $0xb, BX, t. j. v desiatkovej sústave 11).
  • Volaním runtime.printstring(SB) sa na základe odovzdaných hodnôt AX (adresa dát) a BX (dĺžka) vypíše výstup do konzoly.
  • Na spracovanie nového riadku sa volá runtime.printnl(SB).
  • Keďže výstup je dokončený, zámok sa uvoľní volaním runtime.printunlock(SB).
  • Pamäť zásobníka o veľkosti 16 bajtov alokovaná pomocou ADDQ $0x10, SP sa obnoví.
  • Pomocou POPQ BP sa obnoví pôvodný ukazovateľ bázy.
  • Pomocou RET sa riadenie vráti na miesto, odkiaľ bola funkcia volaná.
  • Ak pri prvej kontrole zásobníka chýbal priestor, volá sa runtime.morestack_noctxt.abi0(SB) na adrese 0x468f95, ktorá dynamicky rozširuje zásobník runtime, ako je v spravovaných jazykoch zvykom.
  • Po dokončení rozšírenia zásobníka sa skokom (JMP) vráti na vstupný bod main.main(SB). Ako vidno, assembly biznis logiky je pomerne jasné a pridáva sa k nemu iba tenká vrstva správy runtime.

Keď nie je optimalizácia

Vyššie uvedená podoba je výsledkom toho, že kompilátor Go automaticky inline-oval dve oddelené funkcie a optimalizoval ich. Na účely učenia však nechceme, aby sa sayHello inline-ovala, preto zdrojový kód skompilujeme s týmto príznakom.

1go build -gcflags="-l" main.go
2

Ak výsledok zobrazíme v shelli, nájdeme duplicitné assembly.

 1yjlee@elegant:~/compare-assembly/go$ go build -gcflags="-l" main.go
 2
 3go tool objdump -s "main\.sayHello" ./main
 4TEXT main.sayHello(SB) /home/yjlee/compare-assembly/go/main.go
 5  main.go:3             0x468f60                493b6610               CMPQ SP, 0x10(R14)
 6  main.go:3             0x468f64                7636                   JBE 0x468f9c
 7  main.go:3             0x468f66                55                     PUSHQ BP
 8  main.go:3             0x468f67                4889e5                 MOVQ SP, BP
 9  main.go:3             0x468f6a                4883ec10               SUBQ $0x10, SP
10  main.go:5             0x468f6e                4889442420             MOVQ AX, 0x20(SP)
11  main.go:5             0x468f73                48895c2428             MOVQ BX, 0x28(SP)
12  main.go:4             0x468f78                e8c3a3fcff             CALL runtime.printlock(SB)
13  main.go:4             0x468f7d                488b442420             MOVQ 0x20(SP), AX
14  main.go:4             0x468f82                488b5c2428             MOVQ 0x28(SP), BX
15  main.go:4             0x468f87                e834acfcff             CALL runtime.printstring(SB)
16  main.go:4             0x468f8c                e8efa5fcff             CALL runtime.printnl(SB)
17  main.go:4             0x468f91                e80aa4fcff             CALL runtime.printunlock(SB)
18  main.go:5             0x468f96                4883c410               ADDQ $0x10, SP
19  main.go:5             0x468f9a                5d                     POPQ BP
20  main.go:5             0x468f9b                c3                     RET
21  main.go:3             0x468f9c                4889442408             MOVQ AX, 0x8(SP)
22  main.go:3             0x468fa1                48895c2410             MOVQ BX, 0x10(SP)
23  main.go:3             0x468fa6                e8d5afffff             CALL runtime.morestack_noctxt.abi0(SB)
24  main.go:3             0x468fab                488b442408             MOVQ 0x8(SP), AX
25  main.go:3             0x468fb0                488b5c2410             MOVQ 0x10(SP), BX
26  main.go:3             0x468fb5                eba9                   JMP main.sayHello(SB)
27

Pri vypnutí inline optimalizácie sa do inštrukcií MOVQ vkladajú operácie, ktoré uložia parametre (AX, BX) na offsety ukazovateľa zásobníka, napríklad 0x20(SP), aby sa zachovali podľa konvencie volania funkcie. Kompilátor teda optimalizuje práve takéto zbytočné presuny pamäte a réžiu volaní, čo sa tu potvrdilo.

Ďalšia časť

V ďalšej časti sa budeme venovať príkazom if a switch v jazyku Go. Ak budem mať čas, plánujem analyzovať aj sekcie runtime jazyka Go.