Apprendre le langage d’assemblage

Le langage d’assemblage est l’équivalent lisible par l’homme du niveau logiciel le plus bas de la programmation informatique – le code machine.

Alors que l’ordinateur comprend tous les programmes comme des nombres, où divers nombres différents ordonnent à l’ordinateur de faire différentes opérations, cela est trop tediuos pour la consommation humaine (sans parler de l’auteur). Par conséquent, les humains programment en utilisant le langage d’assemblage, qui a une correspondance presque 1:1 avec le code machine.

Apprenez le langage de programmation C. Si vous connaissez le C (ou le C++), l’assemblage sera plus facile.

C# et Java sont de bons langages, mais il y a peu ou pas d’outils pour vous montrer à quoi ressemble le code C# et Java en langage d’assemblage. (En fait, Java et C# ont leur propre langage d’assemblage de code d’octet qui est plutôt différent, et distant, des architectures réelles de jeu d’instructions des CPU matérielles.)

À partir du code C (et C++), nous pouvons utiliser les compilateurs pour nous montrer à quoi ressemble le langage d’assemblage. Le C est un langage de bas niveau, et si vous savez comment chaque construction en C se traduit en langage d’assemblage, vous connaîtrez le langage d’assemblage ! Voici des exemples de constructions C que nous voulons connaître pour le langage d’assemblage :

.

C Construct Equivalent en langage d’assemblage
variable locale registre PCU ou emplacement mémoire de la pile
variable globale accès global ou emplacement mémoire const
référence à un tableau un motif d’addition et d’accès mémoire
goto instruction de branchement inconditionnel
instruction if un motif dans if-.goto : test conditionnel & instructions de branchement
bouclewhile un motif dans if-.goto
assignation modification de registre ou de mémoire
instructions arithmétiques instructions arithmétiques
paramètre. passage modification du registre ou de la mémoire de pile
paramètre réception accès au registre ou à la mémoire de pile
fonction. call instruction d’appel
fonction entry motif prologue
fonction exit motif épilogue
valeur de retour modification du registre et épilogue

Comment le langage d’assemblage diffère des autres langages de programmation

En langage d’assemblage, vous serez exposé aux ressources nues du matériel sous-jacent du CPU. (De plus, les choses que nous pouvons écrire en C en une ligne prennent plusieurs lignes en assembleur.)

  • Vous serez conscient de choses comme les registres du CPU, et la pile d’appel – ces ressources doivent être gérées pour qu’un programme fonctionne (et dans d’autres langages, ces ressources sont automatiquement gérées pour nous.) La conscience de ces ressources est critique pour l’appel et le retour des fonctions. (Les registres du CPU sont peu nombreux par rapport à la mémoire, cependant, ils sont super rapides, et aussi constants dans leurs performances.)

  • Il n’y a pas d’instructions structurées en langage d’assemblage – donc, toutes les constructions de flux de contrôle doivent être programmées dans un style « If-Goto » (dans d’autres langages, le compilateur traduit les instructions structurées en If-Goto.) Le style if-goto utilise des étiquettes comme cibles. (Bien que le C supporte l’utilisation de goto et d’étiquettes, les programmeurs préfèrent les déclarations structurées la plupart du temps, donc nous ne voyons pas souvent de goto et d’étiquettes en C.)

Langage d’assemblage contre le code machine

Le langage d’assemblage est destiné à être lu et écrit par les humains.

Étiquettes

En langage d’assemblage, nous utilisons beaucoup d’étiquettes. Les usages des étiquettes se traduisent par des nombres en code machine et les étiquettes disparaissent dans le code machine. Les étiquettes (par rapport à l’utilisation directe des nombres) permettent d’apporter de petites modifications aux programmes – sans elles, même un petit changement serait en fait un énorme changement.

Le processeur a besoin de savoir où se trouve un opérande, peut-être un opérande de données ou peut-être un emplacement de code. Pour les emplacements de code, par exemple, typiquement, le processeur voudra savoir à quelle distance se trouvent ces éléments de l’emplacement de code qu’il est en train d’exécuter – c’est ce qu’on appelle l’adressage relatif pc. Ainsi, pour exécuter une instruction si-alors, nous pourrions dire au processeur : sous certaines conditions, sauter N instructions, où N représente la taille ou le nombre d’instructions à sauter (ou non), c’est-à-dire que N est la taille de la partie alors – ce serait en code machine. Si nous devions ajouter une instruction à la then-part, le nombre d’instructions à sauter augmente (et si nous retirons une instruction de la then-part, diminue).

