Erste Schritte mit Go Assembly

Dieser Artikel ist eine Übersetzung und Ergänzung von Kapitel I: Eine Einführung in die Baugruppe.

Dieser Artikel geht von folgenden Personen aus:

Umgebung

$ go version
go version go1.10 linux/amd64

Pseudo-Assemblierung

Die vom Go-Compiler ausgegebene Assembly ist eine Abstraktion und nicht der tatsächlichen Hardware zugeordnet. Der Go-Assembler übersetzt diese Pseudo-Assembler in eine Maschinensprache, die der Zielhardware entspricht.

Es könnte hilfreich sein, sich so etwas wie Java-Bytecode vorzustellen.

Der größte Vorteil einer solchen Zwischenschicht besteht darin, dass die Anpassung an neue Architekturen erleichtert wird. Weitere Informationen finden Sie unter [* Das Design des Go Assembler *] von Rob Pike (https://talks.golang.org/2016/asm.slide#1).

Das Wichtigste, was Sie über Go-Assemblys wissen müssen, ist die Tatsache, dass Go-Assemblys nicht direkt der Zielhardware entsprechen. Einige sind direkt an die Hardware gebunden, andere nicht. Dadurch muss der Assembler die Pipeline nicht mehr übergeben. Stattdessen kann der Compiler die Pseudo-Assembly verarbeiten, die diese Hardware abstrahiert, und die Anweisungsauswahl (in diesem Fall die Go-Assembly zur eigentlichen Assembly). Die Konvertierung nach) erfolgt nun teilweise nach der Codegenerierung (der Generierung der Go-Assembly durch den Generator). Als Beispiel einer Pseudoanordnung kann der MOV-Befehl der GO-Anordnung in einen "Löschen" - oder "Laden" -Befehl usw. umgewandelt werden, oder er kann abhängig von der Architektur unverändert bleiben (obwohl sich der Name ändern kann). Während gängige Architekturkonzepte wie Speicherbewegungen und Aufrufe und Rückgaben von Unterprogrammen abstrahiert werden, werden hardwarespezifische Anweisungen häufig unverändert dargestellt.

Der Go-Assembler ist ein Programm, das diese Pseudo-Assembly analysiert und in Anweisungen zur Eingabe in den Linker konvertiert.

Beispiel mit einem einfachen Programm

Betrachten Sie den folgenden Code.

//go:noinline
func add(a, b int32) (int32, bool) { return a + b, true }

func main() { add(10, 32) }

Lassen Sie uns diesen Code zu einer Assembly kompilieren.

$ GOOS=linux GOARCH=amd64 go tool compile -S direct_topfunc_call.go
0x0000 TEXT		"".add(SB), NOSPLIT, $0-16
  0x0000 FUNCDATA	$0, gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB)
  0x0000 FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
  0x0000 MOVL		"".b+12(SP), AX
  0x0004 MOVL		"".a+8(SP), CX
  0x0008 ADDL		CX, AX
  0x000a MOVL		AX, "".~r2+16(SP)
  0x000e MOVB		$1, "".~r3+20(SP)
  0x0013 RET

0x0000 TEXT		"".main(SB), $24-0
  ;; ...omitted stack-split prologue...
  0x000f SUBQ		$24, SP
  0x0013 MOVQ		BP, 16(SP)
  0x0018 LEAQ		16(SP), BP
  0x001d FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
  0x001d FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
  0x001d MOVQ		$137438953482, AX
  0x0027 MOVQ		AX, (SP)
  0x002b PCDATA		$0, $0
  0x002b CALL		"".add(SB)
  0x0030 MOVQ		16(SP), BP
  0x0035 ADDQ		$24, SP
  0x0039 RET
  ;; ...omitted stack-split epilogue...

Dissecting add

0x0000 TEXT "".add(SB), NOSPLIT, $0-16

--0x0000: Wird relativ zum Beginn der Befehlsoffsetfunktion dargestellt.

