[GO] Was passiert, wenn Sie "bauen" gehen?

Dieser Artikel ist eine Übersetzung von How “go build” Works.

Wie kompiliert "go build" das einfachste Golang-Programm?

Dieser Artikel soll diese Frage beantworten.

Betrachten Sie das einfachste Programm unten.

// main.go
package main

func main() {}

Wenn Sie "go build main.go" ausführen, wird eine ausführbare Datei mit 1,1 MB "main" gedruckt und nichts unternommen. Was hat go build getan, um diese Nichtstun-Binärdatei zu erstellen?

Der Befehl go build bietet einige nützliche Optionen.

  1. -work: go build erstellt einen temporären Ordner für Ihre Arbeitsdateien. Dieses Argument gibt den Speicherort dieses Ordners aus und entfernt ihn nach dem Erstellen nicht
  2. -a: Golang speichert zuvor erstellte Pakete zwischen. -a bewirkt, dass go build den Cache ignoriert, sodass der Build alle Schritte druckt
  3. -p 1: Hiermit wird der in einem einzelnen Thread auszuführende Prozess festgelegt und die Ausgabe linear protokolliert.
  4. -x: go build ist ein Wrapper für andere Golang-Tools wie compile. -x druckt die an diese Tools gesendeten Befehle und Argumente

Wenn Sie "go build -work -a -p 1 -x main.go" ausführen, erhalten Sie viele Protokolle sowie "main". Dies tun Sie, wenn Sie "main" mit "build" erstellen. Wird es uns sagen.

Das Protokoll gibt zuerst den folgenden Inhalt aus.

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

Dies ist ein Arbeitsverzeichnis mit einer ähnlichen Struktur wie die folgende.

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

go build definiert ein Aktionsdiagramm für die Aufgabe, die abgeschlossen werden muss.

Jede Aktion in diesem Diagramm erhält ein eigenes Unterverzeichnis (definiert in "NewObjdir").

Der erste Knoten im Diagramm, "b001", ist die Stammaufgabe zum Kompilieren der Hauptbinärdatei.

Die Anzahl der abhängigen Aktionen ist groß und endet mit "b008". (Ich weiß nicht, wohin b005 gegangen ist, aber ich denke nicht, dass es ein Problem ist, also werde ich es weglassen.)

b008

Die erste Aktion, die ausgeführt werden muss, ist "b008" am Ende des Diagramms.

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

In b008

  1. Erstellen Sie ein Aktionsverzeichnis (diese Beschreibung wird im Folgenden weggelassen, da alle Aktionen dies tun).
  2. Erstellen Sie eine "importcfg" -Datei zur Verwendung mit dem Tool "compile" (leer).
  3. Ändern Sie das Verzeichnis in den Quellordner für das Paket runtime / internal / sys. Dieses Paket enthält Konstanten, die zur Laufzeit verwendet werden
  4. Kompilieren Sie das Paket
  5. Verwenden Sie build id, um die Metadaten in das Paket zu schreiben ( -w) und kopieren Sie das Paket in den go-build-Cache (alle Pakete werden zwischengespeichert, daher wird diese Beschreibung im Folgenden weggelassen). Machen)

Lassen Sie uns dies in die Argumente aufteilen, die an das Tool compile gesendet werden (auch erklärt in go tool compile --help).

  1. -o Zieldatei ausgeben
  2. Entfernen Sie das Präfix "$ WORK / b008 =>" aus dem Quelldateipfad "-trimpath"
  3. Legen Sie den Paketnamen fest, der beim Import von "-p" verwendet wird
  4. -std`` Kompilieren der Standardbibliothek (ich war mir zu diesem Zeitpunkt nicht sicher)
  5. - + Laufzeit kompilieren (das wusste ich auch nicht)
  6. Der Compiler -complete gibt das vollständige Paket aus, nicht C oder Assembly
  7. Geben Sie den Metadaten "-build id" eine Build-ID
  8. -goversion Die für das kompilierte Paket erforderliche Version
  9. -D Der relative Pfad für den lokalen Import ist" "
  10. -importcfg Informationen zum Importieren der Konfigurationsdatei finden Sie in anderen Paketen
  11. Erstellen Sie das Paket "-pack" als Archiv ".a" anstelle der Objektdatei ".o"
  12. -c Wie viel zur Erstellungszeit parallel verarbeitet werden soll
  13. Liste der Dateien im Paket

