Il s'agit de la représentation du langage machine tel qu'il est réellement exécuté par le processeur. Chaque instruction en assembleur représente un code numérique compris du processeur et exécuté.
Il s'agit du plus bas niveau de programmation possible. Les structures évoluées de la programmation (objets, boucles, structures conditionnelles if then else) n'existent pas en assembleur.
Chaque processeur possède son jeu d'instruction, et par conséquent son langage assembleur. Cette page n'abordera donc que les processeurs compatibles Intel 8086. Tous les processeurs dits grand public (Pentium et Athlon) ont gardé une compatibilité ascendante avec leurs prédécesseurs. Théoriquement, Ms-Dos 1.0 ou Windows 1.0 devraient fonctionner sur votre dernier Pentium à 3 Ghz (concrètement, je ne sais pas je n'ai pas essayé).
Delphi, Java, C++, Rebol, ... Il existe de nombreux langages de programmation différents. Ces langages sont qualifiés d'évolués. Evolués, car ils masquent la complexité de la programmation. Ils proposent des couches qui rendent le matériel abstrait. Pour écrire un logiciel de messagerie internet, pas besoin de connaître la marque de la carte réseau ou du modem.
L'assembleur, de part sa nature, est proche du matériel. Il est donc utilisé pour la programmation système : drivers et parties sensibles des systèmes d'exploitation.
[haut de page]Le code source suivant est écrit selon la syntaxe du compilateur TASM. Le programme fonctionne sous Ms-Dos (sans Windows) et affiche "Bonjour, le monde" à l'écran.
; Le segment de données Data Segment Message DB "Bonjour, le monde !$" Data EndS ; Le segment de code Code Segment Assume Cs : Code, Ds : Data Main Proc ; Affichage du message Mov Ah, 09h Mov Dx, Offset Message Int 21h ; Indicateur de fin du programme, ; et appel à MS-DOS Mov Ah, 0C00h Int 21h Main EndP Code EndS End Main
Dans un programme exécutable (*.exe), on sépare les données du code. Les données sont placées dans
un segment (zone de mémoire), et le code dans un autre. Dans Data Segment, on définit
les données (ici, une chaîne de caractère). Dans Code Segment, on tape le code. La
procédure à appeler est Main Proc. Les trois lignes suivantes déclenchent l'affichage
du message. Les deux dernières enfin permettent de quitter le programme et de rendre le contrôle à
DOS.
Quand vous programmerez en assembleur, vous vous apercevrez vite que maîtriser le binaire et l'hexadécimal est indispensable. L'appel des interruptions se fait en hexadécimal.Voici une table qui vous permettra de repérer facilement les équivalences entre binaire, décimal et hexadécimal.
|
|
C'est une façon beaucoup plus simple de représenter le binaire. Pour l'ordinateur, notre base 10 ne signifie rien. Il ne reconnaît que la base 2 (le binaire). L'hexadécimal permet de représenter le binaire sans avoir à taper de longues séries de 0 ou de 1. Les conversions binaires <=> hexa sont beaucoup plus simples que les conversions binaires <=> décimal.
Comme vous pouvez le voir, en hexadécimal, 9+1=A. En base 16, - eh oui ! - il faut attendre 16 pour passer à 10, et 32 pour passer à 20. Hum...
Il faut décomposer chaque entier sous forme de puissances de 2.
Par exemple 25 = 16 + 8 + 1 = 24 + 23 + 20
Ensuite, on allume chaque bit correspond à la puissance de 2 : ici le bit 4, le bit 3, et le bit 0. Cela donne le nombre binaire 11001. En refaisant l'opération en sens inverse, on réobtient le nombre décimal.
C'est beaucoup plus simple : à chaque suite de 4 bits, correspond un chiffre hexadécimal. En utilisant le tableau précédent, 1 donne 1, et 1001 donne 9, donc 1 1001 donne 19 en hexadécimal. C'est grâce à cette facilité de conversion binaire <=> hexadécimal que la base 16 (= 24) a été choisie.
[haut de page]Le compilateur ne différencie pas majuscules et minuscules, et ne tient pas compte du nombre d'espaces. Les instructions suivantes sont identiques et correctes :
Mov Ah, 1 mOV aH , 1 MOV AH, 1 mov ah,1
Chaque instruction est différenciée d'une autre par un passage à la ligne. Les commentaires sont introduits avec le caractère ';'.
Il n'y a pas de typage. Un caractère peut être lu comme un nombre, ou un pointeur. Par contre, il faut respecter un format :
DB définit une variable d'un octet (de 0 à 255)DW définit une variable d'un mot (2 octets ; de 0 à 65 535)DD définit une variable d'un double mot (4 octets ; de 0 à 4 294 967 296)Les variables sont définies dans une zone spéciale (un segment) sous la forme 'Nom_Variable DB Valeur'. On peut définir des tableaux simplement :
Nom_Tableau1 DB 1, 2, 3, 4 définit un tableau de 4 octets initialisées aux valeurs 1, 2, 3 et 4Nom_Tableau2 DW 0 Dup (125) définit un tableau de 125 mots initialisées à 0
Pour manipuler une valeur dans un tableau, écrire Nom_Tableau1[0] (= 1)
; Registres <-- Valeurs Mov Ax, 65535 ; (décimal) Mov Cl, 01101b ; (binaire) Mov Dh, 0FAh ; (hexa) ; Entre registres Mov Ax, Bx Mov Cl, Dh ; Entre Registres et Variables Mov Cx, variable_de_deux_octets Mov variable_de_un_octet, Dl ; Registres <-- Adresses Mémoire Mov Ax, Offset variable ; Ax <- adresse de variable Mov Cx, [ 5Ah ] ; Cx <- valeur à l'adresse 5A
; Incrémentation Inc Ax ; Ax <- Ax + 1 Inc ma_variable ; Décrémentation Dec Ax Dec ma_variable ; Addition Add Ax, 5 ; Ax <- Ax + 5 Add Bh, toto ; Bh <- Bh + toto Add toto, Cx ; toto <- toto + Cx ; Soustraction Sub Ax, 5 ; Ax <- Ax - 5 Sub Bh, toto ; Bh <- Bh - toto Sub toto, Cx ; toto <- toto - Cx
; AND bit à bit Mov Ah, 0101b ; Ah <- 5 Mov Bh, 1001b ; Bh <- 9 And Ah, Bh ; Ah <- Ah AND Bh ; Ah vaut 0001b, soit 1 ; OR bit à bit Mov Ah, 0101b ; Ah <- 5 Mov Bh, 1001b ; Bh <- 9 Or Ah, Bh ; Ah <- Ah OR Bh ; Ah vaut 1101b, soit 13 (8+4+1) ; XOR bit à bit Mov Ah, 0101b ; Ah <- 5 Mov Bh, 1001b ; Bh <- 9 Or Ah, Bh ; Ah <- Ah OR Bh ; Ah vaut 1100b, soit 12 (8+4) ; NOT bit à bit Mov Ah, 0101b ; Ah <- 5 Not Ah ; Ah <- NOT Ah ; Ah vaut 1010b, soit 10 (8+2)
Toutes les comparaisons se font à l'aide de l'instruction CMP. On utilise ensuite les instructions de saut conditionnel Jump if Equal, Jump if Greater, ... Il faut définir des labels, des étiquettes, les endroits dans le programme où sauter si le test est vérifié (comme les GOTO en Basic).
; Egalité (Jump if Equal) Cmp Ax, 5 JE label_1 ; Différence (Jump if Not Equal) Cmp Ax, ma_variable JNE label_2 ; Inférieur, Supérieur, Inf. ou égal, Sup. ou égal ; (Jump if Lower, Greater, Lower or Equal, Greater or Equal) Cmp Ch, 0 JL label_1 Cmp Dh, Ah JG label_2 Cmp Al, 01001b JLE label_3 Cmp variable, 65 JGE label_4 label_1: instructions... label_2: instructions... label_3: instructions... label_4: instructions... ; Saut non conditionnel : Jump JMP label_1[haut de page]
Un caractère est représenté sous la forme d'un octet. Le codage ASCII définit un jeu de 256 caractères dont seul un tiers peut être affiché par le clavier. Le code 97 est le code du 'a', le 98 du 'b', le 99 du 'c'... Il existe des caractères spéciaux : afficher le 7 émet un bip, et le code 13 est le code de la touche Entrée.
Les chaînes de caractères sont considérées comme des tableaux de caractères :
Message DB 'Bonjour$' est la même chose que
Message DB 'B', 'o', 'n', 'j', 'o', 'u', 'r', '$' ou que
Message DB 66, 111, 110, 106, 111, 119, 114, 36
La seule obligation est le caractère '$' qui indique la fin de la chaîne (sinon les octets situés à la suite du message en mémoire sont aussi affichés comme caractères).
[haut de page]Comme les caractères, ils peuvent être ramenés à des nombres. Une adresse est en réalité le nombre d'octets depuis le début (d'un programme, d'une zone mémoire), généralement représentée sur 16 bits (2 octets, ou un mot). Attention, si vous confondez un pointeur avec un caractère, ou un entier, le compilateur ne vous dira rien, il se peut que rien ne se passe (de toutes façons, il ne s'agit que d'octets pour le processeur), mais il se peut aussi qu'il se passe une grosse couille.
Voici quelques compilateurs : asm (de Arrowsoft), masm (Microsoft Assembleur), tasm (Turbo Assembleur, de Borland). La syntaxe est la même pour ces trois compilateurs. Il existe également gas (gnu c compiler), et nasm (Netwide Assembleur), compilateur gratuit pour Dos, Windows, ou Linux. Mais la syntaxe de ces deux derniers est assez différente. Tous les exemples sont écrits suivant la syntaxe de tasm.
Pour générer l'exécutable à partir du fichier objet créé par le compilateur, il vous faut un linker. Il existe link (Microsoft) ou tlink (Borland), liste non exhaustive (recherchez sur internet). Alors que le compilateur se charge uniquement de transcrire en langage machine le code source du programme, l'éditeur de liens permet d'ajuster les adresses des différentes procédures.
Prenons un exemple concret : imaginons un programme C qui fasse appel à une fonction toto() :
#includeint main() { printf("\n Appel de toto"); toto(); exit(0); }
La compilation de ce programme se passera sans problème : le programme transcrit chaque instruction en langage assembleur. En rencontrant la ligne toto();, le programme notera simplement l'appel à la fonction toto. Le résultat obtenu est un fichier objet.
Lors de l'édition des liens, le programme cherchera alors l'emplacement de la fonction
toto dans un des fichiers objet passés en paramètre. Il recherchera également la
fonction printf et la fonction exit. Dans le programme exécutable final,
il indiquera l'adresse de ces fonctions pour que celles-ci puissent être appelées.
En mode réel, le processeur ne peut adresser que le premier mégaoctet de la mémoire vive. Pour accéder à toute zone mémoire, le processeur a besoin d'une adresse sur 20 bits. Cependant, le processeur ne peut travailler qu'avec des octets (ou bytes : 8 bits), des mots (ou words : 16 bits), et à partir du 80386 des doubles (Dword : 32 bits). Cette adresse est donc composée de deux nombres de 16 bits : le Segment et l'Offset.
Voici comment le processeur retrouve l'adresse :
Adresse = Segment * 16 + Offset
Multiplier par 16 le segment revient, en binaire, à décaler de 4 bits vers la gauche le nombre obtenu : on simule un nombre sur 20 bits. Mais si on utilisait que le segment, on ne pourrait accéder qu'aux valeurs situées à des emplacements multiples de 16. On additionne alors le second nombre, l'offset. Ce qui implique que l'adresse A000:0032 correspond à la même zone mémoire que A001:0016 ou encore A002:0000. Il y a plein de combinaisons possibles.
[haut de page]Il s'agit en quelque sorte de variables temporaires, d'une "mémoire cache". On y dépose des valeurs temporaires, des paramètres, ... Le processeur peut accéder aux registres beucoup plus rapidement qu'il n'accède aux autres zones mémoires. De plus, le processeur utilise les registres pour des opérations qu'il ne peut faire en une seule fois.
Par exemple, le processeur est incapable de copier le contenu d'une zone mémoire dans une autre en une seule opération, il est obligé de passer par un registre :
Copier variable1 dans registre Copier registre dans variable2
Tous les registres peuvent se voir manipuler par n'importe quel programme, excepté Cs et Ip. Lors de certaines opérations, le processeur accède aux registres et éventuellement les modifie. Il est donc important de connaître, pour chaque fonction, les registres lus, et ceux modifiés. Pour les processeurs 8086 à 80286, les registres sont :
| Nom | Taille | Fonction |
|---|---|---|
| Cs (Code Segment) | 16 bits | Mémorise le segment où se trouve le code en cours d'exécution (ne peut pas être modifié par le programme). |
| Ds (Data Segment) | 16 bits | Mémorise le segment où se trouve les données du programme. |
| Ss (Stack Segment) | 16 bits | Mémorise le segment où se trouve la pile de données du programme |
| Es (Extra Segment) | 16 bits | Ce segment peut être utilisé pour faire ce que l'on veut. Par exemple, il peut pointer sur la mémoire écran. |
| Nom | Taille | Fonction |
|---|---|---|
| Ax (Accumulateur) | 16 bits | On l'utilise généralement pour des opérations arithmétiques, telles que MUL (multiplication) ou DIV (division). Ax peut se diviser en deux sous-registres de 8 bits. Ah représente les 8 premiers bits, et Al les 8 derniers. |
| Bx (Base) | 16 bits | Bx est utilisé lors de l'accès à une zone mémoire sous forme de tableau, il représente l'indice de ce tableau. Par exemple, on écrira Mov Dx, Es:[Bx]. Bx peut se diviser en deux sous-registres de 8 bits. Bh représente les 8 premiers bits, et Bl les 8 derniers. |
| Cx (Compteur) | 16 bits | Lors de l'appel d'instructions comme REP (répéter) ou LOOP (boucle), c'est le registre Cx qui est lu Cx peut se diviser en deux sous-registres de 8 bits. Ch représente les 8 premiers bits, et Cl les 8 derniers. |
| Dx (Données) | 16 bits | Ce registre est généralement utilisé pour stocker des données provisoires. Dx peut se diviser en deux sous-registres de 8 bits. Dh représente les 8 premiers bits, et Dl les 8 derniers. |
(à combiner avec une adresse de segment)
| Nom | Taille | Fonction |
|---|---|---|
| Si (Source Index) | 16 bits | Lors d'opérations sur les chaînes de caractères, comme MOVSB ou SCASB, Ds:[Si] désigne la variable 'source'. |
| Di (Destination Index) | 16 bits | Lors d'opérations sur les chaînes de caractères, comme MOVSB ou SCASB, Es:[Di] désigne la variable 'destination'. |
| Bp (Base Pointeur) | 16 bits | Bp a un rôle proche de celui de Bx, mais il est généralement utilisé avec le segment de pile (Ss:[Bp]). |
| Ip (Instruction Pointeur) | 16 bits | Cs:[Ip] indique la prochaine instruction à exécuter. Tout comme Cs, Ip ne peut être manipulé par le programme exécuté. |
| Sp (Stack Pointeur) | 16 bits | Ss:[Sp] indique le dernier élément de la pile. Chaque opération PUSH (empiler) ou POP (dépiler) modifie le registre Sp. |
Lorsqu'un registre peut contenir une adresse (les registres Bx, Bp, Si, Di, Ip, Sp), on peut accéder à la valeur placée à cette adresse en temps que variable :
; On place dans Bx l'adresse de la variable 'var' Mov Bx, Offset var ; On place à l'adresse d'offset contenue dans Bx ; (c'est-à-dire la variable 'var') la valeur 15 Mov [Bx], 15
Pour spécifier le segment, on le place devant :
; On place dans Bx l'adresse de la variable 'var' Mov Bx, Offset var ; On place à l'offset contenu dans Bx, ; et le segment contenu dans Es la valeur 15 Mov Es:[Bx], 15
Si aucun registre de segment n'est précisé, c'est le segment par défaut qui est utilisé : Cs pour Ip (mais de toutes façons il est impossible de modifier ces registres) Ds pour Bx et Si, Es pour Di, Ss pour Bp et Sp.
[Haut de page]Cette manipulation pourra vous sembler archaïque, puisqu'elle se base sur les standard de plus de vingt ans, ceux du 8086. Mais comme il s'agit d'assembleur 16 bits, tous les exemples sont dans le même cas.
Code Segment Assume Cs: Code Main Proc Mov Ax, 0B800h ; Le registre Ax prend la valeur hexa B800 Mov Es, Ax ; c'est l'adresse de la mémoire vidéo Mov Bx, 0 ; On place le code ascii 'A' à l'adresse Es:Bx, c'est-à-dire B800:0000. ; A l'adresse B800:0001 on trouve l'octet d'attribut couleur de 'A' ; Pour passer au prochain octet de donnée, on ajoute 2 à Bx Boucle: Mov Es:[Bx], Byte Ptr 'A' Add Bx, 2 ; Une page texte normale comporte 25 lignes de 80 caractères. ; On continue donc tant que Bx < 25 * 80 * 2 = 4000 ; JB signifie Jump if Below, "Saut si inférieur à" Cmp Bx, 4000 JB Boucle Main EndP Code EndS End Main
Ce court programme remplira la page écran de caractères 'A'.
[haut de page]C'est ce en quoi consiste le projet SOS. Il s'agit d'un programme que l'on copie sur le secteur de démarrage d'une disquette et qui s'exécute lorsque l'on démarre l'ordinateur.
[Haut de page]Si vous avez apprécié (ou détesté) cet article ou ce site, vous avez la possibilité de donner une note d'évaluation et indiquer les points forts, les points faibles de ce site. Ce service est offert par l'institut de statistiques Weborama.
Eric Minso - novembre 2003 - La Caverne Informatique - http://cavinfo.fr.st/