--TEXT "". Add: Die Direktive TEXT gibt an, dass das Symbol" ".add im Abschnitt .text enthalten ist und dass sich die folgenden Anweisungen in dieser Funktion befinden. Die leere Zeichenfolge "" "wird zum Zeitpunkt der Verknüpfung durch den aktuellen Paketnamen ersetzt. Diesmal wird es "main.add" sein.

--(SB): SB ist ein virtuell definiertes Register in der Go-Assembly, bei dem es sich um einen "Static-Base" -Zeiger handelt. Es stellt den Beginn des Programmadressraums dar. Das "". Add (SB) "zeigt an, dass das" ". Add" -Symbol einen konstanten Versatz aufweist, der vom Linker ab dem Beginn des Adressraums berechnet wird. Mit anderen Worten, es handelt sich um eine globale Bereichsfunktion mit einer festen Adresse. Sie können dies deutlich mit "objdump" sehen.

$ objdump -j .text -t direct_topfunc_call | grep 'main.add'
000000000044d980 g     F .text	000000000000000f main.add

objdump Ergänzung

---j .text Nur Textabschnitt angezeigt -- -t Symboltabelle anzeigen --000000000044d980 g F .text 000000000000000f main.add Adresse 0x44d980 hat ein globales Funktionssymbol mit dem Namen main.add

Alle benutzerdefinierten Symbole werden als Offsets von den Pseudoregistern FP (lokal) und SB (global) beschrieben. Da das Pseudoregister SB als Ursprung des Speichers betrachtet werden kann, kann das Symbol foo (SB) als Symbol betrachtet werden, das die Adresse von foo darstellt.

--NOSPLIT: Weist den Compiler an, keine Präambel * stack-split * einzufügen, um festzustellen, ob der aktuelle Stack erweitert werden muss. Da die Funktion "add" keine lokalen Variablen enthält und keine Stapelrahmen erfordert, muss der aktuelle Stapel nicht erweitert werden. Daher ist die Überprüfung der Stapelerweiterung bei jedem Aufruf der Funktion eine Verschwendung von CPU-Ressourcen. Der Compiler erkennt dies automatisch und setzt automatisch das NOSPLIT-Flag. Die Stapelerweiterung wird später im Abschnitt Goroutine erwähnt.

-- $ 0-16: $ 0 steht für die Anzahl der dieser Funktion zugewiesenen Stapelrahmenbytes, 16 für die Größe des vom Aufrufer übergebenen Arguments (+ Rückgabewert). (16 Bytes mit int 32 x 3 + bool (mit 4 Bytes ausrichten))

Im allgemeinen Fall folgt auf die Größe des Stapelrahmens die Größe der Argumente, die durch ein Minuszeichen getrennt sind. (Dieses Minuszeichen stellt keine Subtraktion dar.) $ 24-8 gibt an, dass die Funktion einen 24-Byte-Stapelrahmen hat und mit einem 8-Byte-Argument aufgerufen wird, das im aufrufenden Stapelrahmen vorhanden ist. Wenn für TEXT kein NOSPLIT angegeben ist, muss die Größe des Arguments angegeben werden. Bei Assembly-Funktionen, die den Go-Prototyp verwenden, überprüft go vet, ob die Argumentgröße korrekt ist.

0x0000 FUNCDATA $0, gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB)
0x0000 FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)

Die Anweisungen FUNCDATA und PCDATA enthalten Informationen zur Verwendung durch den GC.

0x0000 MOVL "".b+12(SP), AX
0x0004 MOVL "".a+8(SP), CX

Mit der Aufrufkonvention von Go können alle Argumente unter Verwendung des vorab zugewiesenen Speicherplatzes im Stapelrahmen des Aufrufers durch den Stapel geleitet werden. Daher liegt es in der Verantwortung des Aufrufers, Argumente an den Angerufenen zu übergeben und die Stapelgröße entsprechend zu verwalten, damit der Rückgabewert des Angerufenen unter dem Anrufer zurückgegeben wird.

Der Go-Compiler generiert keine PUSH / POP-Anweisungen. Stattdessen wird der Stapel erweitert oder verkleinert, indem SP addiert oder subtrahiert wird. Dies ist ein Pseudoregister, das auf die Oberseite des Stapels zeigt.

