Runtimestruktur for administrerede sprog og analyse af Go-binærfiler i AMD64-assembler

Hvad er et managed sprog?
Et managed sprog er et sprog, der i modsætning til et unmanaged sprog, altså et sprog der udfører programmørens logik uden at afvige meget fra den, hjælper brugeren med at undgå farlig lavniveau-håndtering ved at udføre GC, runtime-optimering, grønne tråde, samtidighedshåndtering og lignende i runtime. Ved sådanne sprog er der den fordel, at man kan koncentrere sig om forretningslogikken og fordybe sig i udviklingen, men på den anden side kan programmets faktiske adfærd afvige fra programmørens intuition, så der somme tider er behov for præcis runtime-tuning. Lad os først se på Go, som er det managed sprog, der er mest tro mod en minimalistisk filosofi, og hvor assembly er ærligt.
Go-sprogets binære struktur
| .text | .data | .gopclntab, .typelink osv. |
|---|---|---|
| Maskinkode der skal køres | Data der skal gemmes | Sektioner for sprogets runtime |
Da Go ikke oversætter til maskinkode 1:1 ud fra det, brugeren har skrevet, er logikken i .text-sektionen tæt forbundet med sprogets runtime-sektioner. Desuden tilføjes funktioner, som brugeren ikke selv har skrevet, såsom runtime.printnl(), til assembly i .text-sektionen. Gennem denne automatiske kodeindsættelse hjælper Go med at frigøre udvikleren fra manuel håndtering.
Kun main-funktionen i Go
Lad os først skrive et simpelt eksempelprogram main.go og se på main fra en AMD64-maskine.
1package main
2
3func sayHello(msg string) {
4 println(msg)
5}
6
7func main() {
8 sayHello("Hello World")
9}
10
Derefter bygger vi således.
1go build main.go
2
Go understøtter go tool til nem lavniveau-fejlfinding. For kun at se så meget assembly som main-funktionen i main-pakken indeholder, indtaster man denne kommando.
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
- Der kontrolleres, om den nuværende goroutine-stackramme har tilstrækkelig plads, ved at sammenligne stackvagtværdien i goroutinens kontrolblokregister (R14) med den aktuelle stackpointer (SP) via CMPQ; hvis pladsen er utilstrækkelig, hoppes (JBE) til indgangspunktet 0x468f95 for stackudvidelse.
- PUSHQ BP gemmer den forrige basispointer på stakken.
- Den aktuelle stackpointer (SP) kopieres til basispointer-registret (BP), så stackens referencepunkt ved funktionens start fastholdes.
- Derefter allokeres 16 bytes lokalt variabelstakrum (SUBQ $0x10, SP), og NOPL bruges til at udfylde med virtuelle instruktioner for at udføre CPU-cache-justering.
- Go-runtime kalder runtime.printlock(SB) for at tage låsen og synkronisere standardoutput for strenge.
- LEAQ-instruktionen bruges til at gemme startadressen for den konstante streng ("Hello World") i AX, som ifølge Go ABI-specifikationen er det første parameterregister.
- Derefter gemmes strengens længde, som repræsenterer 11 i decimal, i BX, det andet parameterregister (MOVL $0xb, BX).
- runtime.printstring(SB) kaldes for at udskrive til konsollen på baggrund af de overførte AX (dataadresse) og BX (længde).
- runtime.printnl(SB) kaldes for at håndtere linjeskift.
- Da udskriften er færdig, frigives låsen via runtime.printunlock(SB).
- ADDQ $0x10, SP gendanner de 16 bytes stakhukommelse, der blev allokeret.
- POPQ BP gendanner den oprindelige basispointer.
- RET returnerer kontrollen til det sted, hvor funktionen blev kaldt.
- Hvis stakkontrollen i starten viste, at pladsen var utilstrækkelig, kaldes runtime.morestack_noctxt.abi0(SB) på adressen 0x468f95, så stack-runtime udvider stakken dynamisk, som det sker i managed sprog.
- Når stakudvidelsen er færdig, vender programmet tilbage til indgangspunktet for main.main(SB) (JMP). Som det fremgår, er assembly for forretningslogikken ret tydelig og har kun et tyndt lag af runtime-håndtering tilføjet.
Når der ikke er optimering
Ovenstående form er resultatet af, at Go-compileren automatisk inliner de to adskilte funktioner og optimerer dem. Til læringsformål vil vi dog i dette tilfælde ikke lade sayHello blive inlinet. For at opnå dette kompilerer vi kildekoden med følgende flag.
1go build -gcflags="-l" main.go
2
Hvis man udskriver resultatet i shellen, findes der duplikeret 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
Når inlining deaktiveres, indsættes MOVQ-instruktioner, som gemmer parametrene (AX, BX) igen i stakken via stakpointer-forskydninger såsom 0x20(SP), for at bevare parametrene i overensstemmelse med funktionskaldskonventionen. Det er dermed blevet bekræftet, at det, compileren optimerer, netop er disse unødvendige hukommelsesoverførsler og kaldoverhead.
Næste gang
Næste gang vil vi behandle if- og switch-sætninger i Go. Hvis der senere er tid, vil vi også analysere Go-runtime-sektionerne.