GoSuda

Felügyelt nyelvek futásidejű szerkezete és a Go bináris AMD64 assembly elemzése

By Lee Yunjin views ...

Mi az a menedzselt nyelv?

A menedzselt nyelv olyan nyelv, amely a nem menedzselt nyelvekkel ellentétben, amelyek esetében a programozó által megírt logikát lényegében csak végrehajtják, a szemétgyűjtést (GC), a futásidejű optimalizálást, a zöld szálakat, a konkurens feldolgozást és hasonló feladatokat a futásidejű környezetben végzi el, így a felhasználónak nem kell veszélyes, alacsony szintű erőforrás-kezelést végeznie. Az ilyen nyelvek előnye, hogy a fejlesztő kizárólag az üzleti logikára koncentrálhat és teljesen elmerülhet a fejlesztésben, hátrányuk azonban, hogy a programozó elképzelése és a program tényleges működése eltérhet egymástól, ezért néha finomhangolt futásidejű beállításokra is szükség lehet. Először a menedzselt nyelvek közül azt a Go nyelvet vizsgáljuk meg, amely a legszigorúbban követi a minimalista filozófiát és az assembly kódja a leginkább őszinte.

A Go nyelv bináris szerkezete

.text.data.gopclntab, .typelink stb.
Végrehajtandó gépi kódTárolandó adatokNyelvi futásidejű szekciók

Mivel a Go nyelv nem fordítja le 1:1 arányban gépi kódra a felhasználó által megadott kódot, a .text szekció logikája szorosan összefügg a nyelvi futásidejű szekciókkal. Emellett a felhasználó által nem írt függvények, például a runtime.printnl(), is bekerülnek a .text szekció assembly kódjába. Ezen automatikus kódbeszúrás révén a Go nyelv segít abban, hogy a fejlesztő megszabaduljon a kézi erőforrás-kezeléstől.

Csak a main függvény vizsgálata Go-ban

Először írjunk egy egyszerű, main.go nevű példaforrást, és nézzük meg a main függvényt AMD64 gépen.

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

Ezután fordítsuk le a következő paranccsal.

1go build main.go
2

A Go az egyszerű alacsony szintű hibakereséshez a go tool eszközt biztosítja. A go toolban a főcsomag main függvényének megfelelő assembly kódrészlet megtekintéséhez a következő parancsot adjuk ki.

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
  • A CMPQ utasítással összehasonlítjuk a jelenlegi goroutine veremkeretének elegendő méretét a goroutine vezérlőblokk regiszterében (R14) lévő veremőr értékkel és a jelenlegi veremmutatóval (SP). Ha a hely nem elegendő, a JBE utasítással a 0x468f95 címre ugrunk, amely a verem bővítésének belépési pontja.
  • A PUSHQ BP utasítással elmentjük a korábbi bázismutatót a verembe.
  • A MOVQ SP, BP utasítással a bázismutató (BP) regiszterbe másoljuk a jelenlegi veremmutatót, így rögzítjük a függvény indulásakor érvényes veremalapot.
  • Ezt követően 16 bájtnyi lokális változó-területet foglalunk le (SUBQ $0x10, SP), a NOPL utasítással pedig virtuális utasításokkal töltjük ki a helyet a CPU-gyorsítótár-igazítás érdekében.
  • A Go futásidejű környezetében a belső karakterláncok szabványos kimenetének szinkronizálása érdekében a runtime.printlock(SB) meghívásával zárat helyezünk el.
  • A LEAQ utasítással a konstansként megadott karakterlánc ("Hello World") kezdőcímét a Go ABI szabványa szerint az első paraméterül szolgáló AX általános célú regiszterbe töltjük.
  • Ezután a karakterlánc hosszát jelölő értéket a második paraméterregiszterbe, BX-be írjuk. (MOVL $0xb, BX, azaz tízes számrendszerben 11)
  • A runtime.printstring(SB) meghívásával az átadott AX (adatcím) és BX (hossz) információk alapján kiíratjuk a konzolra a szöveget.
  • A sortörés kezelése érdekében meghívjuk a runtime.printnl(SB) függvényt.
  • A kiírás befejeződött, ezért a runtime.printunlock(SB) hívással feloldjuk a zárat.
  • Az ADDQ $0x10, SP utasítással visszaállítjuk a 16 bájtnyi lefoglalt veremterületet.
  • A POPQ BP utasítással visszaállítjuk a korábbi bázismutatót.
  • A RET utasítással a vezérlést visszaadjuk a függvényt hívó helyre.
  • Ha az első veremellenőrzésnél nem volt elegendő hely, a 0x468f95 címen lévő runtime.morestack_noctxt.abi0(SB) függvényt hívjuk meg, amely a menedzselt nyelvekhez illő módon dinamikusan bővíti a verem futásidejű környezetét.
  • A verem bővítésének befejezése után a JMP utasítással visszatérünk a main.main(SB) belépési pontjára. Amint látható, az üzleti logika assembly kódja meglehetősen egyértelmű, és csupán vékony futásidejű erőforrás-kezelés egészül ki vele.

Optimalizálás nélkül

A fenti forma annak az eredménye, hogy a Go fordító automatikusan beágyazta (inline) a két különálló függvényt. Mivel azonban a tanulás céljából nem szeretnénk, hogy ebben az esetben a sayHello függvény beágyazásra kerüljön, a forrást a következő kapcsolóval fordítjuk le.

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

A terminálon az eredményt megvizsgálva ismétlődő assembly kódrészleteket találunk.

 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

Ha kikapcsoljuk a beágyazást, a függvényhívási konvenciónak megfelelően a paraméterek (AX, BX) megőrzése érdekében olyan MOVQ műveletek kerülnek be, amelyek a 0x20(SP) stackmutató-eltolású helyekre töltik vissza az értékeket. Vagyis megállapítható, hogy a fordító optimalizációja éppen ezeknek a felesleges memóriamozgatási műveleteknek és hívási többletterhelésnek a kiküszöbölésére irányul.

Következő alkalom

A következő alkalommal a Go nyelv if és switch utasításaival foglalkozunk. Ha később marad rá idő, a Go futásidejű szekcióit is elemezni fogjuk.