Section courante

A propos

Section administrative du site

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 :

  1. 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 :

  1. Message DB 'Bonjour, comment allez-vous ?','$'
  2.  
  3. MOV AH, 09h
  4. MOV DX, OFFSET Message
  5. INT 21h

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

Notes importantes concernant la mémoire

Quelques éléments fondamentaux à toujours garder en tête :

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 :

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 :

É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 :

É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 :

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 :

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 :

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 :

  1. MOV AX,0       ; est correcte
  2. MOV      AX,0  ; est correcte
  3. MOV    AX,  0  ; est correcte
  4. MOV  AX   , 0  ; est correcte
  5. MOVAX,0        ; est incorrecte

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 :

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 :

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 :

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 :

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 :

  1. ;IDENT    INSTR   OPERANDES     COMMENTAIRES
  2.  
  3. St2       DB      100 DUP (?)   ; Chaîne de maximum 98 caractères + retour + '$'
  4. St3       DB      10 DUP (?)    ; Chaîne de maximum 8 caractères + retour + '$'
  5. Temp1     DW      ?             ; Variable temporaire de type mot (16 bits)
  6. Temp2     DW      ?             ; Deuxième variable temporaire de type mot

Décryptons rapidement chaque ligne :

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

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

Les registres de déplacement en 64 bits

Comme pour les versions précédentes, ces registres sont aussi élargis :

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 :

Chacun peut également être accédé en versions réduites :

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

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

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 :

  1. DEC AX      ; décrémente AX de 1, il modifie aussi CF, ZF, SF, OF, AF
  2. CMP AX, 1   ; compare AX avec 1, il modifie aussi CF, ZF, SF, OF, AF

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 :

  1. JA     ; saut si au-dessus, teste CF et ZF
  2. JNE    ; saut si non égal, teste ZF
  3. JS     ; saut si signe négatif, teste SF

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

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 :

  1. Vrai   EQU 1      ; "Vrai" est défini comme valant 1
  2. 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 :

  1. Cinq   EQU 2*2+1      ; Évalue à 5
  2. 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 :

Exemples pratiques :

  1. 00000011b   ; constante binaire représentant la valeur 3
  2. 0FFFh       ; constante hexadécimale représentant 4095
  3. 1024d       ; constante décimale explicite (équivalente à 1024)
  4. 1024        ; même valeur, base décimale par défaut
  5. 'a'         ; caractère ASCII 'a' (valeur 97 en décimal)
  6. '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 :

  1. FFEDh      ; Incorrect : commence par une lettre, l'assembleur croit que c'est un label
  2. 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 :

Exemples pratiques :

  1. Status    DB 0                 ; Réserve un octet initialisé à 0, identificateur : "Status"
  2. XYZ       DB 1,2,3             ; Réserve 3 octets avec les valeurs 1, 2 et 3
  3. Autre     DB ?                 ; Réserve un octet sans initialisation (valeur indéfinie)
  4. Table     DB 5 DUP(?)          ; Réserve 5 octets non initialisés (tableau de 5 éléments)
  5. AutreTab1 DB 5 DUP (1,2,3,4,5) ; Réserve 5 octets initialisés avec les valeurs indiquées
  6. 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 :

Exemples :

  1. Cent      DW 1000            ; Réserve 2 octets initialisés à 1000
  2. Trois     DW 100,200,300     ; Réserve 3 mots : total de 6 octets
  3. TableW    DW 5 DUP (?)       ; Réserve 5 mots non initialisés (10 octets)

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 :

Exemples d'utilisation :

  1. Addresse1 DD 0FFFF0000h      ; Entrepose une valeur hexadécimale de 32 bits
  2. Trois     DD 100,200,300     ; Réserve 3 DWORD initialisés (12 octets)
  3. TableDW   DD 5 DUP (?)       ; Réserve 5 DWORD non initialisés (20 octets au total)

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 :

  1. Message  DB 'Bienvenue dans le programme$'
  2. Error    DB 'Erreur, veuillez recommencer$'
  3. autre    DB 'J''espère que vous appréciez ce programme...'

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 :

Exemples de définitions mixtes :

  1. Mess     DB 'Voici un message suivi de CR et LF',13,10,'$'
  2. Beep     DB 07,'Ce message est avec un Beep (ASCII 7)','$'

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.



Dernière mise à jour : Samedi, le 26 avril 2025