Que se passe-t-il lorsque vous «allez construire»?

Cet article est une traduction de Comment fonctionne «go build».

Comment «go build» compile-t-il le programme Golang le plus simple?

Cet article vise à répondre à cette question.

Considérez le programme le plus simple ci-dessous.

// main.go
package main

func main() {}

Lancer go build main.go imprime un fichier exécutable de 1,1 Mo main et ne fait rien. Qu'a fait go build pour créer ce binaire à ne rien faire?

La commande go build offre des options utiles.

  1. -work: go build crée un dossier temporaire pour vos fichiers de travail. Cet argument imprime l'emplacement de ce dossier et ne le supprime pas après la construction
  2. -a: Golang met en cache les paquets précédemment construits. -a fait que go build ignore le cache, donc la construction imprime toutes les étapes
  3. -p 1: Ceci définit le processus à effectuer dans un seul thread et enregistre la sortie de manière linéaire.
  4. -x: go build est un wrapper pour d'autres outils Golang tels que compile. -x affiche les commandes et arguments envoyés à ces outils

Lancer go build -work -a -p 1 -x main.go vous donnera beaucoup de journaux ainsi que main, et ce que vous faites lorsque vous créez main avec build Va nous dire.

Le journal génère d'abord le contenu suivant.

WORK=/var/folders/rw/gtb29xf92fv23f0zqsg42s840000gn/T/go-build940616988

Il s'agit d'un répertoire de travail avec une structure similaire à la suivante.

├── b001
│   ├── _pkg_.a
│   ├── exe
│   ├── importcfg
│   └── importcfg.link
├── b002
│   └── ...
├── b003
│   └── ...
├── b004
│   └── ...
├── b006
│   └── ...
├── b007
│   └── ...
└── b008
    └── ...

go build définit un graphe d'action pour la tâche à accomplir.

Chaque action de ce graphe obtient son propre sous-répertoire (défini dans NewObjdir).

Le premier nœud du graphe, «b001», est la tâche racine pour la compilation du binaire principal.

Le nombre d'actions dépendantes est grand et se termine par «b008». (Je ne sais pas où est allé b005, mais je ne pense pas que ce soit un problème, je vais donc l'omettre.)

b008

La première action à entreprendre est «b008» à la fin du graphique.

mkdir -p $WORK/b008/

cat >$WORK/b008/importcfg << 'EOF'
# import config
EOF

cd /<..>/src/runtime/internal/sys

/<..>/compile 
  -o $WORK/b008/_pkg_.a 
  -trimpath "$WORK/b008=>" 
  -p runtime/internal/sys 
  -std 
  -+ 
  -complete 
  -buildid gEtYPexVP43wWYWCxFKi/gEtYPexVP43wWYWCxFKi 
  -goversion go1.14.7 
  -D "" 
  -importcfg $WORK/b008/importcfg 
  -pack 
  -c=16 
  ./arch.go ./arch_amd64.go ./intrinsics.go ./intrinsics_common.go ./stubs.go ./sys.go ./zgoarch_amd64.go ./zgoos_darwin.go ./zversion.go

/<..>/buildid -w $WORK/b008/_pkg_.a

cp $WORK/b008/_pkg_.a /<..>/Caches/go-build/01/01b...60a-d

Dans b008

  1. Créez un répertoire d'actions (cette description est omise ci-après car toutes les actions le font)
  2. Créez un fichier importcfg à utiliser avec l'outil compile (vide)
  3. Remplacez le répertoire par le dossier source du package runtime / internal / sys. Ce package contient des constantes utilisées lors de l'exécution
  4. Compilez le package
  5. Utilisez build id pour écrire les métadonnées dans le package ( -w) et copiez le package dans le cache go-build (tous les packages sont mis en cache, donc cette description est omise ci-après) Faire)