L’utilisation d’étiquettes tient automatiquement compte des changements (par ex. ajout, suppression d’instructions) – une instruction de branchement qui utilise une étiquette comme cible s’accommode des changements dans le compte du nombre d’instructions à sauter conditionnellement.

Pseudo-instructions

La plupart des intructions en langage d’assemblage se traduisent 1:1 en instructions de code machine. Cependant, occasionnellement, nous devons dire à l’assembleur quelque chose qui ne se traduit pas directement en code machine. Des exemples typiques sont de dire à l’assembleur que les instructions suivantes sont du code (habituellement via une directive .text) vs des données (habituellement via une directive .data). Comme les instructions régulières, ces instructions sont généralement écrites sur leur propre ligne, cependant, les informent l’assembleur plutôt que de générer directement des instructions de code machine.

Écrire le langage d’assemblage

En C, nous écrivons des déclarations ; ces déclarations s’exécutent consécutivement les unes après les autres, par défaut de manière séquentielle, jusqu’à ce qu’une certaine construction de flux de contrôle (boucle, if) change le flux. En langage d’assemblage, il en va de même.

En C, les instructions ont des effets sur les variables – on peut considérer que des instructions distinctes communiquent entre elles par l’intermédiaire de variables. Il en va de même en langage d’assemblage : les instructions communiquent entre elles par l’intermédiaire de l’effet qu’elles ont sur les variables, bien que, comme il y a plus d’instructions (nécessairement, car les instructions sont plus simples), il y aura aussi nécessairement plus de variables, beaucoup sont des temporaires de très courte durée utilisées pour communiquer d’une instruction à une autre.

Programmation de base en langage d’assemblage

Nous alternons entre la sélection des registres et la sélection des instructions. Comme les instructions en langage d’assemblage sont assez simples, nous avons souvent besoin d’utiliser plusieurs instructions, oui, interconnectées via des valeurs comme variables dans les registres du cpu. Lorsque cela se produit, nous aurons besoin de sélectionner un registre pour contenir les résultats intermédiaires.

Sélection de registre

Pour sélectionner le registre, nous avons besoin d’un modèle mental des registres qui sont occupés et ceux qui sont libres. Avec cette connaissance, nous pouvons choisir un registre libre à utiliser pour contenir un résultat temporaire (de courte durée). Ce registre restera occupé jusqu’à ce que nous l’utilisions pour la dernière fois, puis il redeviendra libre – une fois libre, il peut être réaffecté à quelque chose de complètement différent. (Dans un certain sens, le processeur ne sait pas vraiment que nous faisons ce repurposage constant car il ne connaît pas l’intention du programme.)

Sélection des instructions

Une fois que nous avons établi les ressources à utiliser, nous pouvons choisir l’instruction spécifique dont nous avons besoin. Souvent, nous ne trouverons pas une instruction qui fait exactement ce que nous voulons, donc nous devrons faire un travail avec 2 ou plusieurs instructions, ce qui signifie, yep, plus de sélection de registre pour les temporaires.

Par exemple, MIPS et RISC V sont des architectures similaires qui fournissent une instruction de branchement de comparaison &, mais ils ne peuvent comparer que deux registres du cpu. Ainsi, si nous voulons voir si un octet dans une chaîne de caractères est un certain caractère (comme un saut de ligne), alors nous devons fournir (charger) cette valeur de saut de ligne (une constante numérique) dans un registre cpu avant de pouvoir utiliser l’instruction de branchement comparer &.

Appel de fonctions &Retourner

Un programme se compose peut-être de milliers de fonctions ! Bien que ces fonctions doivent partager la mémoire, celle-ci est vaste, et elles peuvent donc s’entendre. Cependant, avec une unité centrale, il y a un nombre limité et fixe de registres (comme 32 ou 16), et, toutes les fonctions doivent partager ces registres !

Pour que cela fonctionne, les concepteurs de logiciels définissent des conventions – que l’on peut considérer comme des règles – pour partager ces ressources fixes limitées. En outre, ces conventions sont nécessaires pour qu’une fonction (un appelant) puisse appeler une autre fonction (un appelé). Les règles sont désignées sous le nom de convention d’appel, qui identifie pour l’appelant/le appelé, où placer/trouver les arguments et les valeurs de retour. Une convention d’appel fait partie de l’interface binaire d’application, qui nous indique en gros comment une fonction doit communiquer avec le monde extérieur (autres fonctions) en code machine. La convention d’appel est définie pour une utilisation logicielle (le matériel s’en moque).

