GoSuda

Structure d'exécution des langages managés et analyse de l'assembleur AMD64 des binaires Go

By Lee Yunjin views ...

Qu'est-ce qu'un langage managé ?

Un langage managé est un langage qui, contrairement aux langages non managés, c'est-à-dire ceux dont le programme exécute essentiellement la logique écrite par le programmeur sans s'en écarter beaucoup, exécute dans son runtime le GC, les optimisations à l'exécution, les green threads, la gestion de la concurrence, etc., afin que l'utilisateur n'ait pas à effectuer lui-même une gestion bas niveau dangereuse. Pour ce type de langage, il présente l'avantage de permettre de se concentrer uniquement sur la logique métier et de se plonger pleinement dans le développement ; en revanche, le comportement réel du programme peut différer de l'intuition du programmeur, si bien qu'un réglage fin du runtime est parfois nécessaire. Tout d'abord, parmi les langages managés, examinons le langage Go, qui est le plus fidèle à une philosophie minimaliste et dont l'assembleur est transparent.

Structure binaire du langage Go

.text.data.gopclntab, .typelink, etc.
Code machine à exécuterDonnées à stockerSections du runtime du langage

Le langage Go ne traduit pas en code machine de manière strictement 1:1 ce que l'utilisateur écrit ; la logique de la section .text est donc étroitement liée à la section du runtime du langage. De plus, des fonctions que l'utilisateur n'a pas écrites, comme runtime.printnl(), sont ajoutées à l'assembleur de la section .text. Grâce à cette insertion automatique de code, le langage Go aide le développeur à s'affranchir de la gestion manuelle.

Examen de la seule fonction main en Go

Commençons par écrire un exemple simple, main.go, et observons main sur une machine AMD64.

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

Ensuite, on compile avec la commande suivante.

1go build main.go
2

Go prend en charge go tool pour faciliter le débogage bas niveau. Pour n'afficher que l'assembleur de la fonction main du paquet main, on saisit la commande suivante.

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

Assembleur

 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
  • Le registre de bloc de contrôle de goroutine (R14) contient la valeur de garde de pile ; CMPQ compare cette valeur à le pointeur de pile courant (SP) afin de vérifier si l'espace de la trame de pile de la goroutine courante est suffisant. Si ce n'est pas le cas, un saut (JBE) est effectué vers l'adresse 0x468f95, point d'entrée de l'extension de pile.
  • PUSHQ BP empile l'ancien pointeur de base afin de le sauvegarder.
  • Le pointeur de base (BP) reçoit une copie du pointeur de pile courant (SP), ce qui fixe le repère de pile au début de la fonction.
  • Ensuite, un espace de variables locales de 16 octets est alloué (SUBQ $0x10, SP), et NOPL est utilisé pour insérer une instruction vide afin d'effectuer l'alignement sur le cache du CPU.
  • Pour synchroniser la sortie standard des chaînes internes du runtime Go, runtime.printlock(SB) est appelée afin de verrouiller.
  • L'instruction LEAQ place dans AX, premier paramètre selon la convention ABI de Go, l'adresse de début de la chaîne constante ("Hello World").
  • Ensuite, la valeur représentant la longueur de la chaîne est placée dans BX, second registre de paramètre (MOVL $0xb, BX, soit 11 en décimal).
  • runtime.printstring(SB) est appelée pour afficher dans la console à partir des informations transmises dans AX (adresse des données) et BX (longueur).
  • runtime.printnl(SB) est appelée pour gérer le retour à la ligne.
  • La sortie étant terminée, le verrou est libéré par runtime.printunlock(SB).
  • ADDQ $0x10, SP restaure la mémoire de pile de 16 octets allouée précédemment.
  • POPQ BP restaure l'ancien pointeur de base.
  • RET restitue le contrôle au point d'appel de la fonction.
  • Si la vérification initiale de la pile a révélé un espace insuffisant, runtime.morestack_noctxt.abi0(SB), située à l'adresse 0x468f95, est appelée afin d'étendre dynamiquement la pile du runtime, comme il convient à un langage managé.
  • Une fois l'extension de pile terminée, l'exécution reprend (JMP) au point d'entrée de main.main(SB). Comme on le voit, l'assembleur de la logique métier est assez clair et n'est accompagné que d'une fine couche de gestion par le runtime.

Cas sans optimisation

La forme ci-dessus résulte de l'inlining automatique, par le compilateur Go, des deux fonctions séparées. Toutefois, à des fins d'apprentissage, nous allons empêcher l'inlining de sayHello. Pour cela, on compile le code avec l'option suivante.

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

En affichant le résultat dans le shell, on constate la présence d'assembleur redondant.

 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

Lorsque l'inlining est désactivé, des instructions MOVQ sont insérées pour recharger les valeurs dans 0x20(SP), etc., afin de préserver les paramètres (AX, BX) conformément à la convention d'appel de la fonction. Ainsi, il est confirmé que ce que le compilateur optimise, ce sont ces opérations inutiles de déplacement en mémoire et ces surcoûts d'appel.

Prochaine fois

La prochaine fois, nous traiterons les instructions if et switch en Go. Si le temps le permet, nous analyserons également les sections du runtime de Go.