Mi a managed language?
Mi az a managed nyelv?
A managed nyelv az unmanaged nyelvektől – azaz azoktól a nyelvektől, amelyek a programozó által írt logikát nem követik érdemben, csupán végrehajtják azt – eltérően olyan funkciókat biztosít futásidőben, mint a GC, a futásidejű optimalizáció, a green threads és a konkurens feldolgozás, így a felhasználónak nem kell kockázatos, alacsony szintű memóriakezeléssel foglalkoznia. Az ilyen nyelvek esetében előny, hogy a fejlesztő kizárólag az üzleti logikára koncentrálhat, ugyanakkor hátrány, hogy a program a programozói intuíciótól eltérően működhet, ami esetenként kifinomult futásidejű hangolást tesz szükségessé. Először a managed nyelvek közül a minimalista filozófiát leginkább követő, az assembly szinten is transzparens 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 futásidejű szekció |
| Mivel a Go nyelv nem 1:1 arányban fordítja le a felhasználói kódot gépi kódra, a .text szekció logikája szorosan összefonódik a nyelvi futásidejű szekciókkal. | ||
| Ezenkívül olyan, a felhasználó által nem definiált függvények, mint a runtime.printnl(), szintén bekerülnek a .text szekció assembly kódjába. | ||
| Ezen automatikus kódbeillesztések révén a Go nyelv segít a fejlesztőnek megszabadulni a manuális kezelés terhétől. |
A main függvény vizsgálata Go nyelven
Először is írjunk egy egyszerű main.go forrásfájlt, és tekintsük 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}
10
Ezt követően fordítsuk le az alábbi módon:
1go build main.go
2
A Go támogatja a go tool eszközt az egyszerű, alacsony szintű hibakeresés érdekében. A fő csomagban lévő main függvényhez tartozó assembly kód megjelenítéséhez az alábbi parancsot használjuk:
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 program a CMPQ utasítással összehasonlítja a veremmutatót (SP) a gorutin vezérlőblokk regiszterében (R14) tárolt veremvédelmi értékkel, és ha a veremterület nem elegendő, a 0x468f95 belépési címre ugrik (JBE) a verem bővítéséhez.
- Az előző bázismutatót a PUSHQ BP utasítással a verembe menti.
- A bázismutató (BP) regiszterbe másolja az aktuális veremmutatót (SP), rögzítve ezzel a verem kezdőpontját a függvény elején.
- Ezt követően 16 bájtnyi helyet foglal a lokális változók számára (SUBQ $0x10, SP), majd a NOPL utasítással virtuális parancsokat tölt be a CPU-gyorsítótár igazítása érdekében.
- A Go futáskörnyezetben a belső sztringek standard kimenetre történő szinkronizálása érdekében meghívja a runtime.printlock(SB) függvényt a zároláshoz.
- A LEAQ utasítással a konstansként tárolt sztring ("Hello World") kezdőcímét a Go ABI szabvány szerinti első paraméter regiszterbe, az AX-be tölti.
- Ezt követően a sztring hosszát a második paraméter regiszterbe, a BX-be tölti (MOVL $0xb, BX, azaz decimálisan 11).
- Meghívja a runtime.printstring(SB) függvényt, amely az AX (adatcím) és BX (hossz) információk alapján kiírja az adatokat a konzolra.
- A soremeléshez meghívja a runtime.printnl(SB) függvényt.
- A kiírás befejeztével a runtime.printunlock(SB) hívásával feloldja a zárolást.
- Az ADDQ $0x10, SP utasítással felszabadítja a korábban lefoglalt 16 bájtos veremterületet.
- A POPQ BP utasítással visszaállítja az előző bázismutatót.
- A RET utasítással visszaadja a vezérlést a hívó függvénynek.
- Amennyiben a kezdeti veremellenőrzés során nem volt elegendő hely, a program meghívja a 0x468f95 címen található runtime.morestack_noctxt.abi0(SB) függvényt, amely a managed nyelvek sajátosságának megfelelően dinamikusan bővíti a veremterületet.
- A verem bővítése után a program visszatér (JMP) a main.main(SB) belépési pontjára. Amint látható, az üzleti logika assembly kódja meglehetősen tiszta, és csak minimális futásidejű menedzsmenttel egészül ki.
Optimalizáció hiánya
A fenti forma annak az eredménye, hogy a Go fordító automatikusan inlining technológiát alkalmazott a két különálló függvényre. Tanulási célból azonban ebben az esetben tiltsuk le a sayHello függvény inliningját. Ehhez a következő jelölővel (flag) fordítsuk a forráskódot:
1go build -gcflags="-l" main.go
2
A parancssori kimenetben ellenőrizve redundáns assembly kód figyelhető meg.
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
Az inlining kikapcsolásakor a függvényhívási konvenciónak megfelelően a paraméterek (AX, BX) megőrzése érdekében a veremmutató eltolásával (pl. 0x20(SP)) MOVQ műveletek kerülnek beillesztésre. Ebből látható, hogy a fordító optimalizációja többek között az ilyen felesleges memóriamozgatási műveletek és a hívási többletköltségek (overhead) 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 fogunk foglalkozni. Amennyiben időnk engedi, a későbbiekben a Go futásidejű szekcióinak elemzésére is sort kerítünk.