Ces documents nous indiquent ce qu’il faut faire en matière de passage de paramètres, d’utilisation de registres, d’allocation/désallocation de pile et de valeurs de retour. Ces documents sont publiés par le fournisseur du jeu d’instructions, ou le fournisseur du compilateur, et sont bien connus afin que le code provenant de divers endroits puisse interagir correctement. Les conventions d’appel publiées doivent être considérées comme acquises, même si le matériel ne le sait pas.

Sans cette documentation, nous ne saurions pas comment faire ces choses (passage de paramètres, etc…). Nous ne sommes pas obligés d’utiliser les conventions d’appel standard, mais il n’est pas pratique d’inventer les nôtres pour autre chose qu’un simple programme jouet exécuté sur un simulateur. Rouler notre propre convention d’appel signifie aucune interopérabilité avec d’autres codes, ce que nous devons faire dans tout programme réaliste, comme faire des appels de système et de bibliothèque.

Le matériel ne sait pas ou ne se soucie pas des conventions d’utilisation du logiciel. Il se contente d’exécuter aveuglément instruction après instruction (très rapidement). Par exemple, pour le matériel, la plupart des registres sont équivalents, même si les registres sont partitionnés ou assignés à des utilisations particulières par les conventions logicielles.

Registres dédiés

Certains registres du CPU sont dédiés à des fins spécifiques. Par exemple, la plupart des processeurs de nos jours ont une forme de pointeur de pile. Sur MIPS et RISC V, le pointeur de pile est un registre ordinaire indifférencié des autres, sauf par définition des conventions logicielles – le choix du registre est arbitraire, mais il est bel et bien choisi, et ce choix est publié pour que nous sachions tous de quel registre il s’agit. Sur x86, par contre, le pointeur de pile est supporté par des instructions dédiées push & pop (ainsi que des instructions d’appel et de retour), donc pour le registre du pointeur de pile il n’y a qu’un seul choix logique.

Partitionnement des registres

Afin de partager les autres registres entre des milliers de fonctions, ils sont partitionnés, par convention logicielle, généralement en quatre ensembles :

  • Registres de passage de paramètres
  • Registres de réception de valeurs de retour
  • Registres « volatils »
  • Registres « non volatils »

Registres de retour de paramètres &

Les registres de paramètres servent à passer des paramètres ! En adhérant à la convention d’appel, les appelés savent où trouver leurs arguments, et les appelants savent où placer les valeurs des arguments. Les registres de paramètres sont utilisés pour le passage des paramètres par les règles établies dans la convention d’appel, et, lorsque plus de paramètres sont passés que la convention de l’unité centrale ne le permet, le reste des paramètres est passé en utilisant la mémoire de la pile.

Les registres de valeur de retour contiennent les valeurs de retour des fonctions, donc quand une fonction se termine, son travail est d’avoir mis sa valeur de retour dans le registre approprié, avant de retourner le flux de contrôle à l’appelant. En adhérant à la convention d’appel, l’appelant et l’appelé savent tous deux où trouver la valeur de retour de la fonction.

Registres volatiles &Registres non volatiles

Les registres volatiles sont libres d’être utilisés par n’importe quelle fonction sans se soucier de l’utilisation précédente ou des valeurs de ce registre. Ils sont bons pour les temporaires nécessaires à l’interconnexion d’une séquence d’instructions, et, ils peuvent également être utilisés pour les variables locales. Cependant, puisque ces registres sont libres d’être utilisés par n’importe quelle fonction, nous ne pouvons pas compter sur le fait qu’ils conservent leurs valeurs pendant un appel de fonction – par définition (convention logicielle) ! Ainsi, après le retour de la fonction, son appelant ne peut pas dire quelles valeurs se trouveraient dans ces registres – toute fonction est libre de les réutiliser, tout en sachant que nous ne pouvons pas compter sur la conservation de leurs valeurs après le retour d’une fonction appelée. Ainsi, ces registres sont utilisés pour les variables et les temporaires qui n’ont pas besoin de s’étendre sur un appel de fonction.

Pour les variables qui ont besoin de s’étendre sur un appel de fonction, il existe des registres non volatils (et la mémoire de pile, aussi). Ces registres sont préservés à travers un appel de fonction (par un logiciel qui adhère à la convention d’appel). Ainsi, si une fonction souhaite avoir une variable dans un registre rapide du processeur, mais que cette variable est active pendant un appel (définie avant un appel et utilisée après l’appel), nous pouvons choisir un registre non volatile, avec l’exigence (définie par la convention d’appel) que la valeur précédente du registre soit restaurée lors du retour à l’appelant. Cela nécessite de sauvegarder la valeur actuelle du registre non volatile (dans la mémoire de la pile) avant de réaffecter le registre, et de la restaurer (en utilisant la valeur précédente de la mémoire de la pile) lors du retour.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.