Accueil de la Caverne Informatique - Le langage C

L'assembleur 16-bits

Introduction à l'assembleur

Qu'est-ce que l'assembleur ?

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é).

Intérêt de l'assembleur

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]

Hello, World !

Premier code

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

"J'y comprends rien !"

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.

[plus de détails sur le format des exécutables Windows] - [haut de page]

Table d'équivalence binaire, décimal, hexadécimal

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.

 
Binaire
 
 
Décimal
 
 
Hexadécimal
 
0 0 0
1 1 1
10 2 2
11 3 3
100 4 4
101 5 5
110 6 6
111 7 7
1000 8 8
1001 9 9
1010 10 A
1011 11 B
1100 12 C
1101 13 D
1110 14 E
1111 15 F
10000 16 10
 
Binaire
 
 
Décimal
 
 
Hexadécimal
 
10001 17 11
10010 18 12
10011 19 13
10100 20 14
10101 21 15
10110 22 16
10111 23 17
11000 24 18
11001 25 19
11010 26 1A
11011 27 1B
11100 28 1C
11101 29 1D
11110 30 1E
11111 31 1F
100000 32 20
100001 33 21

A quoi ça sert, l'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...

Conversion Binaire <=> Décimal

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.

Conversion Binaire <=> Hexadé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]

Syntaxe de l'assembleur (avec tasm)

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 :

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 :

Pour manipuler une valeur dans un tableau, écrire Nom_Tableau1[0] (= 1)

[haut de page]

Les instructions de base de l'assembleur

Affectations

; 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

Arithmétique

; 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

Logique

; 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)

Comparaisons

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]

Caractères et chaînes de caractères

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]

Les pointeurs

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.

Compilation et édition des liens

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() :

#include 

int 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.

[haut de page]

Segments et Offset

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]

Les registres du processeur

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 :

Registres de Segment

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.

Registres de Travail

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.

Registres d'Offset

(à 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]

Manipuler la mémoire vidéo

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]

Créer une disquette bootable

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]


Accueil de la Caverne Informatique - Le langage C

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/