Les premiers pas
Le langage d'assemblage est souvent considéré comme particulièrement ardu à apprendre, et il devient encore plus difficile à expliquer, ce qui, de toute évidence, contribue à la difficulté de son apprentissage. La raison en est qu'il est quasiment impossible d'adopter une approche linéaire ou structurée dans laquelle on pourrait expliquer chaque élément d'une manière séquentielle, de la même façon qu'on le ferait pour des langages plus hauts niveaux comme BASIC ou Pascal. En effet, une instruction apparemment simple dans un langage comme BASIC ou Pascal se traduit par une série d'instructions plus complexes et détaillées lorsqu'on la transcrit en langage d'assemblage. Prenons un exemple simple :
En BASIC, une instruction comme :
- PRINT "Bonjour, comment allez-vous ?"
est facile à comprendre et se résume à une seule ligne de code. Cependant, en langage d'assemblage, la même tâche de afficher un message à l'écran nécessite plusieurs lignes d'instructions distinctes. Voici comment cela se traduit dans un programme d'assemblage 80x86 en mode réel sous un système d'exploitation DOS :
Ici, pour comprendre comment le programme fonctionne, il ne suffit pas de regarder une seule ligne de code et de savoir immédiatement ce qu'elle fait. En BASIC, on pourrait dire que l'instruction PRINT est simplement suivie du message entre guillemets, et que cette seule ligne d'instruction suffit pour afficher un message. Mais en langage d'assemblage, chaque détail compte et nécessite une explication plus approfondie. En effet, il est nécessaire de décomposer le programme d'assemblage en plusieurs parties pour comprendre son fonctionnement.
Par exemple, pour comprendre l'assembleur 80x86, il faudra d'abord expliquer :
- L'instruction DB (Define Byte) qui est utilisée pour déclarer une chaîne de caractères ou un message. Ici, cela signifie que le message à afficher est entreposé dans une zone mémoire et se termine par un caractère spécial '$', qui sert à marquer la fin de la chaîne dans les systèmes DOS.
- L'instruction MOV étant utilisée pour déplacer des données d'un registre à un autre. Ici, MOV AH, 09h charge dans le registre AH une valeur qui indique à l'ordinateur quel service DOS exécuter, en l'occurrence l'affichage d'une chaîne de caractères.
- L'utilisation de DX et OFFSET. DX est un registre dans lequel on stocke l'adresse mémoire du message à afficher, et OFFSET est utilisé pour obtenir l'adresse exacte de la variable Message en mémoire.
- La fonction INT 21h, qui appelle une fonction de l'interface DOS. L'interruption 21h est un moyen pour un programme d'interagir avec le système d'exploitation, et ici elle indique au système d'exécuter la fonction associée à l'affichage d'une chaîne de caractères, identifiée par la valeur dans AH.
Ainsi, tandis que le langage BASIC semble direct et facile à comprendre à première vue, l'assembleur exige une analyse détaillée de chaque instruction, des registres utilisés, des fonctions du système et de la manière dont les données sont manipulées au niveau le plus bas. Cette nécessité d'expliquer chaque petite partie du programme en assembleur peut rendre son apprentissage et son explication beaucoup plus complexes que dans des langages de programmation de haut niveau, où une seule instruction peut accomplir ce qui en assembleur nécessite plusieurs étapes distinctes.
Généralités
Dans cette section, nous allons aborder les concepts fondamentaux liés au fonctionnement d'un micro-ordinateur, notamment en utilisant un microprocesseur de la famille 80x86. Nous explorerons en détail la structure de la mémoire, l'entreposage des données, ainsi que les types d'instructions permettant de manipuler et de traiter ces informations au niveau du processeur. Nous étudierons aussi les bases de l'addition binaire, étant essentielle pour comprendre le fonctionnement des calculs dans un environnement numérique. De plus, nous verrons différentes méthodes de conversion entre les diverses représentations des données, ainsi que les différents modes d'adressage utilisés dans les instructions du processeur.
Les instructions du langage d'assemblage sont organisées en différentes catégories, chacune jouant un rôle spécifique dans le traitement des données et des opérations de base. Ces catégories incluent, entre autres, la définition et la gestion des données, le transfert des informations entre différents registres et zones mémoire, ainsi que la gestion de la pile. La pile est un espace mémoire crucial pour l'entreposage temporaire de données pendant l'exécution des programmes, et nous verrons comment elle peut être manipulée efficacement.
Une autre catégorie d'instructions traite des ruptures de séquence, permettant de contrôler le flux d'exécution du programme, notamment en introduisant des sauts conditionnels ou des appels à des sous-programmes. Nous explorerons aussi les instructions dédiées à la manipulation de chaînes de caractères, une fonctionnalité essentielle pour la gestion des textes et des données sous forme de chaînes, que ce soit pour l'affichage à l'écran ou pour la communication avec d'autres programmes.
En outre, nous aborderons les opérations logiques, incluant des instructions permettant de réaliser des tests et des manipulations sur des bits individuels ou des groupes de bits dans des données, et les opérations arithmétiques, qui sont cruciales pour les calculs mathématiques de base comme l'addition, la soustraction, la multiplication et la division.
Toutes les instructions que nous examinerons dans cette section nous permettront d'écrire des programmes pouvant être exécutés sur une gamme de microprocesseurs compatibles 80x86, y compris les modèles 8088, 8086, 80286 et 80386. Ces processeurs, bien que différents dans leur architecture et leurs capacités, partagent une compatibilité avec les instructions que nous allons apprendre, permettant ainsi aux programmes d'être transférés et exécutés sur plusieurs générations de microprocesseurs sans nécessiter de modifications substantielles du code source.
Les outils nécessaires
Il existe plusieurs méthodes pour encoder un programme en assembleur pour la famille de microprocesseurs 80x86. Chacune de ces méthodes a ses avantages et ses inconvénients, et le choix de la méthode dépend principalement de la taille et de la complexité du programme que vous souhaitez développer. La première méthode consiste à coder les instructions directement en mémoire à l'aide d'un programme spécifique, tel que le programme DEBUG. Cette approche, bien que relativement simple, présente des limites importantes. En effet, elle ne convient que pour de très petits programmes, car elle impose une gestion manuelle et en temps réel de la mémoire, ce qui devient vite impraticable pour des programmes de plus grande envergure. Nous verrons dans cette section comment cette méthode fonctionne et pourquoi elle est mieux adaptée à des cas très simples.
La seconde méthode, bien plus courante et adaptée aux programmes de plus grande taille, consiste à encoder le code source dans un fichier texte, puis à assembler ce fichier pour générer un programme exécutable. Cette méthode est largement utilisée dans le développement de logiciels en assembleur, car elle offre une flexibilité et une efficacité accrues. Le processus d'assemblage transforme un fichier source contenant des instructions en assembleur en un fichier exécutable, prêt à être exécuté sur un microprocesseur 80x86. Cependant, pour arriver à ce stade, plusieurs étapes sont nécessaires, et chacune d'elles nécessite des outils spécifiques et des connaissances techniques. Voici les étapes détaillées de ce processus :
- Préparation du programme : Avant même d'entrer dans la phase de codage proprement dite, il est crucial de bien préparer le programme, en définissant sa structure, ses algorithmes et ses méthodes de calcul. Cette phase de planification est essentielle et doit être réalisée sur papier ou à l'aide de diagrammes pour s'assurer que le programme est bien conçu et organisé. Une planification soigneuse permet d'éviter des erreurs coûteuses dans les étapes ultérieures et garantit une meilleure lisibilité du code source.
- Codage du programme source : Une fois la structure du programme définie, il faut coder le programme source en assembleur dans un fichier texte. Ce fichier texte sera ensuite utilisé pour générer le programme exécutable. Le code source doit être écrit en utilisant des instructions d'assembleur, étant des commandes compréhensibles par le processeur. Pour cela, il existe plusieurs éditeurs de texte qui permettent de créer ce fichier, comme les éditeurs de lignes EDLIN ou EDIT, ou tout autre traitement de texte capable de sauvegarder un fichier au format ASCII. Il est important de noter qu'EDLIN n'est pas un éditeur très pratique et qu'il n'est recommandé que pour des programmes très simples et courts. Il est difficile à manouvrer, surtout pour des projets plus complexes. En revanche, des éditeurs plus adaptés, comme l'éditeur intégré dans Turbo Pascal, peuvent grandement faciliter la tâche. Si vous disposez de l'outil TURBO.COM, il est fortement conseillé de l'utiliser, car il simplifie considérablement la rédaction du code.
- Transformation du code source en langage machine : L'étape suivante consiste à convertir le code source en un fichier exécutable, ce qui nécessite l'utilisation d'un assembleur, tel que MASM (Microsoft Macro Assembler) ou TASM (Turbo Assembler). Ce processus, appelé l'assemblage, prend le code source écrit en assembleur et le transforme en langage machine, un langage binaire directement compréhensible par le microprocesseur. Pendant cette étape, l'assembleur va également vérifier le code pour détecter d'éventuelles erreurs de syntaxe ou de logique. Si des erreurs sont trouvées, il sera nécessaire de corriger le code source et de relancer la phase de transformation. Cette phase peut également être accompagnée d'un processus de lien, où des outils comme LINK.EXE et EXE2BIN.EXE, disponibles sur les disquettes MS-DOS, sont utilisés pour finaliser la création du fichier exécutable.
- Vérification du bon fonctionnement du programme : Après avoir généré l'exécutable, il est impératif de tester le programme pour vérifier qu'il fonctionne comme prévu. Pour cette étape, des outils comme DEBUG, étant inclus avec MS-DOS (fichier DEBUG.COM), peuvent être d'une grande utilité. DEBUG permet d'exécuter un programme ligne par ligne, en offrant la possibilité de voir ce qui se passe à chaque étape de l'exécution du programme. Cette méthode permet de repérer les erreurs ou les dysfonctionnements du programme et de les corriger efficacement. DEBUG offre également une fonctionnalité de débogage en temps réel, qui est essentielle pour les programmes écrits en assembleur, où les erreurs peuvent être difficiles à repérer.
Ces étapes forment le cycle de développement classique pour un programme en assembleur destiné aux microprocesseurs 80x86. Le processus complet peut sembler long et complexe, mais il offre un contrôle total sur chaque aspect du programme, de la conception initiale à la génération du fichier exécutable final. Grâce à une préparation minutieuse et l'utilisation des outils adéquats, le développement de programmes en assembleur devient une tâche plus abordable, même pour des projets relativement ambitieux.
Fonctionnement d'un micro-ordinateur 80x86
L'apprentissage de l'assembleur 80x86 exige de comprendre certains principes fondamentaux du fonctionnement matériel, car ce langage est très proche de l'architecture de la machine elle-même. Contrairement aux langages de haut niveau masquant les détails internes du microprocesseur, l'assembleur reflète directement le fonctionnement du microprocesseur. Avant de plonger dans l'étude détaillée du langage assembleur 80x86, il est donc indispensable de se familiariser avec quelques concepts essentiels concernant l'organisation de la mémoire, des données, des instructions, et le traitement des informations dans un micro-ordinateur.
L'élément de base de tout système informatique est le bit, qui ne peut prendre que deux valeurs : 0 ou 1. Ces deux états correspondent à une situation électrique, soit éteinte (0) soit allumée (1), ce que l'on assimile souvent aux notions de FAUX et VRAI dans la logique binaire. Un bit, à lui seul, étant très limité en termes de capacité d'information, plusieurs bits sont regroupés pour former des unités plus grandes : un octet (8 bits), un mot (16 bits), un double mot (32 bits), ou encore un quadruple mot (64 bits). Cette structuration permet de représenter une quantité bien plus vaste d'informations dans la mémoire et les registres du processeur.
Le rôle principal d'un micro-ordinateur est d'exécuter des programmes, lesquels manipulent soit des données numériques, soit des caractères alphabétiques. Afin de permettre cette manipulation, il faut pouvoir représenter en mémoire trois grandes catégories d'éléments :
- des instructions (les ordres donnés au microprocesseur);
- des données numériques (nombres, résultats de calculs,...);
- des caractères (lettres, chiffres, symboles).
Les instructions
Chaque instruction du processeur est codée sous forme binaire. Grâce à un ensemble de n bits, il est possible de créer 2^n combinaisons différentes. Par exemple, avec 8 bits, on peut obtenir 256 combinaisons possibles. Cela suffit à représenter un grand nombre d'instructions différentes. Chacune de ces combinaisons correspondra donc à une commande précise que le microprocesseur saura interpréter et exécuter. Lorsque le processeur rencontre une de ces combinaisons binaires, il exécutera une opération spécifique : addition, déplacement de données, branchement conditionnel,... Le mécanisme précis de décodage et d'exécution de ces instructions sera détaillé dans une section ultérieure.
Les caractères
Les caractères, eux aussi, sont représentés par des combinaisons de bits. Pour standardiser la correspondance entre une combinaison binaire et un caractère, on utilise un codage connu sous le nom de séquence ASCII (American Standard Code for Information Interchange). Dans ce système, chaque caractère (lettre, chiffre, ponctuation,...) est associé à une valeur codée sur 8 bits. Par exemple, la lettre 'A' correspond à la valeur binaire 01000001. Cela permet d'afficher et de manipuler du texte dans les programmes, en traduisant simplement des séries de bits en caractères lisibles.
Les données numériques
La représentation des données numériques dans un micro-ordinateur est légèrement plus complexe que celle des instructions ou des caractères. Sur un simple octet (8 bits), nous ne pouvons représenter que 256 valeurs différentes (de 0 à 255 en notation non signée), ce qui s'avère rapidement insuffisant pour beaucoup d'applications. De plus, pour pouvoir effectuer des calculs, il est nécessaire de représenter non seulement des entiers positifs, mais aussi des nombres négatifs, des zéros, et parfois des nombres très grands.
Pour comprendre la manière dont les nombres sont construits, analysons comment les humains manipulent les nombres au quotidien. Examinons les exemples suivants :
|
Le nombre 7 = 7 × 1 Le nombre 27 = 2 × 10 + 7 × 1 Le nombre 127 = 1 × 100 + 2 × 10 + 7 × 1 Le nombre 4127 = 4 × 1000 + 1 × 100 + 2 × 10 + 7 × 1 Le nombre 84127 = 8 × 10000 + 4 × 1000 + 1 × 100 + 2 × 10 + 7 × 1 |
La représentation décimale
De cet exemple, on observe que chaque chiffre, en partant de la droite, est multiplié par une puissance croissante de 10 : unités, dizaines, centaines, milliers,... Ces puissances correspondent à 100, 101, 102, 103, et ainsi de suite. Ce système, basé sur 10 chiffres (de 0 à 9), est appelé représentation en base 10, ou plus simplement représentation décimale.
Ce principe de pondération par puissances de 10 est universellement utilisé pour représenter tous les nombres dans notre système quotidien. Dans un ordinateur, cependant, les puissances ne sont pas basées sur 10, mais sur 2, car tout est fondé sur des états binaires. La conversion entre ces bases est essentielle pour comprendre comment les ordinateurs entreposent et manipulent les données numériques (0 et base-1).
La représentation binaire
Après avoir exploré la représentation décimale traditionnelle, tentons maintenant d'appliquer une méthode équivalente avec seulement les deux signes disponibles dans un ordinateur, à savoir 0 et 1. Rappelons-nous qu'en base 10, nous utilisions dix chiffres distincts allant de 0 à 9, où 9 est égal à la base moins 1. De façon similaire, en informatique, nous n'avons que deux symboles disponibles, 0 et 1, et ici 1 représente la base moins 1, ce qui confirme que nous travaillons en base 2.
La méthode pour représenter un nombre en base 2, autrement appelée représentation binaire, repose donc sur un principe analogue à celui utilisé en base 10, mais adapté aux puissances de 2 :
- Combiner une série de signes 0 et 1 de façon séquentielle ;
- Multiplier celui le plus à droite par 2 exposant 0, soit 1 ;
- Multiplier celui juste à gauche par 2 exposant 1, soit 2 ;
- Multiplier ensuite par 2 exposant 2, soit 4 ;
- Continuer ainsi en multipliant par 2 exposant 3 (8), 2 exposant 4 (16), et ainsi de suite.
Voyons quelques exemples pour rendre cela plus clair :
| Base 2 | Base 10 |
|---|---|
| 00000001 | 1 = 1 × 1 |
| 00000011 | 3 = (1 × 2) + (1 × 1) |
| 00001011 | 11 = (1 × 8) + (0 × 4) + (1 × 2) + (1 × 1) |
| 00011011 | 27 = (1 × 16) + (1 × 8) + (0 × 4) + (1 × 2) + (1 × 1) |
Chaque chiffre binaire correspond donc à une puissance croissante de 2 en allant de droite à gauche, exactement comme chaque chiffre décimal correspond à une puissance croissante de 10.
Remarque importante : En mathématiques, il est essentiel de se souvenir que tout nombre, aussi grand soit-il, élevé à la puissance 0 est toujours égal à 1. C'est ce principe fondamental justifiant pourquoi, dans toutes les bases (et notamment en binaire), le chiffre le plus à droite est multiplié par 1.
Ainsi, pour calculer la valeur en base 10 d'un nombre en binaire, nous multiplions simplement chaque chiffre 0 ou 1 par les puissances successives de 2 (1, 2, 4, 8, 16, 32, 64, 128, etc.) en fonction de sa position, puis nous additionnons les résultats obtenus. Cette technique est ce que l'on appelle la représentation en base 2, ou plus simplement représentation binaire.
Limites d'un octet
Dans un micro-ordinateur standard, un octet est constitué de 8 bits. Cela signifie que le plus grand nombre entier que l'on peut représenter sur 8 bits est :
| 11111111 en binaire = 255 en décimal |
Cela s'explique par la formule générale :
| 2^8 - 1 = 255 |
où 8 correspond au nombre de bits dans l'octet. On enlève 1 car on commence à compter à partir de 0.
Extension sur plusieurs octets
Lorsque l'on veut représenter des nombres plus grands que 255, un seul octet ne suffit plus. Il est alors nécessaire de regrouper plusieurs octets ensemble. Les représentations les plus utilisées dans les systèmes 80x86 sont sur :
- 1 octet : capacité maximale = 28 - 1 = 255
- 2 octets (un mot) : capacité maximale = 216 - 1 = 65 535
- 4 octets (un double mot) : capacité maximale = 232 - 1 = 4 294 967 295
Chaque fois que l'on double le nombre d'octets, on multiplie de manière exponentielle la quantité d'informations pouvant être représentée.
Les nombres négatifs
Jusqu'à présent, nous avons parlé exclusivement de nombres positifs. Pourtant, dans la réalité et dans la programmation, il est tout aussi essentiel de pouvoir manipuler des nombres négatifs. Comment faire cela en assembleur 80x86 ? Peut-on simplement ajouter un caractère supplémentaire contenant le signe moins ('-'), comme on le ferait dans une chaîne de texte en ASCII ? Évidemment, la réponse est non.
La représentation des nombres négatifs doit être intégrée directement dans la structure binaire des données pour rester efficace et rapide. Heureusement, il n'existe que deux états pour un nombre : positif ou négatif. Cette situation binaire se prête parfaitement au codage sur un seul bit. Ainsi, au lieu d'utiliser un caractère supplémentaire, le bit de poids fort (le bit situé le plus à gauche dans l'ensemble des bits représentant le nombre) est réservé pour représenter le signe :
- Si ce bit est égal à 0, alors le nombre est positif.
- Si ce bit est égal à 1, alors le nombre est négatif.
Ce système est extrêmement efficace mais a un coût : puisqu'un bit est réservé au signe, il reste moins de bits disponibles pour la valeur numérique proprement dite.
Voyons concrètement ce que cela implique :
| Taille | Composition (signe + valeur) | Intervalle représentable |
|---|---|---|
| 1 octet (8 bits) | 1 bit de signe + 7 bits de valeur | de -127 à +128 soit -(27-1) à 27 |
| 1 mot (16 bits) | 1 bit de signe + 15 bits de valeur | de -32 767 à +32 768 soit -(215-1) à 215 |
| 1 double mot (32 bits) | 1 bit de signe + 31 bits de valeur | de -2 147 483 647 à +2 147 483 648 soit -(231-1) à 231 |
| 1 quadruple mot (64 bits) | 1 bit de signe + 63 bits de valeur | de -9 223 372 036 854 775 807 à +9 223 372 036 854 775 808 soit -(263-1) à 263 |
Par exemple :
- Avec 8 bits, vous pouvez coder 256 valeurs distinctes, mais en mode signé, elles se répartissent entre les valeurs négatives et positives.
- De la même manière, avec 16 bits, on atteint 65 536 valeurs, mais en mode signé, elles vont de -32 767 à +32 768.
Particularité du processeur 8088 et des suivants
À partir du processeur 8088, l'architecture du 80x86 introduit une grande souplesse : le microprocesseur peut manipuler les nombres en arithmétique signée (c'est-à-dire en tenant compte du bit de signe) ou en arithmétique non signée (où tous les bits servent uniquement pour représenter une valeur positive). Cela signifie que le même ensemble de bits peut être interprété de deux manières différentes selon l'instruction utilisée :
- Signé : prise en compte du bit de signe, ce qui permet de travailler avec des nombres négatifs.
- Non signé : tous les bits sont utilisés pour représenter uniquement des valeurs positives, ce qui double pratiquement la valeur maximale atteignable.
Ce double mode de fonctionnement est l'une des forces du langage assembleur 80x86, car il offre au programmeur une grande flexibilité pour optimiser la mémoire et le traitement selon les besoins spécifiques de l'application.
Le poids des bits et des octets
En assembleur 80x86, il existe une convention fondamentale qu'il est essentiel de bien comprendre avant de commencer à manipuler efficacement les données en mémoire : la numérotation et l'importance relative des bits dans un octet. Par convention, le bit situé le plus à droite dans un octet est désigné comme le bit numéro 0, tandis que le bit tout à gauche est numéroté bit numéro 7.
Cette distinction n'est pas seulement une question de position : elle est également liée à la valeur que chaque bit représente. Le bit numéro 0 est appelé "bit de poids faible" ou "Least Significant Bit" (LSB), car c'est celui qui est multiplié par la plus petite puissance de 2 (20, soit 1). À l'inverse, le bit numéro 7 est connu comme le "bit de poids fort" ou "Most Significant Bit" (MSB), car il est associé à la puissance de 2 la plus élevée dans l'octet (27, soit 128).
Cette notion de poids ne s'arrête pas aux bits individuels : elle s'étend également aux ensembles de données plus grands comme les mots (16 bits) et les doubles mots (32 bits). Pour un mot, constitué de deux octets, on parlera alors de "l'octet de poids faible" pour celui de droite, et de "l'octet de poids fort" pour celui de gauche. De la même manière, lorsqu'on travaille avec un double mot (deux mots), le premier mot est le mot de poids faible et le second est le mot de poids fort.
En pratique, cela signifie que lors de l'entreposage en mémoire, les éléments de poids faible viennent avant les éléments de poids fort. C'est ce que l'on appelle le format Petit-boutiste (little-endian), étant utilisé par les microprocesseurs 80x86. Ainsi, pour un mot de 16 bits par exemple, les 8 bits les moins significatifs seront entreposés à l'adresse la plus basse en mémoire, et les 8 bits les plus significatifs à l'adresse suivante.
Comprendre cette organisation est indispensable lorsqu'on manipule directement les registres ou qu'on accède à la mémoire avec précision, car de nombreuses opérations en assembleur exploitent cette hiérarchie entre poids faible et poids fort pour le traitement efficace des données.
la représentation hexadécimale
Lorsqu'on travaille en assembleur 80x86, il devient vite indispensable de maîtriser une dernière forme de représentation très fréquemment utilisée : la notation hexadécimale, aussi appelée notation en base 16. Cette méthode d'écriture se révèle particulièrement précieuse pour manipuler efficacement de grandes quantités de bits, notamment lorsque les données dépassent 8 bits et s'étendent sur 2 octets (16 bits) ou plus. Dans ces cas, écrire ou lire les nombres en base binaire ou même en base décimale devient non seulement laborieux, mais également source de nombreuses erreurs humaines.
Pour remédier à ce problème, la notation hexadécimale a été introduite. Elle repose sur la base 16, un choix loin d'être anodin : 16 est une puissance exacte de 2 (2? = 16), ce qui rend la conversion entre binaire et hexadécimal extrêmement rapide et directe. En effet, chaque groupe de 4 bits (un "nibble") peut être représenté par un seul chiffre hexadécimal, ce qui simplifie considérablement la lecture et l'écriture des valeurs binaires longues.
Cependant, comme une base 16 nécessite 16 symboles distincts et que les chiffres arabes standards ne couvrent que de 0 à 9, il a fallu compléter les 6 valeurs manquantes en utilisant les premières lettres de l'alphabet. Ainsi, en hexadécimal, les correspondances suivantes sont établies :
| Hexadécimal | Décimal |
|---|---|
| A | 10 |
| B | 11 |
| C | 12 |
| D | 13 |
| E | 14 |
| F | 15 |
Voyons maintenant quelques exemples concrets de conversion d'une valeur hexadécimale vers sa valeur décimale :
- 8 correspond simplement à 8 × 1 = 8 en base 10.
- 78 correspond à 7 × 16 + 8 × 1 = 112 + 8 = 120 en base 10.
- A78 correspond à 10 × 256 + 7 × 16 + 8 × 1 = 2560 + 112 + 8 = 2680 en base 10.
- EA78 correspond à 14 × 4096 + 10 × 256 + 7 × 16 + 8 × 1 = 57344 + 2560 + 112 + 8 = 60024 en base 10.
Conversion binaire ↔ hexadécimal
La grande force de l'hexadécimal réside dans la facilité de conversion entre binaire et hexadécimal. La règle est simple : chaque chiffre hexadécimal correspond exactement à 4 bits binaires.
Prenons l'exemple du nombre hexadécimal EA78. Pour le convertir en binaire, il suffit de convertir séparément chaque chiffre hexadécimal en sa valeur binaire équivalente sur 4 bits :
| Hexadécimal | Décimal | Binaire |
|---|---|---|
| E | 14 | 1110 |
| A | 10 | 1010 |
| 7 | 7 | 0111 |
| 8 | 8 | 1000 |
En alignant ces représentations binaires, on obtient :
| EA78 (hexadécimal) → 1110101001111000 (binaire) |
On voit immédiatement que la représentation binaire brute est beaucoup plus longue et difficile à lire que son équivalent hexadécimal compact.
À l'inverse, pour convertir un nombre binaire en hexadécimal, la méthode est également très intuitive : il suffit de regrouper les bits par paquets de 4, en partant de la droite, et de convertir chaque groupe.
Prenons le nombre binaire 1110101001111000. En le découpant par groupes de 4 bits :
| 1110 1010 0111 1000 |
Et en convertissant chaque groupe :
| Binaire | Décimal | Hexadécimal |
|---|---|---|
| 1110 | 14 | E |
| 1010 | 10 | A |
| 0111 | 7 | 7 |
| 1000 | 8 | 8 |
Nous retrouvons donc le nombre hexadécimal :
| 1110101001111000 (binaire) → EA78 (hexadécimal) |
La mémoire
La mémoire d'un micro-ordinateur basé sur un processeur de la famille 80x86 peut être vue comme une grande série d'emplacements alignés les uns à la suite des autres, chacun capable d'entreposer une valeur. Ces emplacements sont organisés sous forme d'une séquence continue de cellules mémoire, chacune identifiée par un numéro unique appelé adresse. Chaque cellule, ou emplacement mémoire, est constituée d'un ensemble fixe de 8 bits, ce que l'on appelle un octet.
Sur les micro-ordinateurs compatibles IBM PC, la mémoire physique est organisée de manière un peu plus complexe : elle est divisée en zones appelées segments. Chaque segment contient exactement 65 536 octets, soit 64 kilo-octets. Cela facilite l'organisation de la mémoire et permet au processeur d'accéder efficacement aux différentes zones.
Ainsi, pour désigner précisément un emplacement mémoire en environnement mode réel ou mode protégé, on utilise deux éléments :
- Un numéro de segment : il désigne la base d'un bloc de 64 Ko.
- Un déplacement (offset en anglais) : il indique combien d'octets il faut compter à partir du début du segment pour atteindre l'octet voulu.
L'adresse mémoire est alors exprimée sous la forme :
| Segment:Déplacement (ou Segment:Offset en anglais) |
Cette méthode permet de travailler sur de grandes quantités de mémoire tout en utilisant des adresses sur 16 bits, ce qui était très pratique à l'époque de l'introduction de ces processeurs.
Les opérations du processeur sur la mémoire
Le microprocesseur 80x86 peut effectuer principalement deux types d'opérations sur les octets entreposés en mémoire :
- La modification du contenu d'un emplacement mémoire : dans ce cas, l'ancienne valeur présente à cette adresse est totalement écrasée par la nouvelle valeur écrite.
- La consultation ou lecture du contenu d'un emplacement mémoire : ici, le processeur lit la valeur stockée sans modifier ni altérer cette valeur. La mémoire reste intacte après une lecture.
Notes importantes concernant la mémoire
Quelques éléments fondamentaux à toujours garder en tête :
- Adresses séquentielles : les emplacements de mémoire sont numérotés de façon continue et séquentielle, en commençant toujours par 0.
- Contenu initial indéfini : lorsqu'un ordinateur est démarré, les contenus de la mémoire sont aléatoires ou indéfinis. Ils ne sont pas automatiquement initialisés à une valeur précise comme 0, ce qui impose au programmeur d'initialiser explicitement les zones mémoire utilisées.
Entreposage des données en mémoire
L'entreposage des différentes données en mémoire dépendra du type de données manipulées :
- Les instructions : chaque instruction du processeur occupe un ou plusieurs octets consécutifs en mémoire. La taille dépend de la complexité et des paramètres utilisés par l'instruction. Par exemple, une instruction simple peut tenir sur 1 ou 2 octets, tandis qu'une instruction plus complexe nécessitant des adresses complètes ou des constantes étendues peut en utiliser 5, 6 ou plus.
- Les caractères : en assemblage standard (basé sur l'encodage ASCII), chaque caractère occupe un seul octet de mémoire. Une chaîne de caractères (string) sera donc entreposée en mémoire sous forme de plusieurs octets consécutifs, chacun représentant un caractère individuel.
- Les valeurs numériques : pour les entiers, la place occupée dépend de leur taille :
- 1 octet pour des entiers sur 8 bits;
- 2 octets pour des entiers sur 16 bits;
- 4 octets pour des entiers sur 32 bits.
Dans chaque cas, ces octets seront alloués de manière contiguë en mémoire.
Ainsi, selon les besoins, les données occuperont une zone plus ou moins étendue, mais toujours continue, facilitant l'accès rapide et ordonné par le microprocesseur.
Les registres
Un micro-ordinateur, tel qu'un PC basé sur un processeur de la famille 80x86, est conçu principalement pour exécuter des programmes. Ces programmes ont pour mission de manipuler des données, communiquer avec les périphériques (clavier, écran, disque dur,...) et réaliser des traitements variés. Dans un tel environnement, les programmes sont eux-mêmes représentés par une suite d'octets stockés dans la mémoire de l'ordinateur. De même, les données que manipulent ces programmes sont également entreposées sous forme d'octets dans cette même mémoire principale.
Puisque programmes et données cohabitent dans la même mémoire physique, il est absolument nécessaire de pouvoir les distinguer clairement. C'est ici qu'interviennent les registres du microprocesseur : ce sont des emplacements spécifiques de mémoire rapide intégrés directement dans le microprocesseur, chacun ayant un rôle bien défini.
Certaines de ces unités d'entreposage serviront à indiquer les segments mémoire où se trouvent les programmes et où résident les données, d'autres seront utilisées pour stocker des résultats intermédiaires lors des calculs ou manipulations. Enfin, certains registres spéciaux servent à contrôler l'exécution des instructions ou à refléter l'état interne du processeur.
Le premier microprocesseur de la famille, le 8088, disposait de 14 registres principaux, répartis en quatre grandes catégories :
- Les registres de segment : Ces registres servent à spécifier dans quelle partie de la mémoire se trouvent les différentes zones :
- Le segment de code (programme);
- Le segment de données;
- Le segment de pile (stack);
- D'autres segments supplémentaires.
- Les registres de travail (ou registres généraux) : Ce sont des registres polyvalents, utilisés pour entreposer temporairement des résultats de calculs ou pour manipuler les données directement :
- Les registres de déplacement : Ces registres représentent un déplacement relatif à un segment donné. Ils sont essentiels pour accéder à différents emplacements mémoire :
- Le registre de drapeaux : Le registre FLAG est un registre très particulier :
- Il contient une série de bits d'état qui indiquent les résultats des opérations (par exemple, si un résultat est nul, s'il y a eu une retenue, un dépassement,...).
- Il est essentiel pour contrôler les branchements conditionnels et pour savoir comment poursuivre l'exécution du programme.
Registres concernés :
| Registre | Description | |
|---|---|---|
| CS (Code Segment) | Contient l'adresse du segment de code (programme en cours d'exécution). | |
| DS (Data Segment) | Contient l'adresse du segment de données. | |
| SS (Stack Segment) | Contient l'adresse du segment de pile. | |
| ES (Extra Segment) | Utilisé pour des accès mémoire supplémentaires. |
| Registre | Description | |
|---|---|---|
| AX (Accumulator Register) | Souvent utilisé pour les opérations arithmétiques. | |
| BX (Base Register) | Utilisé dans les calculs d'adresses. | |
| CX (Counter Register) | Utilisé pour le comptage dans les boucles ou les décalages. | |
| DX (Data Register) | Utilisé pour certaines opérations de multiplication ou d'entrée/sortie. |
| Registre | Description | |
|---|---|---|
| SI (Source Index) | Index source pour certaines opérations de transfert. | |
| DI (Destination Index) | Index destination pour certaines opérations de transfert. | |
| IP (Instruction Pointer) | Contient l'adresse de l'instruction suivante à exécuter. | |
| BP (Base Pointer) | Utilisé pour accéder aux paramètres de fonctions sur la pile. | |
| SP (Stack Pointer) | Pointe vers le sommet de la pile. |
Évolution avec les processeurs 32 bits (à partir du 80386)
Avec l'arrivée du processeur 80386, les microprocesseurs passent au traitement 32 bits. Pour tenir compte de ce changement, les registres sont étendus à 32 bits et renommés :
- Registres de segments :
- CS, DS, SS, ES (toujours présents),
- FS et GS (nouveaux registres pour gérer plus de segments).
- Registres de travail (32 bits) :
- EAX, EBX, ECX, EDX (préfixe "E" pour "Extended").
- Registres de déplacement (32 bits) :
- ESI (Extended SI),
- EDI (Extended DI),
- EIP (Extended IP),
- EBP (Extended BP),
- ESP (Extended SP).
- Registre de drapeaux :
- EFLAGS : extension 32 bits du registre de drapeaux.
Évolution vers le 64 bits (architecture x64)
Avec les processeurs récents basés sur l'architecture x64, les registres évoluent à nouveau pour supporter le traitement 64 bits, permettant de manipuler des volumes de données bien plus importants :
- Registres de travail (64 bits) :
- RAX, RBX, RCX, RDX (le préfixe "R" indique 64 bits).
- Registres de déplacement (64 bits) :
- RSI, RDI, RIP, RBP, RSP.
- Registre de drapeaux :
- RFLAGS, correspondant à la version 64 bits du registre de drapeaux.
Remarque finale : Il existe également d'autres registres internes utilisés exclusivement par le microprocesseur pour ses propres opérations (comme la gestion de la mémoire virtuelle, la protection matérielle,...). Ces registres ne sont pas accessibles directement par les programmes en assembleur standard et ne sont donc pas détaillés ici.
Le microprocesseur
Le microprocesseur appartenant à la célèbre famille 80x86 constitue véritablement le cour névralgique d'un micro-ordinateur. C'est lui orchestrant, dirigeant et contrôlant la quasi-totalité des opérations qui s'y déroulent. Il est conçu pour être rapide, polyvalent, mais en réalité, il n'est capable que d'exécuter un ensemble limité d'instructions élémentaires. Ces instructions, bien que simples individuellement, permettent, une fois combinées dans des programmes plus complexes, de réaliser des tâches très sophistiquées.
Les types d'opérations que peut exécuter un microprocesseur 80x86 sont les suivants :
- Déplacement de données en mémoire : Le microprocesseur est capable de transférer des informations d'un emplacement mémoire à un autre, ou entre un registre et la mémoire. Cette opération, appelée MOV en assembleur, est fondamentale pour le traitement des programmes, car manipuler efficacement les données est essentiel.
- Inversion de bits (NOT) : Le microprocesseur peut inverser l'état de chacun des bits d'une valeur binaire. Cela signifie que tous les bits à 0 deviennent des 1, et vice versa. Cette opération, appelée NOT, est couramment utilisée en logique binaire pour obtenir des résultats complémentaires ou pour annuler certains états.
- Opérations logiques (AND, OR, XOR) : Le microprocesseur peut effectuer diverses opérations logiques entre deux valeurs binaires :
- Addition de bits : Le microprocesseur est aussi capable de réaliser des additions de nombres binaires. L'addition est l'une des opérations fondamentales de l'informatique, qu'il s'agisse de simples calculs arithmétiques, de calculs d'adresses ou de traitements de données complexes. Cette addition est effectuée bit par bit, en tenant compte des retenues éventuelles.
- Déplacements et rotations de bits : Le microprocesseur peut également déplacer les bits d'une valeur vers la gauche ou vers la droite (SHIFT) ou bien faire tourner ces bits circulairement (ROTATE).
- Et d'autres opérations encore : Au-delà de ces instructions de base, le processeur est aussi capable d'effectuer d'autres opérations comme :
| Instruction | Description |
|---|---|
| AND (et logique) | Le résultat est 1 seulement si les deux bits sont à 1 ; |
| OR (ou logique) | Le résultat est 1 si au moins l'un des deux bits est à 1 ; |
| XOR (ou exclusif) | Le résultat est 1 si un seul des deux bits est à 1, mais pas les deux. |
Ces opérations sont particulièrement importantes pour des tâches telles que le masquage de bits, la création de conditions complexes, ou encore la gestion de drapeaux dans les programmes.
Ces opérations sont cruciales pour effectuer des multiplications ou divisions rapides par des puissances de deux, pour manipuler des structures de bits spécifiques, ou encore pour crypter des données.
La syntaxe
Comme dans tous les langages de programmation, qu'ils soient de haut ou de bas niveau, il existe en assembleur 80x86 un ensemble strict de règles que tout programmeur doit impérativement respecter. Ces règles, définissant la syntaxe, sont essentielles pour que le code soit correctement compris et interprété par l'assembleur 80x86, puis exécuté par le microprocesseur. Cette section va détailler en profondeur l'ensemble des caractères autorisés ainsi que quelques grandes conventions de syntaxe propres à ce langage.
L'ensemble des caractères autorisés
L'assembleur 80x86 reconnaît uniquement un sous-ensemble de caractères issus du standard ASCII (American Standard Code for Information Interchange). Tous les caractères ne sont donc pas acceptés dans l'écriture du code source. Les caractères valides incluent :
- Les lettres de l'alphabet (A à Z et a à z). Attention : les caractères accentués (é, è, à, ...) ne sont pas autorisés et doivent être évités.
- Les chiffres numériques (0 à 9), utilisés principalement pour représenter des constantes ou pour définir des noms d'identificateurs lorsqu'ils ne débutent pas par un chiffre.
- Une série spécifique de caractères spéciaux, parmi lesquels : + - * / = ( ) [ ] ; ' . ! , _ : @ $ ainsi que les caractères de contrôle suivants : espace ( ), tabulation (TAB), retour chariot (RETURN) et saut de ligne (LF).
En ce qui concerne les lettres, l'assembleur ne fait pas de différence entre majuscules et minuscules, sauf à l'intérieur des chaînes de caractères, où la casse est strictement respectée. Voici quelques exemples pour illustrer cela :
| Code | Interprétation |
|---|---|
| MOV AX,0 | équivalent à mOv ax,0 |
| inc BX | équivalent à INC bx |
| 'abc' | est différent de 'ABC' |
En dehors des chaînes de caractères, vous pouvez écrire votre code en majuscules ou en minuscules indifféremment.
Les séparateurs
Afin de rendre le code lisible et de permettre à l'assembleur de distinguer clairement les différentes parties d'une instruction, il est nécessaire de séparer les éléments (instructions, identificateurs, opérandes,....) à l'aide d'au moins un espace.
Concernant les opérandes dans une instruction (c'est-à-dire les données ou adresses manipulées), la virgule (,) est utilisée pour les séparer. Un espace peut apparaître après la virgule mais n'est pas obligatoire. Voici quelques exemples corrects et incorrects :
Il est donc indispensable de respecter ces règles de séparation pour que le code puisse être assemblé sans erreur.
Les identificateurs
Les identificateurs sont des noms choisis par le programmeur pour désigner des étiquettes (labels) ou des variables dans le programme. Quelques règles fondamentales encadrent leur définition :
- Le premier caractère d'un identificateur doit impérativement être une lettre (A à Z ou a à z).
- Les identificateurs peuvent contenir des lettres, des chiffres et le caractère souligné (_), mais jamais d'espaces.
- Le nombre total de caractères composant un identificateur ne doit pas dépasser 80.
- Un identificateur ne doit pas être un mot réservé de l'assembleur.
Ces règles garantissent que les identificateurs restent lisibles, sans ambiguïté, et facilement exploitables par l'assembleur.
Les mots réservés
L'assembleur 80x86 possède un ensemble de mots réservés ne pouvant pas être utilisés pour nommer des identificateurs. Ces mots remplissent des rôles spécifiques dans la syntaxe et sont classés en quatre grandes catégories :
- Les mnémoniques d'instructions : Ce sont les noms des instructions exécutées par le microprocesseur, comme MOV, ADD, INC, SUB,...
- Les directives d'assemblage : Elles sont utilisées pour guider l'assemblage du programme et fournir des informations au compilateur, comme TITLE, ASSUME, SEGMENT,...
- Les noms des registres : Ces mots désignent les registres internes du processeur, tels que AX, BX, IP, SP, DS,...
- Les mots définissant la taille des données : Ils précisent la nature des données manipulées : BYTE, WORD, DWORD, QWORD,...
Sous aucun prétexte, un programmeur ne peut utiliser l'un de ces mots réservés pour désigner une variable, une constante ou une étiquette sous peine d'entraîner une erreur lors de l'assemblage du programme.
Les données
Généralités
Dans cette section, nous allons explorer en détail la manière dont on définit les données dans un programme écrit en assembleur pour la famille de microprocesseurs 80x86. Ces définitions se font à l'aide d'un ensemble particulier d'instructions qu'on appelle des pseudo-instructions. Contrairement aux instructions classiques du langage assembleur, qui sont directement traduites en opérations machines exécutées par le microprocesseur, les pseudo-instructions, quant à elles, s'adressent à l'assembleur lui-même. Leur but est d'indiquer à l'assembleur comment organiser ou préparer les données en mémoire, et non pas d'indiquer des opérations à exécuter. C'est pour cela qu'aucune pseudo-instruction ne correspond à un code machine spécifique : elles ne seront pas traduites en instructions binaires dans le programme final.
En plus de la définition de données, nous verrons également les différentes manières d'accéder à ces données en mémoire, car une fois définies, encore faut-il pouvoir les utiliser efficacement dans le programme.
Les types de données manipulables
Dans un programme assembleur 80x86, on peut être amené à manipuler trois grandes catégories de données. Chacune d'elles est traitée différemment selon sa nature :
- Les valeurs immédiates : Ce sont des nombres littéraux écrits tels quels dans le code. Ils sont directement intégrés dans l'instruction au moment de l'assemblage. Par exemple : 100, 24, ou 0FFFh sont des valeurs immédiates. Ces valeurs sont dites «immédiates» car elles sont disponibles immédiatement dans l'instruction, sans devoir être cherchées ailleurs. Exemple : En langage BASIC, dans l'instruction LET A=150, le nombre 150 est une valeur immédiate.
- Les constantes et les variables : Il s'agit ici de données nommées par des identificateurs que le programmeur choisit. Ces identificateurs permettent de référencer soit une constante (valeur figée), soit une variable (valeur pouvant évoluer). Pour ce faire, il faudra définir à la fois le nom et le type de la donnée. Exemple : En langage PASCAL, la constante PI est définie par l'instruction CONST PI=3.1416, tandis qu'une variable de type entier est définie par VAR A:INTEGER.
- Les zones mémoire : Ce type de données représente des emplacements spécifiques dans la mémoire vive. Dans ce cas, le programme devra connaître l'adresse exacte à laquelle accéder ainsi que la taille (ou la longueur) des données à manipuler. Exemple : En BASIC, les instructions PEEK et POKE servent à consulter (PEEK) ou à modifier (POKE) des données situées à une adresse mémoire précise.
Format des instructions de définition de données
Lorsque le programmeur souhaite déclarer des données, il utilise des instructions comportant généralement jusqu'à quatre champs distincts. Ces champs sont les suivants :
- Un identificateur (facultatif), donnant un nom à la donnée ou à la variable.
- Le mnémonique de la pseudo-instruction, comme DB ou DW, précisant la taille et le type de données à allouer.
- Une ou plusieurs opérandes, c'est-à-dire la valeur de la donnée ou une directive de réservation (?, DUP,...).
- Un commentaire optionnel, commençant par un point-virgule ;, servant à décrire la finalité ou le rôle de cette donnée.
Il n'y a aucune contrainte rigide quant à l'ordre ou à l'alignement de ces champs dans le fichier source du programme. Toutefois, par souci de clarté et pour rendre le code plus lisible, il est d'usage d'organiser le texte source en quatre colonnes bien alignées. Cette convention facilite grandement la lecture et la maintenance du code, notamment dans les projets de plus grande envergure.
Exemple pratique de définition de données
Voici un extrait de code montrant comment structurer correctement plusieurs définitions de données dans un programme assembleur :
Décryptons rapidement chaque ligne :
- St2 est une étiquette associée à un bloc de 100 octets non initialisés (?), ce qui pourrait servir, par exemple, à stocker une chaîne de caractères.
- St3 fonctionne de la même manière, mais pour une chaîne plus courte.
- Temp1 et Temp2 réservent chacun un mot (soit 2 octets) non initialisé en mémoire, utile pour des calculs intermédiaires.
Les registres
Les registres sont des zones d'entreposage internes au microprocesseur de la famille 80x86. Contrairement à la mémoire vive (RAM) classique, étant externe au microprocesseur, ces registres sont situés à l'intérieur même du microprocesseur, ce qui les rend extrêmement rapides d'accès. Ils jouent un rôle fondamental dans le fonctionnement du système, car ils servent à exécuter les instructions, stocker temporairement des données, gérer les adresses, et suivre l'état du processeur pendant l'exécution d'un programme.
Dans les microprocesseurs 8086 et 8088, étant les premiers représentants de cette famille, on compte 14 registres principaux. Pour faciliter la compréhension de leur usage, ces registres sont généralement classés en quatre grandes catégories, chacune ayant un rôle bien spécifique :
- Les registres de segment, déterminant les zones de mémoire utilisées.
- Les registres de travail, aussi appelés registres généraux, utilisés pour entreposer et manipuler des données.
- Les registres de déplacement (ou d'offset), servant à calculer des adresses mémoire effectives.
- Le registre des drapeaux, aussi appelé FLAGS, contenant les indicateurs d'état du microprocesseur.
Les registres de segment
Ces registres servent à désigner les segments de mémoire dans lesquels se trouvent les différentes parties d'un programme. En mode réel, la mémoire est divisée en segments de 64 Ko, et chaque segment doit être référencé par un de ces registres. Il y a quatre registres de segment principaux :
- CS (Code Segment) : il contient l'adresse du segment mémoire où résident les instructions du programme (le code machine). Ce segment est lu par le processeur pour déterminer quelle instruction exécuter ensuite.
- DS (Data Segment) : il pointe vers la partie de la mémoire contenant les données utilisées dans le programme (variables, constantes,...).
- SS (Stack Segment) : il désigne la zone mémoire utilisée pour la pile (stack), servant notamment à entreposer les adresses de retour lors des appels de sous-programmes et les variables locales.
- ES (Extra Segment) : c'est un segment supplémentaire utilisé principalement pour des opérations sur les chaînes de caractères, ou pour le transfert de blocs de données. Il peut être manipulé pour accéder à des données en dehors des segments traditionnels.
On verra plus loin que deux types de programmes existent en mode réel : les fichiers COM et les fichiers EXE. Les programmes COM sont des exécutables simples et compacts utilisant un seul segment mémoire de 64 Ko pour tout le programme : code, données et pile. Dans ce cas, les registres CS, DS, SS et ES sont tous initialisés à la même valeur, car ils pointent vers un unique segment. En revanche, les programmes EXE, étant plus complexes, peuvent utiliser plusieurs segments différents, ce qui implique des valeurs différentes dans chacun des registres de segment.
Les registres de travail
Également appelés registres généraux, ces registres sont utilisés pour réaliser des opérations de traitement sur les données. Ce sont des registres polyvalents, pouvant aussi bien servir à stocker des résultats intermédiaires qu'à participer directement à des calculs ou des comparaisons. Il y en a quatre :
- AX (Accumulateur) : c'est le registre principal pour les opérations arithmétiques. Il est souvent utilisé dans les instructions de multiplication, division et d'entrée/sortie. On l'appelle aussi le registre accumulateur car il est souvent utilisé pour accumuler des résultats.
- BX (Base Register) : souvent utilisé comme registre de base pour l'adressage indirect, ou comme registre auxiliaire dans les opérations arithmétiques.
- CX (Counter Register) : il joue le rôle de compteur, notamment pour les boucles (avec l'instruction LOOP) ou pour répéter une instruction un certain nombre de fois (comme REP MOVSB).
- DX (Data Register) : il sert de registre auxiliaire, souvent utilisé pour des opérations spécifiques comme la division et la multiplication de grande taille, ou dans certains appels système.
Chacun de ces registres, bien qu'ils fassent 16 bits de long, peut être divisé en deux sous-registres de 8 bits, ce qui permet un traitement plus fin :
- AX = AH (High) + AL (Low)
- BX = BH (High) + BL (Low)
- CX = CH (High) + CL (Low)
- DX = DH (High) + DL (Low)
Cette division permet, par exemple, de travailler uniquement sur les 8 bits de poids fort (avec AH) ou de poids faible (avec AL) sans affecter l'autre moitié du registre.
Il est important de noter que les opérations réalisées sur les registres sont beaucoup plus rapides que celles impliquant directement la mémoire. C'est pourquoi, chaque fois que c'est possible, un bon programmeur assembleur privilégiera l'usage des registres.
Les registres de déplacement (offset)
Ces registres ont pour rôle de définir une adresse relative, aussi appelée déplacement (offset), par rapport à une adresse de segment donnée. Ils permettent donc d'obtenir l'adresse effective utilisée pour accéder à la mémoire. Ces registres sont au nombre de cinq :
- SI (Source Index) : c'est un registre utilisé principalement dans les opérations sur les chaînes de caractères, où il représente l'adresse source à partir de laquelle les données seront lues. Il est habituellement associé au registre DS.
- DI (Destination Index) : comme SI, il est utilisé pour les chaînes de caractères, mais cette fois en tant qu'adresse de destination pour les données à écrire. Par défaut, il est associé à DS, mais dans les instructions de copie (MOVS,...), il peut être associé à ES.
- IP (Instruction Pointer) : c'est un registre très particulier contenant l'adresse de la prochaine instruction à exécuter. Il est combiné avec le registre CS (CS:IP) pour désigner l'instruction en cours. Ce registre ne peut pas être modifié directement par le programmeur. Il est mis à jour automatiquement lors des appels (CALL), des sauts (JMP, JE,...) ou lors des interruptions.
- BP (Base Pointer) : ce registre est utilisé principalement pour accéder aux données placées dans la pile, notamment lors des appels de sous-programmes. Il est combiné avec le registre SS (SS:BP).
- SP (Stack Pointer) : il contient l'adresse de la dernière position utilisée dans la pile. Il est également associé à SS (SS:SP), et il est automatiquement mis à jour à chaque PUSH ou POP.
Remarque importante : tous ces registres de déplacement, à l'exception d'IP, peuvent être utilisés comme opérandes dans les instructions arithmétiques ou logiques sur 16 bits. Cela permet une grande souplesse dans les calculs et les manipulations de données, directement à partir des registres.
Les registres dans l'architecture 32 bits (à partir du 80386)
Avec l'apparition du microprocesseur 80386, Intel a introduit une extension de l'architecture initiale en passant du mode 16 bits au mode 32 bits. Cette évolution visait à permettre des traitements de données plus volumineux, à accéder à une mémoire plus grande et à optimiser les performances globales. L'un des changements majeurs a été la modification et l'élargissement des registres internes du microprocesseur.
Les registres existants dans le 8086 ont été étendus à 32 bits, et quelques nouveaux ont été ajoutés. Les conventions de nommage ont également évolué : un préfixe "E" (pour "Extended") a été ajouté devant le nom des registres 16 bits pour désigner leur version 32 bits.
Les registres de segments (inchangés avec 2 nouveaux)
En 32 bits, les registres de segment originaux sont toujours présents, avec l'ajout de deux nouveaux registres :
- CS : Code Segment
- DS : Data Segment
- SS : Stack Segment
- ES : Extra Segment
- FS : Segment additionnel utilisé notamment dans la gestion avancée des processus léger.
- GS : Un autre segment supplémentaire, souvent utilisé par le système d'exploitation pour accéder à des structures internes du noyau.
Même si les registres de segment sont conservés, leur rôle a été fortement réduit en mode protégé (mode 32 bits réel). L'adressage devient en grande partie linéaire, et les segments sont davantage utilisés pour la gestion mémoire avancée, comme l'isolation des processus ou le multitâche.
Les registres de travail en 32 bits
Les anciens registres AX, BX, CX et DX sont étendus :
- EAX : Extended Accumulator Register
- EBX : Extended Base Register
- ECX : Extended Counter Register
- EDX : Extended Data Register
Ces registres contiennent désormais 32 bits, ce qui permet d'effectuer des calculs avec des entiers plus grands et d'adresser une mémoire plus vaste.
Note : les versions 16 bits et 8 bits restent utilisables. Ainsi :
- AX est la partie basse (16 bits) de EAX ;
- AL est la partie basse de AX (8 bits), AH la partie haute ;
- Il en va de même pour EBX, ECX et EDX.
Cela permet d'utiliser le même registre avec des granularités différentes selon les besoins du programme.
Les registres de déplacement en 32 bits
Les anciens registres de déplacement ont aussi été étendus :
- ESI : Extended Source Index
- EDI : Extended Destination Index
- EBP : Extended Base Pointer
- ESP : Extended Stack Pointer
- EIP : Extended Instruction Pointer
Comme en 16 bits, ces registres servent au calcul des adresses effectives ou au suivi de l'exécution du programme. EIP, tout comme IP en 16 bits, ne peut pas être modifié directement. Il est automatiquement mis à jour lors des appels (CALL), des sauts (JMP, JE,...), ou des interruptions.
Les registres dans l'architecture 64 bits (x64)
Avec l'arrivée des processeurs x64 (introduits avec l'AMD64, puis adoptés par Intel), l'architecture a encore évolué pour permettre l'exécution de programmes manipulant des données sur 64 bits. Cette architecture permet non seulement un accès à une mémoire plus grande (jusqu'à plusieurs térabytes), mais elle double aussi la capacité des registres.
Les registres de travail en 64 bits
Les registres de travail 32 bits sont maintenant étendus à 64 bits et reçoivent le préfixe R :
- RAX : 64-bit Accumulator
- RBX : 64-bit Base
- RCX : 64-bit Counter
- RDX : 64-bit Data
Ces registres peuvent toujours être accédés en versions 32 bits (EAX, EBX,...), 16 bits (AX, BX...) et 8 bits (AL, AH...). Cela offre une très grande flexibilité. Par exemple :
- RAX contient EAX (32 bits)
- EAX contient AX (16 bits)
- AX contient AL (8 bits bas) et AH (8 bits haut)
Les registres de déplacement en 64 bits
Comme pour les versions précédentes, ces registres sont aussi élargis :
- RSI : 64-bit Source Index
- RDI : 64-bit Destination Index
- RBP : 64-bit Base Pointer
- RSP : 64-bit Stack Pointer
- RIP : 64-bit Instruction Pointer (équivalent de EIP mais en 64 bits)
RIP est introduit pour permettre l'adressage relatif dans certaines instructions (RIP-relative addressing), ce qui rend l'exécution du code plus flexible en mémoire (utile pour les bibliothèques partagées, position-independent code,...).
Nouveaux registres généraux
L'un des grands changements du mode x64 est l'apparition de 8 nouveaux registres généraux :
- R8 à R15, soit un total de 16 registres de travail de 64 bits
Chacun peut également être accédé en versions réduites :
- R8D (32 bits), R8W (16 bits), R8B (8 bits)
Cela offre bien plus de possibilités au compilateur et au programmeur pour optimiser le code, en réduisant le recours à la mémoire.
Le registre de drapeaux (FLAGS)
Le registre FLAGS est une composante fondamentale du fonctionnement des processeurs 8086/8088. Il s'agit d'un registre spécial de 16 bits n'étant pas utilisé pour entreposer des données ou des adresses, mais pour garder une trace de l'état du microprocesseur à la suite de l'exécution d'instructions. Contrairement à d'autres registres, la valeur binaire globale de FLAGS (c'est-à-dire sa valeur entière sur 16 bits) n'a pas de signification directe. En effet, ce registre est exploité bit par bit, chaque bit jouant un rôle bien défini, influençant le comportement du processeur ou servant à orienter les instructions suivantes.
Les bits de ce registre sont appelés "indicateurs" ou "flags", et on les répartit généralement en deux grandes familles : les indicateurs d'état, reflétant le résultat d'opérations précédentes, et les indicateurs de contrôle, qui modifient le comportement de certaines instructions.
Les indicateurs d'état (Status Flags)
Ces indicateurs donnent des informations précieuses sur le résultat de la dernière instruction exécutée, en particulier pour les instructions arithmétiques, logiques et de comparaison. Voici les principaux :
| Bit | Signification | Abréviation |
|---|---|---|
| 0 | Carry (retenue) | CF |
| 2 | Parité | PF |
| 4 | Retenue auxiliaire | AF |
| 6 | Zéro | ZF |
| 7 | Signe | SF |
| 11 | Débordement (Overflow) | OF |
- CF (Carry Flag) : Ce bit est mis à 1 lorsqu'une retenue (ou un emprunt) est généré(e) à partir du bit de poids fort lors d'une opération arithmétique. Il est surtout utile pour détecter un dépassement en arithmétique non signée.
- PF (Parity Flag) : Il est mis à 1 si le résultat d'une opération contient un nombre pair de bits à 1. Cela permet une forme rudimentaire de détection d'erreurs.
- AF (Auxiliary Carry Flag) : Indique une retenue entre le 3e et le 4e bit, principalement utile pour les opérations arithmétiques en BCD (Binary Coded Decimal).
- ZF (Zero Flag) : Ce drapeau est activé (mis à 1) si le résultat d'une opération est égal à zéro. Il reste à 1 jusqu'à ce qu'une autre opération arithmétique ou logique le modifie.
- SF (Sign Flag) : Il reflète le bit de poids fort du résultat. Si celui-ci est 1, le résultat est interprété comme négatif (en arithmétique signée). S'il est 0, le résultat est positif.
- OF (Overflow Flag) : Ce bit est activé quand il y a débordement en arithmétique signée. Par exemple, si l'addition de deux entiers positifs donne un résultat négatif, le drapeau OF est mis à 1.
Les indicateurs de contrôle (Control Flags)
Ces indicateurs servent à configurer le fonctionnement interne du processeur et influencent la façon dont certaines instructions s'exécutent.
| Bit | Signification | Abréviation |
|---|---|---|
| 8 | Trap | TF |
| 9 | Interruption | IF |
| 10 | Direction | DF |
- TF (Trap Flag) : Lorsqu'il est activé, ce bit provoque une interruption après chaque instruction, permettant ainsi un mode pas-à-pas très utile pour le débogage.
- IF (Interrupt Flag) : Il contrôle si le microprocesseur accepte ou ignore les interruptions externes. S'il est à 1, les interruptions sont autorisées. S'il est à 0, elles sont ignorées temporairement.
- DF (Direction Flag) : Il influence la direction du balayage de la mémoire pour certaines instructions comme MOVS, LODS, STOS. S'il est à 0, le balayage se fait vers l'adresse croissante (incrémentation), s'il est à 1, il se fait vers l'adresse décroissante (décrémentation). Ce bit peut être manipulé via les instructions CLD (clear direction) et STD (set direction).
Bits non utilisés
Les bits 1, 5, 12, 13, 14 et 15 du registre FLAGS ne sont pas utilisés dans le microprocesseur 8086/8088. Ils sont réservés pour un usage futur ou ignorés par le microprocesseur. Cela signifie que leur valeur ne doit pas être modifiée ou interprétée par le programmeur.
Relations entre les instructions et les indicateurs
De nombreuses instructions affectent les indicateurs, en particulier les instructions arithmétiques, logiques et de comparaison. Voici quelques exemples pratiques :
D'autres instructions n'affectent pas les indicateurs, ou bien les modifient de manière spécifique. Il est donc important de consulter la documentation pour savoir quand et comment chaque drapeau est affecté.
Les instructions conditionnelles, comme les sauts (JMP, JE, JA,...), s'appuient sur l'état de ces indicateurs pour décider de leur comportement :
Il faut aussi noter que dans le cas d'instructions non arithmétiques, les indicateurs CF (Carry) et OF (Overflow) sont souvent réinitialisés à zéro. Certains indicateurs peuvent aussi rester inchangés ou devenir indéterminés, selon l'instruction.
Récapitulatif des indicateurs arithmétiques
Les indicateurs OF, CF, ZF, SF et PF sont qualifiés d'arithmétiques, car ils sont systématiquement mis à jour par les instructions effectuant des calculs ou des comparaisons. Leur bonne interprétation est essentielle pour écrire un code fiable, notamment pour la gestion des conditions et des boucles.
Le registre EFLAGS (32 bits) et RFLAGS (64 bits)
Avec l'arrivée des processeurs 32 bits (à partir du 80386), le registre FLAGS a été étendu à 32 bits et renommé EFLAGS (Extended FLAGS). Cette évolution a permis d'introduire de nouveaux indicateurs tout en conservant la compatibilité avec les 16 premiers bits du registre original. Plus tard, avec l'architecture x64 (processeurs AMD64/Intel 64), ce registre est devenu RFLAGS, étendu à 64 bits, bien que de nombreux bits restent réservés ou inutilisés à ce jour.
Ces évolutions ont permis un contrôle plus fin du comportement du processeur, notamment pour les environnements multitâches, la virtualisation, et les protections mémoire.
Structure générale de EFLAGS et RFLAGS
Voici une vue simplifiée de l'agencement des bits dans les registres EFLAGS (32 bits) et RFLAGS (64 bits). Les 16 premiers bits sont hérités du registre FLAGS original (8086), et les autres bits ajoutent des indicateurs spécialisés ou réservés pour un usage futur :
| Bit | Nom | Signification |
|---|---|---|
| 0 | CF | Carry Flag (retenue) |
| 1 | Réservé (non utilisé) | |
| 2 | PF | Parity Flag (parité) |
| 3 | Réservé | |
| 4 | AF | Auxiliary Carry Flag (drapeau de retenue auxiliaire) |
| 5 | Réservé | |
| 6 | ZF | Zero Flag (zéro) |
| 7 | SF | Sign Flag (signe) |
| 8 | TF | Trap Flag (exécution pas-à-pas) |
| 9 | IF | Interrupt Enable Flag (drapeau d'activation d'interruption) |
| 10 | DF | Direction Flag (drapeau de direction) |
| 11 | OF | Overflow Flag (débordement) |
| 12 | IOPL (0-1) | I/O Privilege Level (niveau d'accès aux ports d'entrée/sortie) |
| 14 | NT | Nested Task Flag (indique un appel de tâche imbriqué) |
| 15 | Réservé | |
| 16 | RF | Resume Flag (reprise après une exception) |
| 17 | VM | Virtual 8086 Mode (activation du mode 8086 virtuel) |
| 18 | AC | Alignment Check (vérification d'alignement mémoire) |
| 19 | VIF | Virtual Interrupt Flag (interruptions virtuelles) |
| 20 | VIP | Virtual Interrupt Pending |
| 21 | ID | Identification Flag (autorise la détection CPUID) |
| 22 à 63 | Réservés ou dépendants du microprocesseur |
Remarque : Tous les bits ne sont pas modifiables par les instructions classiques. Certains ne peuvent être lus ou écrits que par des instructions privilégiées (comme PUSHF, POPF, IRET,...) ou dans des contextes système (comme les hyperviseurs ou le mode kernel).
Nouveaux indicateurs introduits dans EFLAGS/RFLAGS
- IOPL (bits 12 à 13) : Le champ IOPL (Input/Output Privilege Level) est un indicateur à deux bits qui spécifie le niveau de privilège requis pour exécuter les instructions d'accès aux ports d'entrée/sortie (IN, OUT,...). Seul le système d'exploitation ou un code tournant en ring 0 (niveau de privilège maximal) peut modifier cette valeur.
- NT (Nested Task - bit 14) : L'indicateur NT est utilisé pour gérer l'imbrication de tâches dans des systèmes d'exploitation supportant la gestion matérielle des tâches. Lorsqu'il est activé, il indique que la tâche actuelle a été appelée par une autre tâche, ce qui permet d'assurer le retour correct à la tâche appelante.
- RF (Resume Flag - bit 16) : Le Resume Flag est utilisé en combinaison avec les mécanismes de débogage matériel. Il permet de reprendre l'exécution après le déclenchement d'un point d'arrêt ou d'une exception, sans que cela en relance une nouvelle.
- VM (Virtual Mode - bit 17) : Ce bit, lorsqu'il est activé, permet de simuler un environnement 8086 dans un système tournant en mode protégé. C'est un élément clé pour assurer la compatibilité avec d'anciens programmes DOS, tout en bénéficiant des mécanismes de protection des systèmes modernes.
- AC (Alignment Check - bit 18) : Permet de déclencher une exception (#AC) si une donnée n'est pas alignée correctement en mémoire. Ce mécanisme est important pour détecter des erreurs de programmation ou des accès mémoire non optimisés.
- VIF & VIP (bits 19 et 20) : Ces indicateurs, Virtual Interrupt Flag et Virtual Interrupt Pending, sont utilisés dans des contextes de virtualisation. Ils simulent la gestion des interruptions dans des machines virtuelles, permettant à un hyperviseur de contrôler l'environnement sans que l'invité puisse manipuler directement l'état du microprocesseur.
- ID (Identification Flag - bit 21) : Ce bit permet au programme d'activer ou non la possibilité d'exécuter l'instruction CPUID, retournant des informations détaillées sur le processeur. Il est notamment utilisé pour déterminer si un microprocesseur supporte certaines extensions (SSE, AVX,...).
Compatibilité et héritage
Les registres EFLAGS et RFLAGS conservent intactes les fonctions des 16 premiers bits du registre FLAGS des microprocesseurs 8086. Cela garantit une compatibilité ascendante totale avec les programmes écrits pour les anciennes architectures.
Les instructions PUSHF et POPF (ou PUSHFD, POPF en 32 bits, et PUSHFQ, POPFQ en 64 bits) permettent de sauvegarder/restaurer ces registres sur la pile. Ces instructions sont souvent utilisées dans les routines systèmes ou pour interagir proprement avec le matériel.
Récapitulatif visuel simplifié (EFLAGS/RFLAGS)
| Bits | [ ... | ID | VIP | VIF | AC | VM | RF | NT | IOPL | OF ... CF ] |
|---|---|
| Nom | [ ... | ID | VIP | VIF | AC | VM | RF | NT | 12-13| OF ... CF ] |
| Taille | 32 bits (EFLAGS) / 64 bits (RFLAGS) |
Le passage de FLAGS → EFLAGS → RFLAGS reflète l'évolution des microprocesseurs vers plus de puissance, de sécurité et de flexibilité. Bien que certains indicateurs comme CF, ZF, OF ou SF soient encore au cour du traitement arithmétique, d'autres comme VIF, ID, IOPL ou AC visent à supporter des environnements multi-utilisateurs, virtualisés ou sécurisés.
Les données et la mémoire
En langage de programmation assembleur 80x86, les données manipulées par le programme peuvent être représentées sous différentes formes, selon leur nature ou leur usage. Parmi les éléments fondamentaux, on trouve les constantes, étant des valeurs fixées au moment de l'assemblage et immuables pendant toute l'exécution du programme. Comprendre la manière de définir et d'utiliser ces constantes est essentiel pour structurer correctement un code assembleur clair, lisible et efficace.
Les constantes
Une constante est une valeur fixe, connue dès l'écriture du programme, et qui ne change jamais au cours de l'exécution. Contrairement aux variables ou aux valeurs entreposées en mémoire, les constantes ne peuvent pas être modifiées dynamiquement. Elles servent principalement à représenter des valeurs logiques, numériques ou symboliques réutilisables dans plusieurs parties du code source. Cela rend le programme plus lisible, plus facile à maintenir et moins sujet aux erreurs.
La définition des constantes se fait par le biais d'instructions particulières appelées pseudo-instructions, étant comprises par l'assembleur mais ne génèrent pas directement de code machine. Ces directives servent uniquement pendant la phase d'assemblage pour faciliter l'écriture du programme.
La pseudo-instruction EQU
La directive EQU (abréviation de "equate") est une pseudo-instruction d'assemblage permettant d'associer un nom (un identificateur) à une valeur constante. L'identificateur agit comme une étiquette symbolique remplaçant la valeur dans le reste du programme. Cette substitution se fait lors de l'assemblage par le compilateur, ce qui signifie que cette valeur est figée dès le début et ne peut plus être modifiée ensuite.
Exemples simples :
- Vrai EQU 1 ; "Vrai" est défini comme valant 1
- Faux EQU 0 ; "Faux" est défini comme valant 0
Dans ce cas, chaque fois que l'on écrit Vrai ou Faux dans le code assembleur, l'assembleur le remplacera respectivement par 1 ou 0. Cela améliore la compréhension du code, en rendant les intentions du programmeur plus explicites.
L'intérêt de EQU ne se limite pas aux simples valeurs : il est aussi possible de définir des expressions arithmétiques, étant évaluées à la compilation. Cela permet de construire des constantes plus complexes à partir d'autres constantes déjà définies.
Exemples avec expressions :
- Cinq EQU 2*2+1 ; Évalue à 5
- Sept EQU Cinq+2 ; Utilise la constante précédemment définie
Remarque importante : Lorsqu'une expression utilise un identificateur (comme Cinq dans l'exemple ci-dessus), celui-ci doit impérativement être défini avant d'être utilisé. Sinon, l'assembleur ne pourra pas évaluer correctement l'expression et renverra une erreur de compilation.
Les constantes immédiates
Une constante immédiate est une valeur insérée directement dans une instruction. Elle est appelée "immédiate" car elle est codée en dur dans l'instruction assembleur, sans passer par une adresse mémoire. Ces constantes sont souvent utilisées comme opérandes dans les opérations arithmétiques ou logiques.
Les constantes immédiates peuvent prendre plusieurs formes, selon la base numérique choisie par le programmeur ou selon qu'il s'agit de valeurs numériques ou de caractères.
Types et formats de constantes immédiates :
- Valeurs numériques : Elles peuvent être exprimées dans différentes bases :
- Caractères et chaînes de caractères : Un caractère est encadré par des apostrophes simples, et une chaîne de caractères contient plusieurs caractères successifs.
| Base | Suffixe |
|---|---|
| Binaire (base 2) | Suffixe b |
| Hexadécimal (base 16) | Suffixe h |
| Décimal explicite (base 10) | Suffixe d |
| Décimal implicite | Aucun suffixe (la base par défaut est le décimal) |
Exemples pratiques :
- 00000011b ; constante binaire représentant la valeur 3
- 0FFFh ; constante hexadécimale représentant 4095
- 1024d ; constante décimale explicite (équivalente à 1024)
- 1024 ; même valeur, base décimale par défaut
- 'a' ; caractère ASCII 'a' (valeur 97 en décimal)
- 'BCDE' ; chaîne de caractères de 4 octets (chaîne ASCII)
Précaution avec les constantes hexadécimales
Il existe une subtilité importante lorsqu'on écrit des constantes hexadécimales. Si la valeur commence par une lettre (par exemple A, B, C,...), il faut précéder cette lettre d'un chiffre (typiquement 0). En effet, sans ce préfixe, l'assembleur risque de confondre la constante avec un identificateur symbolique, ce qui générera une erreur.
Exemples corrects et incorrects :
- FFEDh ; Incorrect : commence par une lettre, l'assembleur croit que c'est un label
- 0FFEDh ; Correct : le 0 indique bien qu'il s'agit d'une constante hexadécimale
Cette règle est particulièrement importante lors de la manipulation de valeurs mémoire, de masques binaires ou de paramètres matériels, souvent exprimés en hexadécimal.
Les variables
En langage de programmation assembleur 80x86, une variable correspond à une zone mémoire réservée explicitement par le programmeur, au moment de l'assemblage du programme. Contrairement aux constantes, ces zones peuvent être modifiées en cours d'exécution, ce qui permet de stocker des informations dynamiques, telles que des résultats intermédiaires de calculs, des états, ou des données d'entrée/sortie.
Ces variables sont définies à l'aide de pseudo-instructions, c'est-à-dire des directives comprises par l'assembleur, mais ne produisant pas directement d'instruction machine. Elles servent uniquement à réserver de la mémoire, éventuellement à l'initialiser, et à associer à chaque bloc mémoire un identificateur que le programmeur pourra utiliser par la suite.
La pseudo-instruction DB (Define Byte)
La directive DB est utilisée pour réserver de la mémoire sur un seul octet (8 bits) par élément, ce qui correspond à des valeurs numériques comprises entre 0 et 255 (type BYTE), ou à des caractères ASCII simples. Cette instruction permet aussi bien de définir une seule variable, que de créer un tableau ou une séquence de caractères.
Lorsqu'on utilise DB, on peut soit initialiser directement les octets avec des valeurs, soit réserver l'espace sans initialisation (à l'aide du symbole ?). Il est également possible d'utiliser la notation DUP (pour "duplicate") afin de générer plusieurs valeurs ou motifs répétitifs. La syntaxe est la suivante :
| NomDeVariable DB valeur[, valeur2, valeur3, ...] |
Remarques :
- Les valeurs multiples doivent être séparées par des virgules.
- Il est possible de combiner des valeurs numériques, des caractères ASCII et l'opérateur DUP dans des définitions plus complexes.
Exemples pratiques :
- Status DB 0 ; Réserve un octet initialisé à 0, identificateur : "Status"
- XYZ DB 1,2,3 ; Réserve 3 octets avec les valeurs 1, 2 et 3
- Autre DB ? ; Réserve un octet sans initialisation (valeur indéfinie)
- Table DB 5 DUP(?) ; Réserve 5 octets non initialisés (tableau de 5 éléments)
- AutreTab1 DB 5 DUP (1,2,3,4,5) ; Réserve 5 octets initialisés avec les valeurs indiquées
- AutreTab2 DB 10 DUP (0,1) ; Réserve 10 octets en alternant les valeurs 0 et 1
Note importante : L'opérateur DUP permet à l'assembleur de dupliquer automatiquement les valeurs données entre parenthèses. Dans le cas d'une séquence plus courte que la taille demandée, les valeurs sont répétées en boucle jusqu'à remplir la totalité de la zone mémoire.
La pseudo-instruction DW (Define Word)
La directive DW permet de réserver de la mémoire sur deux octets consécutifs, soit 16 bits (un "mot" ou WORD). Elle est utilisée pour entreposer des entiers plus grands, dans l'intervalle de 0 à 65 535 (en non signé) ou de -32 768 à 32 767 (en signé), ou pour construire des structures de données plus étendues. La syntaxe est la suivante :
| NomDeVariable DW valeur[, valeur2, valeur3, ...] |
Particularités :
- Comme avec DB, on peut définir des variables simples, des tableaux ou utiliser l'opérateur DUP.
- Lors de l'initialisation d'un mot (16 bits), l'octet de poids faible est stocké en mémoire avant l'octet de poids fort (conformément à l'ordre little-endian utilisé par les processeurs Intel x86/x64).
Exemples :
Chaque élément défini avec DW prend 2 octets, ce qui est à prendre en compte pour le calcul des déplacements (offsets) ou pour l'alignement mémoire.
La pseudo-instruction DD (Define Double Word)
La directive DD sert à allouer de la mémoire par blocs de 4 octets (32 bits), c'est-à-dire un double mot ou DWORD. Ces zones mémoire sont utilisées pour stocker des entiers de plus grande taille (jusqu'à 4 294 967 295 en non signé), ou plus couramment, des adresses mémoire complètes (segment + offset), des pointeurs ou des valeurs de type réel ou structuré (dans certains cas avancés). La syntaxe est la suivante :
| NomDeVariable DD valeur[, valeur2, valeur3, ...] |
Caractéristiques :
- Comme pour DW, l'ordre d'enregistrement des octets respecte le format little-endian : les octets de poids faible sont stockés en premier.
- DD est souvent utilisé dans les programmes 32 bits ou pour représenter les registres d'adresse.
Exemples d'utilisation :
Rappel : Lorsque vous utilisez des valeurs hexadécimales commençant par une lettre, vous devez précéder la constante d'un 0 (comme dans 0FFFF0000h) pour éviter que l'assembleur ne l'interprète comme un identificateur (ce qui causerait une erreur).
Les chaînes de caractères
En langage assembleur, une chaîne de caractères (ou chaîne de texte) est simplement une séquence d'octets représentant des caractères ASCII, que l'on entrepose dans une zone mémoire. Pour ce faire, on utilise généralement la pseudo-instruction DB (Define Byte), qui permet d'assigner des valeurs octet par octet - or, chaque caractère ASCII étant codé sur un octet, cela se prête parfaitement au entreposage de chaînes de caractères.
Les chaînes de caractères sont délimitées par des apostrophes simples (') et leur contenu est traité comme une succession de valeurs ASCII. L'assembleur traduit chaque caractère en son code numérique correspondant.
Définition de chaînes simples avec DB
Pour définir une chaîne de caractères destinée à l'affichage, on l'écrit entre apostrophes simples. Dans certains environnements, comme DOS avec les appels d'interruption (par exemple INT 21h), il est important de terminer la chaîne de caractères par un caractère spécial - souvent le symbole dollar ($) - afin d'indiquer la fin de la chaîne à la fonction d'affichage.
Exemples de définitions simples :
Dans la dernière ligne, l'apostrophe présente à l'intérieur de la chaîne (J'espère) est échappée en la doublant ('') afin que l'assembleur comprenne qu'il s'agit bien d'un caractère littéral, et non de la fin de la chaîne de caractères. Le caractère $ est obligatoire dans certains systèmes pour marquer la fin du texte à afficher, sans quoi l'appel système risquerait de lire au-delà de la chaîne de caractères.
Combiner texte et codes ASCII dans une chaîne de caractères
Il est également possible - et courant - d'insérer des codes ASCII numériques directement dans une chaîne, en les séparant par des virgules. Cela permet d'intégrer des caractères non imprimables ou spéciaux dans un message, comme les retours à la ligne (CR, LF) ou même un bip sonore (BEL).
Ces valeurs décimales correspondent aux codes ASCII standards. Par exemple :
- 07 → BEL (Bip sonore)
- 10 → LF (Line Feed)
- 13 → CR (Carriage Return)
Exemples de définitions mixtes :
Dans la variable Mess, la chaîne de texte est suivie de deux codes : 13 (retour chariot) puis 10 (saut de ligne). Cela permet un retour à la ligne complet sur un affichage console type DOS. Dans Beep, le message commence directement par le code 07, qui provoque un bip sonore à l'affichage, suivi d'un message textuel classique.