Die meisten dieser Argumente sind für alle Kompilierungsbefehle gleich, daher werden wir diese Beschreibung unten weglassen.

Die Ausgabe von b008 ist eine Archivdatei mit dem Namen $ WORK / b008 / _pkg_.a, die runtime / internal / sys entspricht.

buildid

Lassen Sie mich erklären, was eine Build-ID ist.

Das Format von "buildid" ist " / ".

Es wird als Index verwendet, um Pakete zwischenzuspeichern und die Leistung von "go build" zu verbessern.

<Aktions-ID> ist der Hash der Aktion (alle Aufrufe, Argumente und Eingabedateien). "" ist der Hash der Ausgabedatei ".a".

Für jede "go build" -Aktion können Sie den Cache nach Inhalten durchsuchen, die von einer anderen Aktion mit derselben "" erstellt wurden.

Dies ist in buildid.go implementiert.

Die buildid wird in der Datei als Metadaten gespeichert, sodass Sie sie nicht jedes Mal hashen müssen, um die <contentid> zu erhalten. Sie finden diese ID mit go tool buildid <file> (sie funktioniert auch binär).

Im obigen "b008" -Protokoll wird die "buildID" vom "compile" -Tool als "gEtYPexVP43wWYWCxFKi / gEtYPexVP43wWYWCxFKi" festgelegt.

Dies ist nur ein Platzhalter und wird mit dem korrekten gEtYPexVP43wWYWCxFKi / b-rPboOuD0POrlJWPTEi mit go tool buildid -w überschrieben, bevor es später zwischengespeichert wird.

b007

Als nächstes kommt 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. Ich erstelle ein "importcfg" mit der Aufschrift "packagefile runtime / internal / sys = $ WORK / b008 / pkg.a". Dies zeigt an, dass "b007" von "b008" abhängt
  2. Kompilieren Sie runtime / internal / math Wenn Sie in math.go schauen, importieren Sie sicherlich runtime / internal / sys made with b008.

Die Ausgabe von b007 ist eine Archivdatei mit dem Namen $ WORK / b007 / _pkg_.a, die runtime / internal / math entspricht.

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

Lassen Sie uns nun aus der regulären .go-Datei ausbrechen und mit der Verarbeitung der .s`-Datei der Go-Assembly auf niedriger Ebene beginnen.

  1. Erstellen Sie eine Header-Datei go_asm.h
  2. Wechseln Sie zum Paket runtime / internal / atomic mit Funktionen auf niedriger Ebene
  3. Führen Sie das Tool "go tool asm" (beschrieben in "go tool asm --help") aus, um die "symabis" -Datei "Symbol Application Binary Interfaces (ABI)" und anschließend die Objektdatei "asm_amd64.o" zu erstellen Erstellen Sie `
  4. Verwenden Sie compile, um eine _pkg_.a-Datei mit einer symabis-Datei und einem Header zu erstellen, der -asmhdr enthält
  5. Fügen Sie mit dem Befehl pack`` asm_amd64.o zu _pkg_.a hinzu

Das Tool "asm" wird hier mit den folgenden Argumenten aufgerufen:

  1. -I: Fügen Sie die Aktionen b007 und den Ordner libexec / pkg / includes ein. include hat drei Dateien asm_ppc64x.h, funcdata.h und textflag.h, alle mit Funktionsdefinitionen auf niedriger Ebene. Beispielsweise definiert FIXED_FRAME die Größe des festen Teils des Stapelrahmens
  2. -D: Enthält vordefinierte Symbole
  3. -gensymabis: Erstellt eine symabis-Datei
  4. -o: Zieldatei ausgeben

Die Ausgabe von b006 ist eine Archivdatei mit dem Namen $ WORK / b006 / _pkg_.a, die runtime / internal / atomic entspricht.

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 ist dasselbe wie b006, außer dass das Ziel in internal / cpu geändert wurde.

Erstellen Sie zuerst die "Symabis" und die Objektdatei, indem Sie "cpu_x86.s" zusammenstellen, kompilieren Sie die go-Datei und kombinieren Sie sie dann, um das Archiv "pkg.a" zu erstellen.

Die Ausgabe von b004 ist eine Archivdatei mit dem Namen $ WORK / b004 / _pkg_.a, die internal / cpu entspricht.

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

"B003" ist dasselbe wie "b004" und "b006".

