lalahop

Assembleur 8086+

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 :

  1. Afficher une chaine de caractères à l'écran.
  2. En utilisant le mode d'adressage immédiat, afficher une lettre (n'importe laquelle) à l'écran.
  3. En utilisant le mode d'adressage direct, afficher le premier caractère d'une chaine de caractère.
  4. En utilisant le mode d'adressage indirect et le registre BX, afficher deux caractères non consécutifs d'une chaine de caractères.
  5. En utilisant le mode d'adressage de base et le registre BX, afficher afficher un caractère de la chaine.
  6. En utilisant le mode d'adressage indexé direct, afficher un caractère de la chaine.
  7. 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 ?).