Premiers pas avec Go Assembly

Cet article est une traduction et un ajout de Chapitre I: A Primer on Go Assembly.

Cet article suppose les personnes suivantes:

environnement

$ go version
go version go1.10 linux/amd64

Pseudo assemblage

La sortie de l'assembly par le compilateur Go est une abstraction et n'est pas mappée au matériel réel. L'assembleur Go traduit ce pseudo-assemblage dans un langage machine qui correspond au matériel cible.

Il peut être utile d'imaginer quelque chose comme du code d'octet Java.

Le plus grand avantage d'avoir une telle couche intermédiaire est qu'elle facilite l'adaptation aux nouvelles architectures. Pour plus d'informations, consultez [* The Design of the Go Assembler *] de Rob Pike (https://talks.golang.org/2016/asm.slide#1).

La chose la plus importante à savoir sur les assemblages Go est le fait que les assemblages Go ne correspondent pas directement au matériel cible. Certains sont directement liés au matériel, mais d'autres ne le sont pas. Cela élimine le besoin de l'assembleur Pass sur le pipeline, au lieu de cela, le compilateur peut gérer le pseudo-assemblage qui fait abstraction de ce matériel et la sélection d'instruction (dans ce cas, l'assembly Go à l'assembly réel). La conversion en) se fait désormais en partie après la génération du code (génération par le générateur de l'assembly Go). À titre d'exemple de pseudo-assemblage, l'instruction MOV de l'assemblage GO peut être convertie en instruction "clear" ou "load", etc., ou elle peut rester telle quelle (bien que le nom puisse changer) en fonction de l'architecture. Alors que les concepts architecturaux courants tels que le mouvement de la mémoire et les appels et retours de sous-programmes sont abstraits, les instructions spécifiques au matériel sont souvent représentées telles quelles.

L'assembleur Go est un programme qui analyse ce pseudo-assemblage et le convertit en instructions d'entrée dans l'éditeur de liens.

Exemple utilisant un programme simple

Considérez le code suivant.

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

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

Compilons ce code dans un assembly.

$ 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: Représenté par rapport au début de la fonction de décalage de l'instruction.

--TEXT "". Add: La directive TEXT indique que le symbole" ".add est contenu dans la section .text et que les instructions suivantes sont le contenu de cette fonction. La chaîne vide " " est remplacée par le nom du package actuel au moment de la liaison. Cette fois, ce sera «main.add».

--(SB): SB est un registre virtuellement défini dans l'assemblage Go, qui est un pointeur "Static-Base". Il représente le début de l'espace d'adressage du programme. Le "" ". Add (SB) indique que le symbole "" ". Add est à un décalage constant calculé par l'éditeur de liens depuis le début de l'espace d'adressage. En d'autres termes, il s'agit d'une fonction de portée globale avec une adresse fixe. Vous pouvez le voir clairement avec objdump.

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

supplément objdump

---j .text Section de texte uniquement affichée -- -t Afficher la table des symboles --000000000044d980 g F .text 000000000000000f main.add Address 0x44d980 a un symbole de fonction global nommé main.add

Tous les symboles définis par l'utilisateur sont décrits comme des décalages par rapport aux pseudo-registres FP (local) et SB (global). Puisque le pseudo-registre SB peut être considéré comme l'origine de la mémoire, le symbole foo (SB) peut être considéré comme un symbole représentant l'adresse de foo.

--NOSPLIT: indique au compilateur de ne pas insérer de préambule * stack-split * pour voir si la pile actuelle doit être développée. Puisque la fonction add n'a pas de variables locales et ne nécessite pas de cadres de pile, il n'est pas nécessaire d'étendre la pile actuelle, donc vérifier l'extension de pile à chaque fois que la fonction est appelée est un gaspillage de ressources CPU. Le compilateur le saura automatiquement et définira automatiquement cet indicateur NOSPLIT. L'expansion de la pile est mentionnée plus loin dans la section Goroutine.

-- $ 0-16: $ 0 représente le nombre d'octets de trame de pile alloués à cette fonction, 16 représente la taille de l'argument (+ valeur de retour) passé par l'appelant. (16 octets avec int 32 x 3 + booléen (aligné sur 4 octets))