[UPDATE: We've discussed about this matter in issue #21: about SP register.]

Mit dem Pseudowiderstand SP werden lokale Variablen und Argumente referenziert. Da SP auf den Anfang des Stapelrahmens zeigt, wird die Referenz mit einem negativen Versatz im Bereich [-framesize, 0) erstellt. z.B. x-8 (SP), y-4 (SP)

Die offizielle Dokumentation besagt, dass benutzerdefinierte Symbole durch einen Versatz vom FP-Register dargestellt werden. Dies gilt jedoch nicht für automatisch generierten Code. Moderne Go-Compiler verweisen immer auf Argumente und lokale Variablen in einem Versatz zum Stapelzeiger. Dies ermöglicht die Verwendung des FP als zusätzliches Allzweckregister auf Plattformen mit einer kleinen Anzahl von Registern, wie z. B. x86. Weitere Informationen finden Sie unter * Stapelrahmenlayout auf x86-64 *. [UPDATE: We've discussed about this matter in issue #2: Frame pointer.]

". b + 12 (SP)" und ".a + 8 (SP)" beziehen sich auf die oberen 12- bzw. 8-Byte-Adressen des Stapels. (Beachten Sie, dass sich der Stapel von der oberen Adresse zur unteren Adresse erstreckt.)

".a" und ".b" sind willkürliche Aliase, die dem Referenzort zugewiesen werden. Der Name hat keinen Einfluss auf Ihre Arbeit, ist jedoch für die Verwendung der indirekten Adressierung in virtuellen Registern unerlässlich.

Das Dokument über FP, bei dem es sich um einen Pseudo-Frame-Zeiger handelt, lautet wie folgt.

FP ist ein virtueller Frame-Zeiger zum Referenzieren von Funktionsargumenten. Der Compiler enthält den Inhalt dieses Registers und verweist auf die Argumente der Funktion auf dem Stapel als Offsets basierend auf diesem Register. Mit anderen Worten, in der 64-Bit-Architektur bezieht sich 0 (FP) auf das erste Argument der Funktion und 8 (FP) auf das zweite Argument. Um jedoch auf diese Weise auf die Argumente zugreifen zu können, müssen Sie mit einem Namen beginnen, z. B. first_arg + 0 (FP) oder second_arg + 8 (FP). (Der Versatz von FP unterscheidet sich vom Fall von SB, dh vom Versatz zum Symbol.) Der Assembler akzeptiert keine unbenannten Schriften wie 0 (FP) und 8 (FP) und erzwingt diese Namensspezifikation. Machen. Der tatsächliche Name ist für Ihre Arbeit irrelevant, wird jedoch zur Dokumentation des Argumentnamens verwendet.

Schließlich gibt es zwei wichtige Dinge.

  1. Das erste Argument "a" wird in "8 (SP)" anstelle von "0 (SP)" gesetzt. Dies liegt daran, dass die Rückgabezieladresse in "0 (SP)" gespeichert ist, wenn der Anrufer eine "CALL" -Anweisung ist.
  2. Argumente werden vom letzten auf den Stapel verschoben.
0x0008 ADDL CX, AX
0x000a MOVL AX, "".~r2+16(SP)
0x000e MOVB $1, "".~r3+20(SP)

ADDL fügt zwei Long-Wörter (4 Byte lange Werte) hinzu und speichert das Ergebnis in AX. Hier werden "AX" und "CX" hinzugefügt und das Ergebnis in "AX" gespeichert. Das Ergebnis wird dann in "". ~ R2 + 16 (SP) "auf dem vorab zugewiesenen Stapel gespeichert, damit der Aufrufer den Rückgabewert erhält. Auch hier hat "". ~ R2 "keine Bedeutung für die Verarbeitung von Inhalten.

Da Go mehrere Rückgabewerte unterstützt, wird in diesem Beispiel die Konstante "true" auch als Rückgabewert zurückgegeben. Wie beim ersten Rückgabewert wird das Ergebnis in "" gespeichert. ~ R3 + 20 (SP) ", obwohl der Offset unterschiedlich ist.

0x0013 RET

Die letzte Pseudoanweisung "RET" besteht darin, den Go-Assembler anzuweisen, die entsprechende Anweisung einzufügen, um von der Unterroutine auf der Zielhardware zurückzukehren. In den meisten Fällen POP die in 0 (SP) gespeicherte Rückgabezieladresse und springe dorthin.

Der letzte Befehl im TEXT-Block muss ein Sprungbefehl sein (normalerweise mit RET). Wenn keine Sprunganweisung vorhanden ist, fügt der Linker eine Anweisung hinzu, um zu sich selbst zu springen, damit die Anweisung nicht über den TEXT-Block hinaus ausgeführt wird.

Da viele Grammatiken und Erklärungen herausgekommen sind, werde ich eine kurze Zusammenfassung schreiben.

;;Globales Funktionssymbol"".Deklarieren Sie hinzufügen(Haupt beim Verknüpfen.add)
;; stack-Fügen Sie keine geteilte Präambel ein
;;Der Stapelrahmen wird mit 0 Byte und 16 Byte übergeben
;; func add(a, b int32) (int32, bool)
0x0000 TEXT	"".add(SB), NOSPLIT, $0-16
  ;; ...omitted FUNCDATA stuff...
  0x0000 MOVL	"".b+12(SP), AX	    ;;Zweites Argument vom aufrufenden Stack-Frame an AX(b)Bewegung
  0x0004 MOVL	"".a+8(SP), CX	    ;;Erstes Argument vom aufrufenden Stack-Frame an CX(a)Bewegung
  0x0008 ADDL	CX, AX		          ;; AX=CX+AX
  0x000a MOVL	AX, "".~r2+16(SP)   ;;Verschieben Sie das in AX gespeicherte Additionsergebnis in den aufrufenden Stapelrahmen
  0x000e MOVB	$1, "".~r3+20(SP)   ;;Konstante`true`Zum aufrufenden Stack-Frame
  0x0013 RET			                ;; 0(SP)Wechseln Sie zu der in gespeicherten Zieladresse

Die Visualisierung des Inhalts des Stapels nach Abschluss der Verarbeitung von "main.add" ist wie folgt.

   |    +-------------------------+ <-- 32(SP)              
   |    |                         |                         
 G |    |                         |                         
 R |    |                         |                         
 O |    | main.main's saved       |                         
 W |    |     frame-pointer (BP)  |                         
 S |    |-------------------------| <-- 24(SP)              
   |    |      [alignment]        |                         
 D |    | "".~r3 (bool) = 1/true  | <-- 21(SP)              
 O |    |-------------------------| <-- 20(SP)              
 W |    |                         |                         
 N |    | "".~r2 (int32) = 42     |                         
 W |    |-------------------------| <-- 16(SP)              
 A |    |                         |                         
 R |    | "".b (int32) = 32       |                         
 D |    |-------------------------| <-- 12(SP)              
 S |    |                         |                         
   |    | "".a (int32) = 10       |                         
   |    |-------------------------| <-- 8(SP)               
   |    |                         |                         
   |    |                         |                         
   |    |                         |                         
 \ | /  | return address to       |                         
  \|/   |     main.main + 0x30    |                         
   -    +-------------------------+ <-- 0(SP) (TOP OF STACK)

(diagram made with https://textik.com)

Dissecting main

Lassen Sie uns den Inhalt der Hauptfunktion noch einmal überprüfen.

func main() { add(10, 32) }
0x0000 TEXT		"".main(SB), $24-0
  ;; ...omitted stack-split prologue...
  0x000f SUBQ		$24, SP
  0x0013 MOVQ		BP, 16(SP)
  0x0018 LEAQ		16(SP), BP
  ;; ...omitted FUNCDATA stuff...
  0x001d MOVQ		$137438953482, AX
  0x0027 MOVQ		AX, (SP)
  ;; ...omitted PCDATA stuff...
  0x002b CALL		"".add(SB)
  0x0030 MOVQ		16(SP), BP
  0x0035 ADDQ		$24, SP
  0x0039 RET
  ;; ...omitted stack-split epilogue...
0x0000 TEXT "".main(SB), $24-0

Gleich wie für die Funktion "Hinzufügen". Dieses Mal sind 24 Bytes im Stapelrahmen gesichert, so dass kein Argument empfangen und kein Rückgabewert zurückgegeben wird.

0x000f SUBQ     $24, SP
0x0013 MOVQ     BP, 16(SP)
0x0018 LEAQ     16(SP), BP

Wiederum ermöglicht die Aufrufkonvention von Go, dass alle Funktionsargumente durch den Stapel geleitet werden.

Durch Subtrahieren von $ 24 Bytes von SP reserviert main 24 Bytes für seinen eigenen Stapelrahmen. (Beachten Sie, dass sich der Stapel nach unten erstreckt)

Verwenden Sie diese reservierten $ 24 Bytes wie folgt.

Schließlich berechnet "LEAQ" nach der Stapelzuweisung die neue Adresse des Rahmenzeigers und speichert sie in "BP". (BP = 16 (SP) wie in x86 lea Anweisung)

0x001d MOVQ     $137438953482, AX
0x0027 MOVQ     AX, (SP)

Der Aufrufer platziert das Argument für Angerufene als 8-Byte-Quad-Wort oben im Stapel. Die platzierten Werte mögen auf den ersten Blick bedeutungslos erscheinen, aber "137438953482" ist eine Sammlung von 4-Byte "10" und "32".

$ echo 'obase=2;137438953482' | bc
10000000000000000000000000000000001010
\____/\______________________________/
   32                              10

Die oberen 32-63 Bits von 137438953482 repräsentieren "100000 (32)" und die unteren 0-31 Bits repräsentieren "000000000000000000000000001010 (10)".

0x002b CALL     "".add(SB)

Rufen Sie die Funktion add mit der Anweisung CALL als relativen Offset zum SB auf.

Beachten Sie, dass CALL eine 8-Byte-Adresse als Rückgabezieladresse am oberen Rand des Stapels platziert, sodass alle SPs, auf die in der add -Funktion verwiesen wird, um 8 Bytes nach unten verschoben werden. Zum Beispiel wird "". A "als" 8 (SP) "anstelle von" 0 (SP) "dargestellt.

0x0030 MOVQ     16(SP), BP
0x0035 ADDQ     $24, SP
0x0039 RET

Schließlich,

  1. Spulen Sie das Rahmenregister "MOVQ 16 (SP), BP" zurück
  2. Geben Sie den reservierten Stapelrahmen "ADDQ $ 24, SP" frei
  3. Zurück

Und beenden Sie die Ausführung der Hauptfunktion

Ich denke, was Sie durch "Hinzufügen" und "Haupt" tun, ist ein allgemeiner Unterprogrammaufruf.

Goroutine Stack Management

Wenn Sie sich die Assembly für Goroutine ansehen, sind Sie mit den Anweisungen für die Stapelverwaltung vertraut.

Damit wir diese Muster so schnell wie möglich verstehen können, wollen wir verstehen, was wir tun und warum wir dies tun.

Stacks

Die Anzahl der Goroutinen, die in Ihrem Go-Programm angezeigt werden, hängt von der jeweiligen Situation ab. Praktische Programme können in Millionenhöhe sein. Die Laufzeit von Go verfolgt einen konservativen Ansatz, um den Goroutine-Stack so zu sichern, dass ihm nicht der Speicher ausgeht. Anfänglich werden von der Laufzeit 2 KB Stapelspeicher für jede Goroutine zugewiesen. (Der Stapel ist tatsächlich dem Heap im Hintergrund zugeordnet.)

Wenn Goroutine ausgeführt wird, ist möglicherweise mehr Speicher erforderlich als die ursprünglich zugewiesenen 2 KB. In diesem Fall kann der Stapel zerstört werden und in andere Speicherbereiche eindringen. Um einen solchen Stapelüberlauf zu verhindern, reserviert die Laufzeit einen Stapel, der doppelt so groß ist wie zuvor, und kopiert den Inhalt des Stapels darauf, wenn Goroutine den Stapel überschreitet. Dieser Prozess heißt * Stack-Split * und ermöglicht es Ihnen, die Stapelgröße von Goroutine effizient und dynamisch zu handhaben.

Splits

Damit * stack-split * funktioniert, fügt der Compiler am Anfang und am Ende jeder Funktion einige Anweisungen ein, die zu einem Stapelüberlauf führen können, damit Sie nach einem Stapelüberlauf suchen können. Wie wir bereits gesehen haben, ist dies für Funktionen nutzlos, bei denen ein Stapelüberlauf unwahrscheinlich ist. Daher kann NOSPLIT dem Compiler mitteilen, dass keine Anweisungen zum Überprüfen eingefügt werden müssen.

Ich habe den Code für * stack-split * in der obigen Hauptfunktion weggelassen, aber schauen wir uns das jetzt an.

0x0000 TEXT	"".main(SB), $24-0
  ;; stack-split prologue
  0x0000 MOVQ	(TLS), CX
  0x0009 CMPQ	SP, 16(CX)
  0x000d JLS	58

  0x000f SUBQ	$24, SP
  0x0013 MOVQ	BP, 16(SP)
  0x0018 LEAQ	16(SP), BP
  ;; ...omitted FUNCDATA stuff...
  0x001d MOVQ	$137438953482, AX
  0x0027 MOVQ	AX, (SP)
  ;; ...omitted PCDATA stuff...
  0x002b CALL	"".add(SB)
  0x0030 MOVQ	16(SP), BP
  0x0035 ADDQ	$24, SP
  0x0039 RET

  ;; stack-split epilogue
  0x003a NOP
  ;; ...omitted PCDATA stuff...
  0x003a CALL	runtime.morestack_noctxt(SB)
  0x003f JMP	0

Beachten Sie, dass dieser Prolog und dieser Epilog so lange wiederholt werden, bis die Stapelgröße groß genug ist.

Prologue

0x0000 MOVQ	(TLS), CX   ;; store current *g in CX
0x0009 CMPQ	SP, 16(CX)  ;; compare SP and g.stackguard0
0x000d JLS	58	        ;; jumps to 0x3a if SP <= g.stackguard0

TLS ist ein virtuelles Register, das von der Laufzeit verwaltet wird und einen Zeiger auf das aktuelle g hat. Dies ist eine Datenstruktur, die alle Zustände von Goroutine verfolgt.

Lassen Sie uns die Definition von g aus dem Quellcode der Laufzeit überprüfen.

type g struct {
	stack       stack   // 16 bytes
  //stackguard0 ist der Stapelzeiger, der mit Prolog verglichen werden soll
  //Normalerweise ist stackgurad0 stack.lo+Wird zum StackGuard, kann aber auch zum StackPreempt werden, um die Vorabfreigabe auszulösen
  //Vorkaufsrecht:Das Verhalten eines Multitasking-Computersystems, eine laufende Aufgabe vorübergehend anzuhalten
	stackguard0 uintptr
	stackguard1 uintptr

	// ...omitted dozens of fields...
}

Da "g.stack" 16 Bytes beträgt, ist "16 (CX)" "g.stackguard0". Dies ist der von der Laufzeit verwaltete Stapelschwellenwert, der mit dem Stapelzeiger verglichen werden kann, um festzustellen, ob Goroutine den Stapelspeicherplatz belegt hat.

Der Stapel wächst in Richtung der unteren Adresse. Wenn also "SP <= stackguard0" ist, ist der Stapelspeicherplatz belegt. In diesem Fall springt der Prolog zum Epilog.

Epilogue

0x003a NOP
0x003a CALL	runtime.morestack_noctxt(SB)
0x003f JMP	0

Der Vorgang des Epilog ist einfach: Rufen Sie einfach zur Laufzeit die Stack-Erweiterungsfunktion auf, um den Stack zu erweitern und zum Prologcode zurückzukehren.

Das NOP vor dem CALL existiert, um zu verhindern, dass der Prologcode direkt zum CALL springt. Abhängig von der Plattform kann es notwendig sein, ziemlich tief zu erklären, daher werde ich die Erklärung weglassen, aber es ist eine übliche Praxis, einen NOP-Befehl vor den CALL-Befehl zu stellen und dorthin zu springen. [UPDATE: We've discussed about this matter in issue #4: Clarify "nop before call" paragraph.]

Diesmal habe ich nur die Spitze des Eisbergs erklärt.

Der Stapelerweiterungsmechanismus ist zu detailliert und komplex, um hier erklärt zu werden. Wenn ich die Gelegenheit dazu hätte, hätte ich gerne ein eigenes Kapitel.

Zusammenfassung

Dieses Mal habe ich versucht, Go Assembly anhand eines einfachen Beispiels zu erklären.

In den verbleibenden Kapiteln werden wir uns eingehender mit der internen Implementierung von Go befassen.

Recommended Posts

Erste Schritte mit Go Assembly
Erste Schritte mit Android!
1.1 Erste Schritte mit Python
Erste Schritte mit apache2
Erste Schritte mit Python
Erste Schritte mit Django 1
Einführung in die Optimierung
Erste Schritte mit Numpy
Erste Schritte mit Spark
Erste Schritte mit Python
Erste Schritte mit Pydantic
Erste Schritte mit Jython
Erste Schritte mit Django 2
Übersetzen Erste Schritte mit TensorFlow
Einführung in Python-Funktionen
Einführung in Tkinter 2: Button
Erste Schritte mit PKI mit Golang ―― 4
Erste Schritte mit Python Django (1)
Erste Schritte mit Python Django (4)
Erste Schritte mit Python Django (3)
Einführung in Python Django (6)
Erste Schritte mit Django mit PyCharm
Erste Schritte mit Python Django (5)
Erste Schritte mit Python Responder v2
Einführung in Git (1) History-Speicher
Erste Schritte mit Sphinx. Generieren Sie Docstring mit Sphinx
Erste Schritte mit Python-Webanwendungen
Erste Schritte mit Python für PHPer-Klassen
Erste Schritte mit Sparse Matrix mit scipy.sparse
Erste Schritte mit Julia für Pythonista
Erste Schritte mit Python Grundlagen von Python
Erste Schritte mit der Cisco Spark REST-API
Beginnend mit USD unter Windows
Erste Schritte mit genetischen Python-Algorithmen
Erste Schritte mit Python 3.8 unter Windows
Erste Schritte mit Python für PHPer-Funktionen
Erste Schritte mit der CPU-Diebstahlzeit
Erste Schritte mit Python3 # 1 Grundkenntnisse erlernen
Python mit Go
Erste Schritte mit Python Web Scraping Practice
Erste Schritte mit Python für PHPer-Super Basics
Erste Schritte mit Python Web Scraping Practice
Erste Schritte mit Dynamo von Python Boto
Erste Schritte mit Lisp für Pythonista: Ergänzung
Erste Schritte mit Heroku, Bereitstellen der Flaschen-App
Erste Schritte mit TDD mit Cyber-dojo bei MobPro
Grale fangen an
Erste Schritte mit Python mit 100 Klopfen bei der Sprachverarbeitung
MongoDB-Grundlagen: Erste Schritte mit CRUD mit JAVA
Erste Schritte mit dem Zeichnen mit matplotlib: Schreiben einfacher Funktionen
Erste Schritte mit der japanischen Übersetzung des Keras Sequential-Modells
[Übersetzung] Erste Schritte mit Rust für Python-Programmierer
Django Erste Schritte Teil 2 mit dem Eclipse Plugin (PyDev)
Erste Schritte mit AWS IoT in Python
Erste Schritte mit Pythons Ast-Modul (Verwenden von NodeVisitor)