Das Hauptproblem bei diesem Paket besteht darin, dass es mehrere ".s" -Dateien gibt, um viele Objektdateien ".o" zu erstellen, von denen jede zur "pkg.a" -Datei hinzugefügt werden muss.

Die Ausgabe von b003 ist eine Archivdatei mit dem Namen $ WORK / b003 / _pkg_.a, die internal / bytealg entspricht.

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

Sie können sehen, warum die vorherigen Aktionen benötigt wurden, indem Sie sich "b002" ansehen.

b002 enthält alle Laufzeitpakete, die zum Ausführen der Go-Binärdateien erforderlich sind. Zum Beispiel enthält "b002" auch eine Go GC-Implementierung namens "mgc.go". Es importiert "b004" ("intern / CPU") und "b006" ("Laufzeit / intern / atomar").

b002 ist vielleicht das komplexeste Paket in der Kernbibliothek, aber der Build selbst ist der gleiche Prozess wie zuvor. Mit anderen Worten, die von "asm" und "compile" ausgegebene Datei wird in "pkg.a" gepackt.

Die Ausgabe von "b002" ist eine Archivdatei mit dem Namen "$ WORK / b002 / pkg.a", die "Laufzeit" entspricht.

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. Erstellen Sie zuerst ein "importcfg", das die "Laufzeit" von "b002" enthält, und kompilieren Sie dann "main.go", um ein "pkg.a" zu erstellen.
  2. Erstellen Sie einen importcfg.link, der zusätzlich zu allen zuvor angezeigten Paketen Befehlszeilenargumente = $ WORK / b001 / _pkg_.a enthält, und verknüpfen Sie sie mit dem Befehl link, um die Datei auszuführen. Ein ... kreieren.
  3. Benennen Sie es schließlich in "main" um und wechseln Sie zum Ausgabeziel.

Ergänzen wir das Argument von "Link".

  1. -buildmode: Erstellt die ausführbare Datei
  2. -extld: Beziehen Sie sich auf einen externen Linker

Ich habe endlich bekommen, wonach ich gesucht habe.

Die "Haupt" -Binärdatei wird aus "b001" geboren.

Ähnlichkeiten mit Bazel

Das Erstellen von Aktionsdiagrammen für ein effizientes Caching ist dieselbe Idee wie die Build-Tools, die Bazel für schnelle Builds verwendet.

Golangs "Aktions-ID" und "Inhalts-ID" entsprechen dem "Aktions-Cache" und dem "Content-Addressable Store (CAS)", die Bazel im Cache verwendet.

Bazel ist ein Google-Produkt, ebenso wie Golang. Es wäre sehr vernünftig für sie, eine ähnliche Philosophie zu haben, wie man Software schnell und genau erstellt.

In Bazels rules_go -Paket können Sie sehen, wie Sie go build im builder-Code erneut implementieren.

Dies ist eine sehr saubere Implementierung, da Aktionsdiagramme, Ordnerverwaltung und Caching von Bazel extern verwaltet werden.

Zum nächsten Schritt

go build hat viel getan, um es zu kompilieren, selbst mit einem Do-Nothing-Programm wie diesem.

Ich habe nicht zu sehr auf das Tool (compile`` asm) und seine Eingabe- und Ausgabedateien ( .a`` .o .s) eingegangen.

Außerdem kompiliere ich diesmal nur das grundlegendste Programm.

Sie können die Kompilierung komplizierter gestalten, indem Sie folgende Schritte ausführen:

  1. Andere Pakete importieren Wenn Sie beispielsweise "fmt" in die Ausgabe "Hello world" importieren, werden dem Aktionsdiagramm 23 weitere Aktionen hinzugefügt.
  2. Verwenden Sie go.mod, um auf externe Pakete zu verweisen
  3. Erstellen Sie für andere Architekturen, indem Sie die Werte von "GOOS" und "GOARCH" ändern. Wenn Sie beispielsweise für "wasm" kompilieren, sind die Inhalte von Aktionen und Argumenten völlig unterschiedlich.

Das Ausführen von "go build" und das Überprüfen der Protokolle ist ein Top-Down-Ansatz, um zu lernen, wie der Go-Compiler funktioniert. Wenn Sie aus den Grundlagen lernen möchten, ist dies ein guter Ausgangspunkt, um in Ressourcen wie:

  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

Was passiert, wenn Sie "bauen" gehen?
[Go] Ausführung / Build / Pakettest
Wenn pyenv BUILD FAILED installiert
Umweltbau, Build -Go-