Dans le cas général, la taille du frame de pile est suivie de la taille des arguments séparés par un signe moins. (Ce signe moins ne représente pas une soustraction) $ 24-8 indique que la fonction a un cadre de pile de 24 octets et sera appelée avec un argument de 8 octets qui existe dans le cadre de pile appelant. Lorsque NOSPLIT n'est pas spécifié pour TEXT, la taille de l'argument doit être spécifiée. Pour les fonctions d'assemblage qui utilisent le prototype Go, un vétérinaire vérifie si la taille de l'argument est correcte.

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

Les directives FUNCDATA et PCDATA contiennent des informations destinées à être utilisées par le GC.

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

La convention d'appel de Go permet à tous les arguments d'être passés à travers la pile en utilisant un espace pré-alloué dans le cadre de pile de l'appelant. Par conséquent, il est de la responsabilité de l'appelant de transmettre des arguments à l'appelé et de gérer la taille de la pile de manière appropriée afin que la valeur de retour de l'appelé soit renvoyée sous l'appelant.

Le compilateur Go ne génère pas d'instructions PUSH / POP. Au lieu de cela, la pile est étendue ou contractée en ajoutant ou en soustrayant SP, qui est un pseudo-registre qui pointe vers le haut de la pile.

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

La pseudo-résistance SP est utilisée pour référencer des variables locales et des arguments. Puisque SP pointe vers le début de la trame de pile, la référence est faite en utilisant un décalage négatif dans la plage [-framesize, 0). par exemple x-8 (SP), y-4 (SP)

