Table des matières
Aujourd'hui, je vous propose un petit exercice : programmer un petit programme inutile en assembleur 8086 et supérieur (certaines des instructions que l'on va utiliser sont apparues après le 8086 comme DIV qui est apparue avec le 8088).
Quand je dis inutile, c'est que je pense qu'écrire du code en assembleur est aujourd'hui dépassé car les compilateurs des langages de programmation de haut niveau (ex : C) sont optimisés donc on ne gagnera pas grand chose, en terme de performances, à programmer en assembleur.
Mais, comme toujours, il y a des exceptions : connaitre et/ou coder en assembleur peut être utile dans certains cas, notamment :
- Coder en assembleur des programmes "cas d'école" peut permettre au programmeur de comprendre un peu mieux le fonctionnement interne d'un ordinateur étant donné que ce langage est le plus proche de la machine. Si vous voulez plus proche, il vous reste le binaire 🙂 . Pour illustrer mes propos, il faut savoir que l'assembleur est enseigné en première année de DUT Informatique dans certains IUT.
- Un autre cas pour lequel l'assembleur peut s'avérer utile : écrire un mini-bootloader voir un mini-noyau, voir ... .
- Enfin, lorsque l'on désassemble un programme, mieux vaut connaître l'assembleur de l'architecture visée (rappelons que l'assembleur dépend de l'architecture 😉 )
Ceci étant dit, voila ce qui vous sera utile pour comprendre la suite : table des interruptions, la liste des instruction assembleur 8086 et modes d'adressages mémoire.
Exercice 1 : énoncé
Voici maintenant les "fonctionnalités" du programme à écrire :
- Afficher une chaine de caractères à l'écran.
- En utilisant le mode d'adressage immédiat, afficher une lettre (n'importe laquelle) à l'écran.
- En utilisant le mode d'adressage direct, afficher le premier caractère d'une chaine de caractère.
- En utilisant le mode d'adressage indirect et le registre BX, afficher deux caractères non consécutifs d'une chaine de caractères.
- En utilisant le mode d'adressage de base et le registre BX, afficher afficher un caractère de la chaine.
- En utilisant le mode d'adressage indexé direct, afficher un caractère de la chaine.
- On stocke un nombre d'une taille moyenne (> 100, < 1000 par exemple) "en dur" dans un registre et on l'affiche à l'écran.
Note : cet énoncé n'est pas de moi : je l'ai repris à T.M. / E.R. et je l'ai légèrement adapté.
Note : Bien que je ne sois pas fan des logiciels privateurs, je tiens à rester accessible pour la majorité de mes visiteurs. C'est pourquoi je propose une solution avec TASM et Windows.
Exercice 1 : sous Windows, avec TASM
Attention : seules les versions 32 bits de Windows ont un sous-système 16 bits (NTVDM pour XP, Windows On Windows depuis) et permettent donc d'exécuter des applications 16 bits. Les versions 64 bits en sont donc dépourvu. Comme compilateur, nous utiliserons TASM. Comme éditeur des liens (linker), nous utiliserons celui fournit avec, à savoir : TLINK.
Pourquoi ne pas faire une version 32 bits ? Parce que, à mon avis, sous Windows, ça manque de panache : on utilise plus les interruptions de la même manière, on se croirait presque plus en ASM vu toutes les fonctions externes que l'on charge ... Néanmoins si ça vous intéresse : Iczelion's Win32 Assembly Homepage.
Il n'y a rien de compliqué en soi. Il faut juste :
- Ne pas oublier qu'à la fin de la déclaration d'une chaine de caractères, il faut un "$".
- Il faut connaitre les principaux modes d'adressage du 8086 (je vous ai donné un lien plus haut).
- Il ne faut pas oublier d'initialiser le segment de données et de quitter le programme en douceur.
- Il faut connaitre les interruptions fournies par le "DOS" dont nous aurons besoin (afficher une chaine de caractère, afficher un caractère, quitter le programme en douceur). Je vous ai donné un lien plus haut.
- Il ne faut pas oublier que, dans la table ASCII, pour obtenir un chiffre sous forme de caractère affichable, il suffit de rajouter 30 en hexadécimal à la représentation binaire lui-même.
Pour compiler, linker et exécuter votre programme, il suffit d'ouvrir une invite de commande, de naviguer (commande cd) jusqu'au dossier dans lequel sont stockés TASM et TLINK (a moins d'avoir ajouter le dossier à la variable d'environnement PATH) et de taper :
> tasm votrefichier.asm > tlink votrefichier > votrefichier |
Voici ce que j'ai écris :
DOSSEG .MODEL SMALL ; 1 ko sera attribué au segment de pile .STACK .DATA message db 'Une chaine de caracteres bidon $' message1 db 'Question 1 : $' message2 db 13,10,'Question 2 : $' message3 db 13,10,'Question 3 : $' message4 db 13,10,'Question 4 : $' message5 db 13,10,'Question 5 : $' message6 db 13,10,'Question 6 : $' message7 db 13,10,'Question 7 : $' .CODE ; Initialisation du segment de données mov ax, @DATA mov ds, ax ; 1) Affichage d'une chaine mov ah, 09h mov dx, offset message1 int 21h mov ah, 09h mov dx, offset message int 21h ; 2) On affiche le caractère 'H' à l'écran mov ah, 09h mov dx, offset message2 int 21h mov ah, 02h mov dl, 'H' int 21h ; 3) On affiche la 1ere lettre de message mov ah, 09h mov dx, offset message3 int 21h mov ah, 02h mov dl, message int 21h ; 4) On affiche la 1ere et 21eme lettre de message mov ah, 09h mov dx, offset message4 int 21h mov bx, offset message mov ah, 02h mov dl, [bx] int 21h ; N'oubliez pas que l'on compte à partir de 0 mov bx, offset message+20 mov ah, 02h mov dl, [bx] int 21h ; 5) On affiche la 28eme lettre de message mov ah, 09h mov dx, offset message5 int 21h mov ah, 02h mov bx, offset message mov dl, [bx + 27] int 21h ; 6) On affiche la 15eme lettre de message mov ah, 09h mov dx, offset message6 int 21h mov SI, 14 mov ah, 02h mov bx, offset message mov dl, [bx + SI] int 21h ; 7) On stocke le nombre 666 en dur et on l'affiche mov ah, 09h mov dx, offset message7 int 21h mov ax, 666 mov bl, 100 div bl mov cx, ax mov ah, 02h mov dl, cl add dl, 30h int 21h mov ax, 0000h mov bl, 10 mov al, ch div bl mov cx, ax mov ah, 02h mov dl, cl add dl, 30h int 21h mov ah, 02h mov dl, ch add dl, 30h int 21h ; FIN mov ax, 4C00h int 21h END |
Exercice 1 : sous un Windows, avec Nasm et le linker Val
Nasm est un compilateur sous licence BSD. Je vous recommande la lecture de sa documentation qui est bien faite et enrichissante, aussi bien sur le logiciel lui-même que sur l'assembleur.
Concernant le linker, je vous recommande Val qui est dans le domaine public et dont les sources sont disponibles. La seule contrainte est que le nom reste le même.
A part une syntaxe différente, cette solution est la même que celle utilisant TASM et TLINK. Ainsi, je vous recommande d'aller lire la solution utilisant TASM pour plus d'informations.
Concernant les différences de syntaxe entre Nasm et TASM :
- exit les symboles peu clairs comme DOSSEG, .DATA, END, etc.
- exit le mot-clé OFFSET. Pour demander l'adresse mémoire d'une variable, on indique juste son nom
- Pour demander le contenu d'une adresse mémoire, on utilise [ ]
Pour compiler, linker et exécuter votre programme, il suffit d'ouvrir une invite de commande, de naviguer (commande cd) jusqu'au dossier dans lequel sont stockés Nasm et Val (a moins d'avoir ajouter le dossier à la variable d'environnement PATH) et de taper :
> nasm -f obj votrefichier.asm > val votrefichier.obj > votrefichier |
Vous savez l'essentiel pour refaire le même programme.
Voici ce que j'ai écris :
segment .data message db 'Une chaine de caracteres bidon $' message1 db 'Question 1 : $' message2 db 13,10,'Question 2 : $' message3 db 13,10,'Question 3 : $' message4 db 13,10,'Question 4 : $' message5 db 13,10,'Question 5 : $' message6 db 13,10,'Question 6 : $' message7 db 13,10,'Question 7 : $' segment stack stack resb 64 stackstop: segment .code ..start: mov ax, data mov ds, ax mov ax, stack mov ss, ax mov sp, stackstop ; 1) Affichage d'une chaine mov ah, 09h mov dx, message1 int 21h mov ah, 09h mov dx, message int 21h ; 2) On affiche le caractère "H" à l'écran mov ah, 09h mov dx, message2 int 21h mov ah, 02h mov dl, 'H' int 21h ; 3) On affiche la 1ere lettre de message mov ah, 09h mov dx, message3 int 21h mov ah, 02h mov dl, [message] int 21h ; 4) On affiche la 1ere et 21eme lettre de message mov ah, 09h mov dx, message4 int 21h mov bx, message mov ah, 02h mov dl, [bx] int 21h mov bx, message+20 mov ah, 02h mov dl, [bx] int 21h ; 5) On affiche la 28eme lettre de message mov ah, 09h mov dx, message5 int 21h mov ah,02h mov bx, message mov dl, [bx + 27] int 21h ; 6) On affiche la 15eme lettre de message mov ah, 09h mov dx, message6 int 21h mov si,14 mov ah,02h mov bx, message mov dl, [bx + si] int 21h ; 7) On stocke le nombre 666 en dur et on l'affiche mov ah, 09h mov dx, message7 int 21h mov ax, 666 mov bl, 100 div bl mov cx, ax mov ah, 02h mov dl, cl add dl, 30h int 21h mov ax, 0000h mov bl, 10 mov al, CH div bl mov cx, ax mov ah, 02h mov dl, cl add dl, 30h int 21h mov ah, 02h mov dl, CH add dl, 30h int 21h ; FIN mov ax, 4C00h int 21h |
Exercice 1 : sous un Windows, avec le compilateur Nasm mais sans linker
Cette solution est très semblable à celle utilisant Nasm et Val, seule la syntaxe change un peu. Ainsi, je vous recommande d'aller lire la solution utilisant Nasm et Val pour plus d'informations. Je pense que cette solution n'est pas très propre. Néanmoins je vous la montre, libre à vous après de faire un choix.
Pour compiler, linker et exécuter votre programme, il suffit d'ouvrir une invite de commande, de naviguer (commande cd) jusqu'au dossier dans lequel est stocké Nasm (a moins d'avoir ajouter le dossier à la variable d'environnement PATH) et de taper :
> nasm -f bin votrefichier.asm -o nom.exe > nom.exe |
Et voici ce que j'ai écris :
org 100h section .data message db 'Une chaine de caracteres bidon $' [... Pareil que précédemment ... ] section .text start: ; 1) Affichage d'une chaine mov ah, 09h mov dx, message1 int 21h [... Pareil que précédemment ... ] ; FIN mov ax, 4C00h int 21h |
Exercice 1 : sous un GNU/Linux, avec Nasm et ld
D'après la doc de Nasm, le format de fichier ELF ne supporte pas les relocations 32 bits -> 16 bits. Néanmoins ld les supporte comme une extension. J'ai essayé pas mal de choses, j'ai cherché sur le net mais je ne suis pas parvenu à linker mon programme 16 bits (utilisation de [BITS 16] dans le code) car ld me renvoi toujours un "relocation truncated to fit: R_386_16 against `.data'".
Néanmoins ce n'est pas une grande perte : cette méthode n'est pas recommandable comme on peut le lire un certain nombre de fois sur internet. Même la documentation de Nasm ne traite pas le fait d'écrire des programmes 16 bits sous GNU/Linux alors qu'elle le fait pour Windows.
Il est néanmoins possible d'écrire du code 32 bits. Ce qui va changer par rapport à Windows, c'est qu'on ne fera plus int 21h mais int 80h pour appeler le noyau et lui demander un service. Les services sont ici les fonctions qu'on a l'habitude d'utiliser en C : exit, write, open, etc. On leur passe les arguments nécessaires, dans l'ordre, dans les registres eax, ebx, ecx, edx ou par la pile (attention à bien empiler : on commence par empiler le dernier argument, ..., puis le premier). Plus de détails : i386 Linux 2.2+ Syscalls et FreeBSD Assembly Language Programming
Pour compiler :
$ nasm -f elf32 votrefichier.asm $ ld -s votrefichier.o -o nom |
ou (sur un OS 64 bits (x86_64)) :
$ nasm -f elf64 votrefichier.asm $ ld -s votrefichier.o -o nom |
ÉDIT du 03/04/2012 à 16h15 :
Sur un OS 64 bits, il vaut mieux compiler et linker de cette manière :
$ nasm -f elf votrefichier.asm $ ld -m elf_i386 -s votrefichier.o -o nom |
En effet, en utilisant la première méthode, la compilation et le linkage s'effectueront sans problèmes mais vous pourriez rencontrer des erreurs de segmentation sur certains programmes et notamment ceux qui font des manipulations sur des variables mémoires (logique ...) ou qui manipulent la pile. Fin de l'édit.
Et voici ce que j'ai écris :
section .data message db 'Une chaine de caractères bidon' lenMessage equ $-message message1 db 'Question 1 : ' lenMessage1 equ $-message1 message3 db 10,'Question 3 : ' lenMessage3 equ $-message3 message4 db 10,'Question 4 : ' lenMessage4 equ $-message4 message7 db 10,'Question 7 : ' lenMessage7 equ $-message7 fin: db 10,'FIN',10 lenFin: equ $-fin ; On déclare des variables non initialisées section .bss chiffre1: resb 1 chiffre2: resb 1 chiffre3: resb 1 section .text global _start _start: ; 1) Affichage d’une chaine ; Sous les systèmes de type UNIX, tout est fichier. ; La console est un fichier. On écrit dedans avec write(). mov eax, 4 mov ebx, 1 mov ecx, message1 mov edx, lenMessage1 int 80h mov eax, 4 mov ebx, 1 mov ecx, message mov edx, lenMessage int 80h ; 2) On affiche le caractère « H » à l’écran ; La solution revient à faire 3) car il faut declarer le ; caractère 'H' dans le segment de données et l'afficher ; 3) On affiche la 1ere lettre de message mov eax, 4 mov ebx, 1 mov ecx, message3 mov edx, lenMessage3 int 80h mov eax, 4 mov ebx, 1 mov ecx, message mov edx, 1 int 80h ; 4) On affiche la 1ere et la 21eme lettre mov eax, 4 mov ebx, 1 mov ecx, message4 mov edx, lenMessage4 int 80h mov esi, message mov eax, 4 mov ebx, 1 mov ecx, esi mov edx, 1 int 80h mov esi, message+20 mov eax, 4 mov ebx, 1 mov ecx, esi mov edx, 1 int 80h ; 5) On affiche la 28eme lettre de message ; Pas possible ; 6) On affiche la 15eme lettre de message ; pas possible ; 7) On stocke le nombre 666 en dur et on l’affiche mov eax, 4 mov ebx, 1 mov ecx, message7 mov edx, lenMessage7 int 80h mov dx, 0 mov ax, 666 mov bl, 100 div bl add al, 30h mov [chiffre1], al mov bl, 10 mov al, ah mov ah, 0 div bl add al, 30h mov [chiffre2], al add ah, 30h mov [chiffre3], ah mov eax, 4 mov ebx, 1 mov ecx, chiffre1 mov edx, 1 int 80h mov eax, 4 mov ebx, 1 mov ecx, chiffre2 mov edx, 1 int 80h mov eax, 4 mov ebx, 1 mov ecx, chiffre3 mov edx, 1 int 80h ; FIN mov eax, 4 mov ebx, 1 mov ecx, fin mov edx, lenFin int 80h mov eax, 1 mov ebx, 0 int 80h |
Comme vous le constater, certains adressages ne sont pas possibles. Cela est dû au fait que la fonction write attend une adresse mémoire. Or, certains adressages retournent le contenu d'une adresse mémoire. On parait toujours trouver une parade mais cela reviendrait à un adressage direct ou à un adressage indirect via un registre (ou alors n'ai-je pas assez réfléchis ?).