Décomposons cela en arguments envoyés à l'outil compile (également expliqué dans go tool compile --help).

  1. -o Fichier de destination de sortie
  2. Supprimez le préfixe$ WORK / b008 =>from -trimpath chemin du fichier source
  3. Définissez le nom du package utilisé dans -p`` import
  4. -std`` compilant la bibliothèque standard (je n'étais pas sûr à ce stade)
  5. - + compilation runtime (je ne le savais pas non plus)
  6. Le compilateur -complete produit le package complet, pas C ou l'assembly
  7. Donnez aux métadonnées -build id un identifiant de construction
  8. -goversion La version requise pour le paquet compilé
  9. -D Le chemin relatif utilisé pour l'importation locale est" "
  10. -importcfg Référez-vous à d'autres packages pour importer le fichier de configuration
  11. Créez le package -pack comme une archive .a au lieu du fichier objet .o
  12. -c Combien traiter en parallèle au moment de la construction
  13. Liste des fichiers du package

La plupart de ces arguments sont les mêmes pour toutes les commandes compile, nous allons donc omettre cette description ci-dessous.

La sortie de b008 est un fichier d'archive appelé $ WORK / b008 / _pkg_.a qui correspond à runtime / internal / sys.

buildid

Laissez-moi vous expliquer ce qu'est un "build id".

Le format de buildid est <actionid> / <contentid>.

Il est utilisé comme index pour mettre en cache les paquets et améliorer les performances de go build.

<actionid> est le hachage de l'action (tous les appels, arguments et fichiers d'entrée). <contentid> est le hachage du fichier .a de sortie.

Pour chaque action «go build», vous pouvez rechercher dans le cache du contenu créé par une autre action avec le même «».

Ceci est implémenté dans buildid.go.

Le buildid est stocké dans le fichier sous forme de métadonnées, vous n'avez donc pas à le hacher à chaque fois pour obtenir le <contentid>. Vous pouvez trouver cet ID avec go tool buildid <file> (cela fonctionne également en binaire).

Dans le journal b008 ci-dessus, le buildID est défini par l'outil compile comme gEtYPexVP43wWYWCxFKi / gEtYPexVP43wWYWCxFKi.

Ceci est juste un espace réservé et sera écrasé par le bon gEtYPexVP43wWYWCxFKi / b-rPboOuD0POrlJWPTEi avec go tool buildid -w avant d'être mis en cache plus tard.

b007

Vient ensuite b007

cat >$WORK/b007/importcfg << 'EOF'
# import config
packagefile runtime/internal/sys=$WORK/b008/_pkg_.a
EOF

cd /<..>/src/runtime/internal/math

/<..>/compile 
  -o $WORK/b007/_pkg_.a 
  -p runtime/internal/math 
  -importcfg $WORK/b007/importcfg 
  ...
  ./math.go
  1. Je crée un importcfg qui dit packagefile runtime / internal / sys = $ WORK / b008 / _pkg_.a Cela indique que b007 dépend de b008
  2. Compilez runtime / internal / math Si vous regardez à l'intérieur math.go, vous importez sûrement runtime / internal / sys fait avec b008.

La sortie de b007 est un fichier d'archive appelé $ WORK / b007 / _pkg_.a qui correspond à runtime / internal / math.

b006

cat >$WORK/b006/go_asm.h << 'EOF'
EOF

cd /<..>/src/runtime/internal/atomic

/<..>/asm 
  -I $WORK/b006/ 
  -I /<..>/go/1.14.7/libexec/pkg/include 
  -D GOOS_darwin 
  -D GOARCH_amd64 
  -gensymabis 
  -o $WORK/b006/symabis 
  ./asm_amd64.s

/<..>/asm 
  -I $WORK/b006/ 
  -I /<..>/go/1.14.7/libexec/pkg/include 
  -D GOOS_darwin 
  -D GOARCH_amd64 
  -o $WORK/b006/asm_amd64.o 
  ./asm_amd64.s

cat >$WORK/b006/importcfg << 'EOF'
# import config
EOF

/<..>/compile 
  -o $WORK/b006/_pkg_.a 
  -p runtime/internal/atomic 
  -symabis $WORK/b006/symabis 
  -asmhdr $WORK/b006/go_asm.h 
  -importcfg $WORK/b006/importcfg
  ...
  ./atomic_amd64.go ./stubs.go

/<..>/pack r $WORK/b006/_pkg_.a $WORK/b006/asm_amd64.o