La documentation officielle indique que les symboles définis par l'utilisateur sont représentés par des décalages du registre FP, mais ce n'est pas le cas pour le code généré automatiquement. Les compilateurs Modern Go font toujours référence aux arguments et aux variables locales à un décalage par rapport au pointeur de pile. Cela permet au FP d'être utilisé comme registre à usage général supplémentaire sur les plates-formes avec un petit nombre de registres, comme x86. Voir * Disposition du cadre de la pile sur x86-64 * pour plus d'informations. [UPDATE: We've discussed about this matter in issue #2: Frame pointer.]

"". b + 12 (SP) "et" "" .a + 8 (SP) "se réfèrent respectivement aux adresses des 12 et 8 octets supérieurs de la pile. (Notez que la pile s'étend de l'adresse supérieure à l'adresse inférieure)

«.a» et «.b» sont des alias arbitraires donnés à l'emplacement de référence. Le nom n'affecte pas ce que vous faites, mais il est essentiel pour utiliser l'adressage indirect sur les registres virtuels.

Le document sur FP, qui est un pseudo pointeur de trame, dit ce qui suit.

FP est un pointeur de trame virtuel pour référencer des arguments de fonction. Le compilateur contient le contenu de ce registre et référence les arguments de la fonction sur la pile comme des décalages basés sur ce registre. En d'autres termes, dans l'architecture 64 bits, 0 (FP) fait référence au premier argument de la fonction et 8 (FP) fait référence au deuxième argument. Cependant, pour accéder aux arguments de cette façon, vous devez commencer par un nom, tel que first_arg + 0 (FP) ou second_arg + 8 (FP). (Le décalage de FP est différent du cas de SB, ce qui signifie le décalage du symbole.) L'assembleur n'accepte pas l'écriture sans nom telle que 0 (FP) et 8 (FP) et force cette spécification de nom. Faire. Le nom réel n'est pas pertinent pour ce que vous faites, mais il est utilisé pour documenter le nom de l'argument.

Enfin, il y a deux choses importantes.

  1. Le premier argument «a» est placé dans «8 (SP)» au lieu de «0 (SP)». C'est parce que l'adresse de destination de retour est stockée dans "0 (SP)" lorsque l'appelant est une instruction "CALL".
  2. Les arguments sont poussés vers la pile à partir du dernier.
0x0008 ADDL CX, AX
0x000a MOVL AX, "".~r2+16(SP)
0x000e MOVB $1, "".~r3+20(SP)

ADDL ajoute deux mots longs (valeurs de 4 octets) et stocke le résultat dans AX. Ici, «AX» et «CX» sont ajoutés et le résultat est stocké dans «AX». Le résultat est ensuite stocké dans "" ". ~ R2 + 16 (SP)` sur la pile pré-allouée pour que l'appelant reçoive la valeur de retour. Là encore, «" ". ~ R2» n'a aucune signification en termes de traitement du contenu.

Go prend en charge plusieurs valeurs de retour, donc dans cet exemple, la constante «true» est également renvoyée en tant que valeur de retour. Comme pour la première valeur de retour, le résultat est stocké dans " ". ~ R3 + 20 (SP), bien que le décalage soit différent.

0x0013 RET

La pseudo-instruction finale «RET» est de demander à l'assembleur Go d'insérer l'instruction appropriée pour revenir du sous-programme sur le matériel ciblé. Dans la plupart des cas, POP l'adresse de destination de retour stockée dans 0 (SP) et y saute.

La dernière instruction du bloc TEXT doit être une instruction de saut (généralement avec RET) S'il n'y a pas d'instruction de saut, l'éditeur de liens ajoute une instruction pour sauter sur lui-même afin qu'il n'exécute pas l'instruction au-delà du bloc TEXT.

Comme beaucoup de grammaire et d'explications ont été publiées, j'écrirai un bref résumé.

;;Symbole de fonction globale"".Déclarer ajouter(Principal lors de la liaison.add)
;; stack-Ne pas insérer de préambule divisé
;;La trame de pile est passée à 0 octet, argument de 16 octets
;; func add(a, b int32) (int32, bool)
0x0000 TEXT	"".add(SB), NOSPLIT, $0-16
  ;; ...omitted FUNCDATA stuff...
  0x0000 MOVL	"".b+12(SP), AX	    ;;Deuxième argument à AX à partir du cadre de pile appelant(b)Bouge toi
  0x0004 MOVL	"".a+8(SP), CX	    ;;Premier argument du frame de pile appelant à CX(a)Bouge toi
  0x0008 ADDL	CX, AX		          ;; AX=CX+AX
  0x000a MOVL	AX, "".~r2+16(SP)   ;;Déplacer le résultat de l'addition stocké dans AX vers le frame de pile appelant
  0x000e MOVB	$1, "".~r3+20(SP)   ;;constant`true`Vers le cadre de pile appelant
  0x0013 RET			                ;; 0(SP)Aller à l'adresse de destination de retour stockée dans

La visualisation du contenu de la pile lorsque le traitement de «main.add» est terminé est la suivante.

   |    +-------------------------+ <-- 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

Revoyons le contenu de la fonction main.

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

Identique à la fonction add. Cette fois, 24 octets sont sécurisés dans la trame de pile afin qu'aucun argument ne soit reçu et qu'aucune valeur de retour ne soit renvoyée.

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

Encore une fois, la convention d'appel de Go passe tous les arguments de fonction à travers la pile.

En soustrayant $ 24 octets de SP, main réserve 24 octets pour sa propre trame de pile. (Notez que la pile s'étend vers le bas)

Utilisez ces 24 octets réservés comme suit.

--8 octets (16 (SP) -24 (SP) ) sont utilisés pour stocker la valeur actuelle du pointeur de trame BP. Cela vous permet de rembobiner la pile (suivez la fonction sous l'appel), ce qui est utile lors du débogage. (MOVQ BP, 16 (SP)) -1 + 3 octets (12 (SP) -16 (SP) ) est réservé pour recevoir la deuxième valeur de retour de la fonction add (bool vaut 1 octet mais amd64 +3 octets pour l'alignement architectural) --4 octets (8 (SP) -12 (SP) ) sont réservés pour recevoir la première valeur de retour de la fonction add (int32) --4 octets (4 (SP) -8 (SP) ) sont réservés pour la valeur de l'argument de la fonctionadd`` b (int32) --4 octets (0 (SP) -4 (SP) ) sont réservés pour la valeur de l'argument de la fonction add a (int32)

Enfin, après l'allocation de la pile, «LEAQ» calcule la nouvelle adresse du pointeur de trame et la stocke dans «BP». (BP = 16 (SP) comme dans l'instruction x86 lea)

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

L'appelant place l'argument de l'appelé sous la forme d'un mot quadruple de 8 octets en haut de la pile. À première vue, les valeurs placées peuvent sembler dénuées de sens, mais "137438953482" est une collection de 4 octets "10" et "32".

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

Les 32-63 bits supérieurs de 137438953482 représentent «100000 (32)» et les bits inférieurs 0-31 représentent «00000000000000000000000000001010 (10)».

0x002b CALL     "".add(SB)

Appelez la fonction add avec l'instruction CALL comme décalage relatif par rapport au SB.

Notez que «CALL» place une adresse de 8 octets en haut de la pile comme adresse de destination de retour, de sorte que tous les «SP» référencés dans la fonction «add» sont décalés de 8 octets vers le bas. Par exemple, "" ". A` est représenté par" 8 (SP) "au lieu de" 0 (SP) ".

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

Finalement,

  1. Rembobinez le registre de trame MOVQ 16 (SP), BP
  2. Libérez le cadre de pile réservé ADDQ $ 24, SP
  3. Retour

Et terminer l'exécution de la fonction principale

Je pense que ce que vous faites via add et main est un appel de sous-programme général.

Gestion de la pile Goroutine

Si vous regardez l'assemblage pour Goroutine, vous serez familiarisé avec les instructions de gestion de pile.

Pour vous aider à comprendre ces modèles le plus rapidement possible, comprenons ce que nous faisons et pourquoi nous le faisons.

Stacks

Le nombre de Goroutines qui apparaissent dans votre programme Go dépend de la situation. Les programmes pratiques peuvent se chiffrer en millions. Le runtime de Go adopte une approche conservatrice pour sécuriser la pile Goroutine afin qu'elle ne manque pas de mémoire. Au départ, 2 Ko d'espace de pile sont alloués par le runtime pour tout Goroutine. (La pile est en fait allouée au tas en arrière-plan)

Lorsque Goroutine s'exécute, il peut nécessiter plus de mémoire que les 2 Ko initialement alloués. Dans ce cas, il peut détruire la pile et envahir d'autres zones de mémoire. Pour éviter un tel débordement de pile, le runtime réserve une pile deux fois plus grande qu'auparavant et y copie le contenu de la pile lorsque Goroutine est sur le point de dépasser la pile. Ce processus s'appelle * stack-split * et vous permet de gérer la taille de la pile de Goroutine de manière efficace et dynamique.

Splits

Pour que * stack-split * fonctionne, le compilateur insère des instructions au début et à la fin de chaque fonction qui peuvent provoquer un débordement de pile pour vous permettre de vérifier un débordement de pile. Comme nous l'avons vu précédemment, cela est inutile pour les fonctions où le débordement de pile est peu probable, donc NOSPLIT peut dire au compilateur qu'il n'est pas nécessaire d'insérer des instructions pour vérifier.

J'ai omis le code pour * stack-split * dans la fonction main ci-dessus, mais regardons-le maintenant.

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

--Au début de la fonction (prologue), Goroutine vérifie si la pile est épuisée, auquel cas il saute à la fin de la fonction (épilogue). --A la fin de la fonction (épilogue), il déclenche le processus d'expansion de la pile, après quoi il revient au début de la fonction (prologue).

Notez que ce prologue et cet épilogue continueront à tourner jusqu'à ce que la taille de la pile soit suffisamment grande.

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 est un registre virtuel géré par le runtime qui a un pointeur vers le g actuel. Il s'agit d'une structure de données qui retrace tous les états de Goroutine.

Vérifions la définition de g à partir du code source du runtime.

type g struct {
	stack       stack   // 16 bytes
  //stackguard0 est le pointeur de pile à comparer avec Prologue
  //Normalement, stackgurad0 est stack.lo+Devient un StackGuard, mais peut aussi être un StackPreempt pour déclencher la préemption
  //Préemption:Le comportement d'un système informatique multitâche pour suspendre temporairement une tâche en cours d'exécution
	stackguard0 uintptr
	stackguard1 uintptr

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

Puisque «g.stack» est de 16 octets, «16 (CX)» est «g.stackguard0». Il s'agit du seuil de pile géré par le runtime, qui peut être comparé au pointeur de pile pour voir si Goroutine a utilisé l'espace de pile.

La pile se développe vers l'adresse inférieure, donc si SP <= stackguard0, l'espace de pile est épuisé. Dans ce cas, le prologue passe à l'épilogue.

Epilogue

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

Le processus d'épilogue est simple: il suffit d'appeler la fonction d'extension de pile au moment de l'exécution pour étendre la pile et revenir au code du prologue.

Le "NOP" avant le "CALL" existe pour empêcher le code du prologue de sauter directement au "CALL". Selon la plate-forme, il peut être nécessaire d'expliquer assez profondément, donc je vais omettre l'explication, mais c'est une pratique courante de mettre une instruction NOP avant l'instruction CALL et d'y sauter. [UPDATE: We've discussed about this matter in issue #4: Clarify "nop before call" paragraph.]

Cette fois, je n'ai expliqué que la pointe de l'iceberg.

Le mécanisme d'extension de la pile est trop détaillé et complexe pour être expliqué ici, alors j'aimerais avoir un chapitre dédié si j'en ai l'occasion.

Sommaire

Cette fois, j'ai essayé d'expliquer Go Assembly en utilisant un exemple simple.

Nous approfondirons la mise en œuvre interne de Go dans les chapitres restants.

Recommended Posts

Premiers pas avec Go Assembly
Premiers pas avec Android!
1.1 Premiers pas avec Python
Premiers pas avec apache2
Premiers pas avec Python
Premiers pas avec Django 1
Introduction à l'optimisation
Premiers pas avec Numpy
Premiers pas avec Spark
Premiers pas avec Python
Premiers pas avec Pydantic
Premiers pas avec Jython
Premiers pas avec Django 2
Traduire Premiers pas avec TensorFlow
Introduction aux fonctions Python
Introduction à Tkinter 2: Button
Premiers pas avec PKI avec Golang ―― 4
Premiers pas avec Python Django (1)
Premiers pas avec Python Django (4)
Premiers pas avec Python Django (3)
Introduction à Python Django (6)
Premiers pas avec Django avec PyCharm
Premiers pas avec Python Django (5)
Premiers pas avec Python responder v2
Introduction à Git (1) Stockage d'historique
Premiers pas avec Sphinx. Générer docstring avec Sphinx
Premiers pas avec les applications Web Python
Premiers pas avec Python pour les classes PHPer
Premiers pas avec Sparse Matrix avec scipy.sparse
Premiers pas avec Julia pour Pythonista
Premiers pas avec Python Bases de Python
Premiers pas avec Cisco Spark REST-API
Commençant par USD sur Windows
Premiers pas avec les algorithmes génétiques Python
Premiers pas avec Python 3.8 sous Windows
Premiers pas avec Python pour les fonctions PHPer
Premiers pas avec CPU Steal Time
Premiers pas avec python3 # 1 Apprenez les connaissances de base
Python avec Go
Premiers pas avec Python Web Scraping Practice
Premiers pas avec Python pour PHPer-Super Basics
Premiers pas avec Python Web Scraping Practice
Premiers pas avec Dynamo de Python boto
Premiers pas avec Lisp pour Pythonista: Supplément
Premiers pas avec Heroku, déploiement de l'application Flask
Premiers pas avec TDD avec Cyber-dojo chez MobPro
Grails pour commencer
Démarrer avec Python avec 100 coups sur le traitement du langage
Principes de base de MongoDB: Premiers pas avec CRUD avec JAVA
Premiers pas avec le dessin avec matplotlib: écrire des fonctions simples
Premiers pas avec la traduction japonaise du modèle séquentiel Keras
[Français] Premiers pas avec Rust pour les programmeurs Python
Django Getting Started Part 2 avec eclipse Plugin (PyDev)
Premiers pas avec AWS IoT facilement en Python
Premiers pas avec le module ast de Python (à l'aide de NodeVisitor)