Mi az a managed language?
Mi a managed nyelv?
A managed nyelv – ellentétben az unmanaged nyelvekkel, amelyek lényegében nem térnek el a programozó által megírt logikától, csupán végrehajtják azt – olyan nyelv, amely futásidőben hajt végre GC-t (Garbage Collection), futásidejű optimalizálást, green thread kezelést, valamint konkurens műveleteket, ezáltal mentesítve a felhasználót a veszélyes, alacsony szintű erőforrás-kezeléstől.
Az ilyen nyelvek esetében előny, hogy a fejlesztő kizárólag a business logic-ra koncentrálhat, ugyanakkor hátrányuk, hogy a program a programozó intuíciójától eltérően is működhet, ami esetenként kifinomult futásidejű tuningot tesz szükségessé.
Először a managed nyelvek közül a leginkább minimalista filozófiát követő és az assembly tekintetében legőszintébb Go nyelvet vizsgáljuk meg.
A Go nyelv bináris struktúrája
| .text | .data | .gopclntab, .typelink stb. |
|---|---|---|
| Végrehajtandó gépi kód | Tárolandó adatok | Nyelvi runtime szekciók |
Mivel a Go nyelv nem 1:1 arányban fordítja a felhasználói bemenetet gépi kódra, a .text szekció logikája szorosan összefonódik a nyelvi runtime szekciókkal.
Emellett olyan függvények is bekerülnek a .text szekció assemblyjébe, amelyeket a felhasználó nem írt meg, mint például a runtime.printnl(). Ezen automatikus kódbeillesztések révén a Go nyelv segít a fejlesztőnek megszabadulni a kézi menedzselés terhétől.
A main függvény vizsgálata Go nyelven
Először is írjunk egy egyszerű main.go forráskódot, és vizsgáljuk meg a main függvényt AMD64 architektúrán.
1package main
2
3func sayHello(msg string) {
4 println(msg)
5}
6
7func main() {
8 sayHello("Hello World")
9}
Ezt követően az alábbi módon építjük (build) fel:
1go build main.go
A Go az egyszerű alacsony szintű hibakeresés érdekében támogatja a go tool használatát.
A go tool segítségével a main csomagból a main függvényre vonatkozó assembly megtekintéséhez az alábbi parancsot adjuk meg:
1go tool objdump -s "main\.main" ./main
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)
- A CMPQ utasítással összehasonlítjuk, hogy a jelenlegi szálra léptünk-e be, majd ha igen, ugrunk a 0x468f95 belépési pontra (Entrypoint).
- A belépési pontot a
PUSHQ BPutasítással helyezzük a stack-re. - A legutóbb adatot tároló SP regiszterben a függvény kezdetekor kijelöljük a stack kezdőpontját, így rögzítve a lokális változók referenciájának belépési pontját.
- Ezt követően lefoglalunk 16 bájtot a lokális változók stack-je számára (
SUBQ $0x10, SP), majd a NOPL utasítással több bájtot kitöltve elvégezzük a CPU cache igazítását. - A Go Runtime-ban a
runtime.printlock(SB)meghívásával zároljuk a string buffer kimenetét. - A LEAQ utasítással a lefoglalt karakterlánc kezdőcímét elmentjük az AX regiszterbe, amely az általános célú regiszterek közül az adattárolásra használt akkumulátor.
- Ezt követően a BX regiszterbe, amelyet műveleti segédletként és ideiglenes adattárolásra használunk, elmentjük a karakterlánc 11-es hosszát. (
MOVL $0Xb, BX) - A runtime.printstring(SB) segítségével az akkumulátor információit az SB felé írjuk ki.
- Egy üres sort szintén a runtime.printnl(SB) segítségével írunk az SB felé.
- A string buffert a runtime.printunlock(SB) segítségével oldjuk fel.
- Az ADDQ $0x10, SP utasítással visszaadjuk a kölcsönvett 16 bájtos stack memóriát. Mivel a belépési pontot a stack-re helyezve jeleztük, a POPQ BP utasítással kivesszük azt a stack-ből, majd kiadjuk a visszatérési szignált.
- Ezt követően a runtime.morestack_noctxt.abi0(SB) segítségével, managed nyelvhez méltóan, lefoglaljuk a megfelelő stack-et és beállítjuk a runtime-ot, beleértve a GC-t is.
- Végül a vezérlés átkerül a menedzselt main.main(SB) címre.
Amint látható, a business logic assemblyje meglehetősen világos, csupán egy vékony runtime kezelés rétegeződik rá.
Optimalizálás nélkül
A fenti forma a Go fordító által automatikusan elvégzett inlining eredménye, ahol a két különálló függvényt összefűzte. Tanulási célból azonban ebben az esetben tiltsuk le a sayHello inliningját.
Ehhez az alábbi flag-gel fordítjuk a forráskódot:
1 go build -gcflags="-l" main.go
A shellben az eredményeket vizsgálva duplikált assembly kód fedezhető fel.
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)
27yjlee@elegant:~/compare-assembly/go$ go tool objdump -s "main\.sayHello" ./main
28TEXT main.sayHello(SB) /home/yjlee/compare-assembly/go/main.go
29 main.go:3 0x468f60 493b6610 CMPQ SP, 0x10(R14)
30 main.go:3 0x468f64 7636 JBE 0x468f9c
31 main.go:3 0x468f66 55 PUSHQ BP
32 main.go:3 0x468f67 4889e5 MOVQ SP, BP
33 main.go:3 0x468f6a 4883ec10 SUBQ $0x10, SP
34 main.go:5 0x468f6e 4889442420 MOVQ AX, 0x20(SP)
35 main.go:5 0x468f73 48895c2428 MOVQ BX, 0x28(SP)
36 main.go:4 0x468f78 e8c3a3fcff CALL runtime.printlock(SB)
37 main.go:4 0x468f7d 488b442420 MOVQ 0x20(SP), AX
38 main.go:4 0x468f82 488b5c2428 MOVQ 0x28(SP), BX
39OVQ 0x20(SP), AX
40 main.go:4 0x468f82 488b5c2428 MOVQ 0x28(SP), BX
41 main.go:4 0x468f87 e834acfcff CALL runtime.printstring(SB)
42 main.go:4 0x468f8c e8efa5fcff CALL runtime.printnl(SB)
43 main.go:4 0x468f91 e80aa4fcff CALL runtime.printunlock(SB)
44 main.go:5 0x468f96 4883c410 ADDQ $0x10, SP
45 main.go:5 0x468f9a 5d POPQ BP
46 main.go:5 0x468f9b c3 RET
47 main.go:3 0x468f9c 4889442408 MOVQ AX, 0x8(SP)
48 main.go:3 0x468fa1 48895c2410 MOVQ BX, 0x10(SP)
49 main.go:3 0x468fa6 e8d5afffff CALL runtime.morestack_noctxt.abi0(SB)
50 main.go:3 0x468fab 488b442408 MOVQ 0x8(SP), AX
51 main.go:3 0x468fb0 488b5c2410 MOVQ 0x10(SP), BX
52 main.go:3 0x468fb5 eba9 JMP main.sayHello(SB)
53yjlee@elegant:~/compare-assembly/go$
Ebből megállapítható, hogy a fordító optimalizációs tevékenysége az ilyen redundáns műveletek, valamint a nem hatékony loop unrolling kiküszöbölésére irányul.
Következő alkalommal
A következő részben a Go nyelv if és switch utasításaival foglalkozunk. Ha az idő engedi, a későbbiekben a Go runtime szekcióinak elemzésére is sort kerítünk.