Maintenant sortons du fichier .go normal et commençons à traiter le fichier .s` de l'assembly Go de bas niveau.

  1. Créez un fichier d'en-tête go_asm.h
  2. Passez au package runtime / internal / atomic des fonctions de bas niveau
  3. Exécutez l'outil go tool asm (décrit dans go tool asm --help) pour créer le fichier symabis "Symbol Application Binary Interfaces (ABI)", puis le fichier objetasm_amd64.o Créer
  4. Utilisez compile pour créer un fichier _pkg_.a avec un fichier symabis et un en-tête contenant -asmhdr
  5. Ajoutez asm_amd64.o à _pkg_.a avec la commande pack

L'outil asm est appelé ici avec les arguments suivants:

  1. -I: Inclut les actions b007 et le dossier libexec / pkg / includes. includes a trois fichiers asm_ppc64x.h, funcdata.h et textflag.h, tous avec des définitions de fonctions de bas niveau. Par exemple, FIXED_FRAME définit la taille de la partie fixe du cadre de pile
  2. -D: Comprend des symboles prédéfinis
  3. -gensymabis: Créer un fichier symabis
  4. -o: fichier de destination de sortie

La sortie de b006 est un fichier d'archive appelé $ WORK / b006 / _pkg_.a qui correspond à runtime / internal / atomic.

b004

cd /<..>/src/internal/cpu

/<..>/asm ... -o $WORK/b004/symabis ./cpu_x86.s
/<..>/asm ... -o $WORK/b004/cpu_x86.o ./cpu_x86.s

/<..>/compile ... -o $WORK/b004/_pkg_.a ./cpu.go ./cpu_amd64.go ./cpu_x86.go

/<..>/pack r $WORK/b004/_pkg_.a $WORK/b004/cpu_x86.o

«b004» est le même que «b006» sauf que la cible a changé en «internal / cpu».

Créez d'abord le symabis et le fichier objet en assemblant cpu_x86.s, compilez le fichier go, puis combinez-les pour créer l'archive _pkg_.a.

La sortie de b004 est un fichier archive appelé $ WORK / b004 / _pkg_.a correspondant à internal / cpu.

b003

cat >$WORK/b003/go_asm.h << 'EOF'
EOF

cd /<..>/src/internal/bytealg

/<..>/asm ... -o $WORK/b003/symabis ./compare_amd64.s ./count_amd64.s ./equal_amd64.s ./index_amd64.s ./indexbyte_amd64.s

cat >$WORK/b003/importcfg << 'EOF'
# import config
packagefile internal/cpu=$WORK/b004/_pkg_.a
EOF

/<..>/compile ... -o $WORK/b003/_pkg_.a -p internal/bytealg ./bytealg.go ./compare_native.go ./count_native.go ./equal_generic.go ./equal_native.go ./index_amd64.go ./index_native.go ./indexbyte_native.go

/<..>/asm ... -o $WORK/b003/compare_amd64.o ./compare_amd64.s
/<..>/asm ... -o $WORK/b003/count_amd64.o ./count_amd64.s
/<..>/asm ... -o $WORK/b003/equal_amd64.o ./equal_amd64.s
/<..>/asm ... -o $WORK/b003/index_amd64.o ./index_amd64.s
/<..>/asm ... -o $WORK/b003/indexbyte_amd64.o ./indexbyte_amd64.s

/<..>/pack r $WORK/b003/_pkg_.a $WORK/b003/compare_amd64.o $WORK/b003/count_amd64.o $WORK/b003/equal_amd64.o $WORK/b003/index_amd64.o $WORK/b003/indexbyte_amd64.o

Faire «b003» équivaut à «b004» et «b006».

Le principal problème avec ce paquet est qu'il existe plusieurs fichiers .s pour créer de nombreux fichiers objets .o, dont chacun doit être ajouté au fichier _pkg_.a.

La sortie de b003 est un fichier d'archive appelé $ WORK / b003 / _pkg_.a qui correspond à internal / bytealg.

b002

cat >$WORK/b002/go_asm.h << 'EOF'
EOF

cd /<..>/src/runtime

/<..>/asm 
  ... 
  -o $WORK/b002/symabis 
  ./asm.s ./asm_amd64.s ./duff_amd64.s ./memclr_amd64.s ./memmove_amd64.s ./preempt_amd64.s ./rt0_darwin_amd64.s ./sys_darwin_amd64.s
  
cat >$WORK/b002/importcfg << 'EOF'
# import config
packagefile internal/bytealg=$WORK/b003/_pkg_.a
packagefile internal/cpu=$WORK/b004/_pkg_.a
packagefile runtime/internal/atomic=$WORK/b006/_pkg_.a
packagefile runtime/internal/math=$WORK/b007/_pkg_.a
packagefile runtime/internal/sys=$WORK/b008/_pkg_.a
EOF

/<..>/compile 
  -o $WORK/b002/_pkg_.a 
  ...
  -p runtime 
  ./alg.go ./atomic_pointer.go ./cgo.go ./cgocall.go ./cgocallback.go ./cgocheck.go ./chan.go ./checkptr.go ./compiler.go ./complex.go ./cpuflags.go ./cpuflags_amd64.go ./cpuprof.go ./cputicks.go ./debug.go ./debugcall.go ./debuglog.go ./debuglog_off.go ./defs_darwin_amd64.go ./env_posix.go ./error.go ./extern.go ./fastlog2.go ./fastlog2table.go ./float.go ./hash64.go ./heapdump.go ./iface.go ./lfstack.go ./lfstack_64bit.go ./lock_sema.go ./malloc.go ./map.go ./map_fast32.go ./map_fast64.go ./map_faststr.go ./mbarrier.go ./mbitmap.go ./mcache.go ./mcentral.go ./mem_darwin.go ./mfinal.go ./mfixalloc.go ./mgc.go ./mgcmark.go ./mgcscavenge.go ./mgcstack.go ./mgcsweep.go ./mgcsweepbuf.go ./mgcwork.go ./mheap.go ./mpagealloc.go ./mpagealloc_64bit.go ./mpagecache.go ./mpallocbits.go ./mprof.go ./mranges.go ./msan0.go ./msize.go ./mstats.go ./mwbbuf.go ./nbpipe_pipe.go ./netpoll.go ./netpoll_kqueue.go ./os_darwin.go ./os_nonopenbsd.go ./panic.go ./plugin.go ./preempt.go ./preempt_nonwindows.go ./print.go ./proc.go ./profbuf.go ./proflabel.go ./race0.go ./rdebug.go ./relax_stub.go ./runtime.go ./runtime1.go ./runtime2.go ./rwmutex.go ./select.go ./sema.go ./signal_amd64.go ./signal_darwin.go ./signal_darwin_amd64.go ./signal_unix.go ./sigqueue.go ./sizeclasses.go ./slice.go ./softfloat64.go ./stack.go ./string.go ./stubs.go ./stubs_amd64.go ./stubs_nonlinux.go ./symtab.go ./sys_darwin.go ./sys_darwin_64.go ./sys_nonppc64x.go ./sys_x86.go ./time.go ./time_nofake.go ./timestub.go ./trace.go ./traceback.go ./type.go ./typekind.go ./utf8.go ./vdso_in_none.go ./write_err.go
  
/<..>/asm ... -o $WORK/b002/asm.o ./asm.s
/<..>/asm ... -o $WORK/b002/asm_amd64.o ./asm_amd64.s
/<..>/asm ... -o $WORK/b002/duff_amd64.o ./duff_amd64.s
/<..>/asm ... -o $WORK/b002/memclr_amd64.o ./memclr_amd64.s
/<..>/asm ... -o $WORK/b002/memmove_amd64.o ./memmove_amd64.s
/<..>/asm ... -o $WORK/b002/preempt_amd64.o ./preempt_amd64.s
/<..>/asm ... -o $WORK/b002/rt0_darwin_amd64.o ./rt0_darwin_amd64.s
/<..>/asm ... -o $WORK/b002/sys_darwin_amd64.o ./sys_darwin_amd64.s
  
/<..>/pack r $WORK/b002/_pkg_.a $WORK/b002/asm.o $WORK/b002/asm_amd64.o $WORK/b002/duff_amd64.o $WORK/b002/memclr_amd64.o $WORK/b002/memmove_amd64.o $WORK/b002/preempt_amd64.o $WORK/b002/rt0_darwin_amd64.o $WORK/b002/sys_darwin_amd64.o

Vous pouvez voir pourquoi les actions précédentes étaient nécessaires en regardant b002.

b002 contient tous les packages d'exécution nécessaires pour exécuter les binaires de Go. Par exemple, b002 contient également une implémentation Go GC appelée mgc.go. Il importe «b004» («internal / cpu») et «b006» («runtime / internal / atomic»).

«b002» est peut-être le paquet le plus complexe de la bibliothèque principale, mais la construction elle-même est le même processus qu'avant. En d'autres termes, le fichier produit par asm et compile est packed to _pkg_.a.

La sortie de b002 est un fichier d'archive appelé $ WORK / b002 / _pkg_.a qui correspond à runtime.

b001

cat >$WORK/b001/importcfg << 'EOF'
# import config
packagefile runtime=$WORK/b002/_pkg_.a
EOF

cd /<..>/main

/<..>/compile ... -o $WORK/b001/_pkg_.a -p main ./main.go

cat >$WORK/b001/importcfg.link << 'EOF'
packagefile command-line-arguments=$WORK/b001/_pkg_.a
packagefile runtime=$WORK/b002/_pkg_.a
packagefile internal/bytealg=$WORK/b003/_pkg_.a
packagefile internal/cpu=$WORK/b004/_pkg_.a
packagefile runtime/internal/atomic=$WORK/b006/_pkg_.a
packagefile runtime/internal/math=$WORK/b007/_pkg_.a
packagefile runtime/internal/sys=$WORK/b008/_pkg_.a
EOF

/<..>/link 
  -o $WORK/b001/exe/a.out 
  -importcfg $WORK/b001/importcfg.link 
  -buildmode=exe 
  -buildid=yC-qrh2sY_qI0zh2-NE7/owNzOBTqPO00FkqK0_lF/HPXqvMz_4PvKsQzqGWgD/yC-qrh2sY_qI0zh2-NE7 
  -extld=clang 
  $WORK/b001/_pkg_.a

mv $WORK/b001/exe/a.out main

First it builds an importcfg that includes runtime built in b002 to then compile main.go to pkg.a

  1. Commencez par créer un importcfg qui inclut le runtime de b002, puis compilez main.go pour créer un _pkg_.a.
  2. Créez un importcfg.link qui contient arguments de ligne de commande = $ WORK / b001 / _pkg_.a en plus de tous les paquets qui apparaissaient auparavant, et liez-les avec la commande link pour exécuter le fichier. Créer un.
  3. Enfin, renommez-le en main et déplacez-vous vers la destination de sortie.

Complétons l'argument de link.

  1. -buildmode: Construisez le fichier exécutable
  2. -extld: se référer à un éditeur de liens externe

J'ai enfin obtenu ce que je cherchais.

Le binaire «main» est né de «b001».

Similitudes avec Bazel

La création de graphiques d'action pour une mise en cache efficace est la même idée que les outils de construction que Bazel utilise pour les versions rapides.

Les "identifiants d'action" et "id de contenu" de Golang correspondent au "cache d'action" et au "magasin adressable par le contenu (CAS)" que Bazel utilise dans le cache.

Bazel est un produit Google, tout comme Golang. Il serait très raisonnable pour eux d'avoir une philosophie similaire sur la façon de créer des logiciels rapidement et avec précision.

Dans le package rules_go de Bazel, vous pouvez voir comment réimplémenter go build dans le code builder.

Il s'agit d'une implémentation très propre, car les graphiques d'action, la gestion des dossiers et la mise en cache sont gérés en externe par Bazel.

À l'étape suivante

go build a fait beaucoup pour le compiler, même avec un programme à ne rien faire comme celui-ci.

Je n'ai pas donné trop de détails sur l'outil (compile`` asm) et ses fichiers d'entrée et de sortie ( .a`` .o .s).

De plus, cette fois, je ne fais que compiler le programme le plus basique.

Vous pouvez rendre la compilation plus compliquée en procédant comme suit:

  1. Importer d'autres packages Par exemple, importer fmt dans la sortie Hello world ajoutera 23 actions supplémentaires au graphique d'action.
  2. Utilisez go.mod pour référencer des packages externes
  3. Construire pour d'autres architectures en changeant les valeurs de GOOS et GOARCH Par exemple, lors de la compilation pour wasm, le contenu des actions et des arguments est complètement différent.

Lancer go build et inspecter les journaux est une approche descendante pour apprendre comment fonctionne le compilateur Go. Si vous voulez apprendre des bases, c'est un excellent point de départ pour plonger dans des ressources telles que:

  1. Introduction to the Go compiler
  2. Go: Overview of the Compiler
  3. Go at Google: Language Design in the Service of Software Engineering
  4. build.go
  5. compile/main.go

References

Recommended Posts

Que se passe-t-il lorsque vous «allez construire»?
[Go] Test d'exécution / construction / package
Lorsque pyenv installe BUILD FAILED
Construction de l'environnement, construction -Go-