Introduction à la programmation Nasm x86
Introduction à la programmation Nasm x86
EISC 1
2018-2019
1 Premier programme
1.1 Structure d’un programme Nasm
Un programme en Nasm a la structure suivante :
global main ; declaration de main en global. 1
2
segment .data 3
; declarations des variables initialisees. 4
segment .bss 5
; declarations des variables non initialisees. 6
segment .text 7
; vos fonctions. 8
9
main : ; point d’entre du programme. 10
; instructions de votre programme 11
12
mov ebx, 0 ; code de sortie du programme. 13
∗
Voir [Link]
1
Les trois dernières lignes indiquent la fin du programme, elles devront apparaître
dans tous vos programmes. À votre avis quelle est l’utilité du point virgule en Nasm ?
Créez un fichier [Link]. Tapez le programme ci-dessous qui affiche le traditionnel
message Hello world.
global main 1
2
segment .data 3
msg : 4
db "Hello␣World", 10 ; declaration de la chaine de caracteres a afficher, 5
; 10 est le code ASCII du retour a la ligne. 6
7
segment .text 8
9
main : 10
; l ’ affichage necessite 3 arguments 11
mov ebx, 1 ; arg1, numero de la sortie pour l’affichage, 12
1.2 Compilation
Vous devez compiler votre programme avant de pouvoir l’exécuter. Ouvrez un ter-
minal, se positionner dans le répertoire contenant le fichier [Link] et saisir les com-
mandes
nasm -f elf32 [Link] 1
ld -o Hello -melf_i386 -e main Hello.o
2
La première commande crée un fichier objet Hello.o et la dernière réalise l’édition des
liens pour obtenir un exécutable Hello. Pour exécuter le programme, il suffit d’utiliser la
commande ./Hello.
Exercice 1. Créez et exécutez un programme qui affiche "Bonne journée !" sans re-
tourner à la ligne. Votre programme devra s’appeler [Link] et son exécutable
Bonjour.
2
1.3 Registres
Les processeurs 32 bits d’architecture x86 travaillent avec des registres qui sont en
nombre limité et d’une capacité de 32 bits, soit 4 octets. Parmi ces registres, les registres
appelés eax, ebx, ecx, edx, edi et esi sont des registres à usage général. L’instruction mov
(déjà utilisée dans le programme [Link]) permet d’affecter une valeur à un registre :
mov eax, 3 ; eax = 3 1
mov eax, 0b101 ; eax = 0b101 = 5 2
mov ebx, eax ; ebx = eax
3
Il existe plusieurs formats pour spécifier une valeur. Par défaut, le nombre est donné en
décimal. Une valeur préfixée par 0b correspond à un nombre donné en binaire.
2 Bibliothèque asm_io
Comme vous l’avez constaté avec le programme [Link], l’affichage est un peu
lourd en Nasm. Afin de se simplifier la vie, nous allons utiliser la bibliothèque asm_io.
Téléchargez les fichiers asm_io.asm et asm_io.inc à l’adresse
[Link]
et ajoutez les dans votre dossier de travail. Cette bibliothèque fournit plusieurs fonctions
comme print_string, et print_int. Voici quelques explications :
1. print_string affiche la chaîne de caractères (terminée par un octet de valeur 0) dont
l’adresse est contenue dans eax ;
2. print_int affiche l’entier signé contenu dans eax.
Nous allons voir comment les utiliser sur un exemple. Testez le programme ci-dessous
que vous nommerez test_biblio.asm.
%include "asm_io.inc" ; importation de la bibliotheque 1
global main 2
3
section .data 4
chaine : db "Le␣registre␣eax␣contient␣:␣", 0 5
retourligne : db 10, 0 6
7
section .text 8
main : 9
mov eax, chaine 10
call print_string ; affichage de chaine. 11
12
mov eax, 13 13
call print_int ; affichage de la valeur du registre eax. 14
15
mov eax, retourligne 16
call print_string ; affichage d’une nouvelle ligne. 17
3
18
; fin du programme 19
mov ebx, 0 20
mov eax, 1 21
int 0x80
22
Pour compiler votre programme (qui utilise la bibliothèque) saisissez les commandes
nasm -f elf32 asm_io.asm 1
nasm -f elf32 test_biblio.asm 2
ld -o test_biblio -melf_i386 -e main test_biblio.o asm_io.o
3
Il est à noter que la 1re ligne permet l’obtention de asm_io.o et que celle-ci n’est à exécuter
qu’une seule fois pour tout le TP : en effet, une fois asm_io.o obtenu, il est inutile de le
générer à nouveau.
3 Mémoire
3.1 Les sous-registres
Les registres eax, ebx, ecx et edx sont subdivisés en sous-registres. La figure 1 montre
la subdivision de eax en ax, ah et al. Le premier octet (celui de poids le plus faible) de
31 ··· 16 15 ··· 0
eax
ax
ah al
15 · · · 8 7 ··· 0
eax est accessible par le registre al (de capacité 8 bits), le deuxième octet de poids le plus
faible est accessible par le registre ah. Les 16 bits de poids faible de eax sont accessibles
par le registre ax (qui recouvre al et ah). Noter que les 2 octets de poids fort ne sont pas
directement accessibles par un sous-registre. De même pour ebx, ecx, et edx, on dispose
des registres analogues bx, bh, bl, cx, ch, cl et dx, dh, dl.
Important. Lorsque l’on modifie le registre al, les registres ax et eax sont eux aussi
modifiés. En effet, al est physiquement une partie de ax qui lui-même est une partie de
eax. Cette remarque est évidement valable pour les autres registres et leurs sous-registres.
4
Exercice 5. Quelles sont les valeurs de eax, ax, ah et al après l’instruction mov eax,
134512768 ? Quelles sont ensuite les valeurs de eax, ax, ah et al après l’instruction mov
al, 0 ?
3.2 Adresse
La mémoire peut être vue comme un tableau de 232 cases contenant chacune un octet.
Le numéro (ou l’indice) d’une case est appelé son adresse. La mémoire est représentée
par un tableau vertical dont les cases indexées de la plus petite adresse à la plus grande.
Une adresse est codée sur 32 bits. Le contenu des registres 32 bits (comme eax, ebx, etc.)
peut représenter un nombre ou adresse en mémoire.
La figure 2 illustre un exemple fictif d’état de la mémoire. Les adresses y sont notées
en hexadécimal.
Adresse Valeur
0x00000000 3
0x00000001 30
0x00000002 90
0x00000003 10
0x00000004 16
0x00000005 9
.. ..
. .
0x00000010 127
.. ..
. .
0xfffffffe 30
0xffffffff 3
Exercice 6. (Mémoire)
Le nombre d’octets lus dépend de la taille de reg. Par exemple, 1 octet sera lu pour
al ou ah, 2 octets seront lus pour ax et 4 pour eax. Un autre exemple :
5
mov al, [0x00000003] ; al recoit l ’ octet stocke a l ’ adresse 3 1
; dans l ’ exemple, al = 10 = 0x0a; 2
mov al, [3] ; Instruction equivalente a la precedente.
3
Au lieu de donner explicitement l’adresse où lire les données, on peut lire l’adresse
depuis un registre. Ce registre doit nécessairement faire 4 octets. Par exemple,
mov eax, 0 ; eax = 0 1
mov al, [eax] ; al recoit l ’ octet situe a l ’ adresse contenue dans eax 2
; dans l ’ exemple, al = 0x03.
3
Exercice 8. Dans l’exemple de mémoire de la figure 2, donnez les valeurs des (sous-
)registres demandés, après les instructions suivantes (avant chaque sous-question, eax
est supposé égal à 0) :
1. ax après l’instruction mov ax, [1] ;
2. ah après l’instruction mov ah, [0] ;
3. eax après l’instruction mov eax, [0] ;
4. eax après l’instruction mov eax, [1].
6
Exercice 9. Testez chacune de ces opérations. Par exemple, faites un programme qui
place x et y dans des registres et affichent leur addition.
Exercice 10. Les intructions not, and, or et xor sont-elles des opérations bit-à-bit ?
5 Les sauts
Jusqu’à présent, vos programmes réalisaient les instructions dans l’ordre ligne à ligne.
Les sauts vous permettent de passer d’un endroit à un autre dans le code. Cela est, par
exemple, utile pour réaliser des boucles. On distingue deux types de sauts : les sauts
inconditionnels et les sauts conditionnels.
Vous pouvez placer dans votre programme des étiquettes comme ceci :
; ... 1
etiq_1 : 2
; ...
3
Il vous est ensuite permis d’aller à n’importe laquelle de ces étiquettes grâce à l’instruction
jmp etiquette
1
section .text 5
main : 6
mov eax, bo 7
jmp etq 8
9
mov eax, me 10
etq : 11
call print_string ; Affichage de chaine.
12
Exercice 13. Réalisez un programme qui affiche avec retour à la ligne les nombres
de 0 à l’infini. Comment interrompre le programme ?
7
5.2 les sauts conditionnels
Les sauts conditionnels sont des sauts qui ne sont réalisés que sous certaines condi-
tions. Une façon simple d’utiliser les sauts conditionnels est en conjonction avec l’instruc-
tion cmp. Par exemple, dans
cmp eax, 0 1
je etq 2
mov ebx, ecx
3
si eax est égal à 0, le programme saute jusqu’à l’étiquette etq et sinon il continue à l’ins-
truction suivante, c’est à dire mov ebx, ecx dans cet exemple. Il existe d’autres instructions
de saut. Par exemple,
cmp eax, 0 1
jge etq
2
Exercice 14. Réalisez un programme qui affiche avec retour à la ligne les nombres
de 0 à 50.
Exercice 18. On rappelle qu’un nombre est dit premier si ses seuls diviseurs sont 1 et
lui-même. Faites un programme qui affiche avec retour à la ligne "x est premier" si le
nombre x placé dans ecx est premier et "x n’est pas premier" sinon.
6 Tableau
Vous pouvez déclarer un tableau dans la section .bss (voir la section 1) par la procé-
dure suivante
section .bss 1
tab: resd 50
2
Cette instruction déclare un tableau tab de 50 × 4 octets. Attention : le tableau n’est pas
initialisé pour autant. Pour affecter la valeur de eax à la case i du tableau, on procède
ainsi
mov [tab + ((i-1)*4)], eax 1
mov dword [tab + ((i-1)*4)], 13
2
8
Exercice 19. Faites un programme qui calcule et affiche la somme et la moyenne des
éléments d’un tableau.
Exercice 20. * On suppose maintenant que les éléments du tableau sont des notes
comprises entre 0 et 20. Faites un programme qui affiche les notes dans l’ordre crois-
sant.
7 La pile
La pile est une zone de la mémoire dans laquelle on peut empiler et dépiler des
données. Pour empiler une valeur sur la pile, on peut procéder ainsi
push 10 ; empile la valeur 10 1
push eax ; empile la valeur contenue dans eax 2
push [eax] ; empile la valeur contenue a l’adresse indiquee par eax
3
Pour dépiler la dernière valeur ajoutée à la pile, on peut procéder ainsi
pop eax
1
Cette instruction extrait la dernière valeur ajoutée à la pile et la place dans le registre
eax.
Exercice 21. Faites un programme qui empile les valeurs 1, 2 et 3 puis les dépile. On
affichera la valeur dépilée après chaque dépilement. Que constatez-vous ?
Le registre esp contient l’adresse de la tête de la pile. Vous n’avez pas à le mettre à
jour, le système le fait déjà pour vous ! La dernière valeur ajoutée à la pile est donc [esp].
Exercice 22. Affichez la valeur de esp avant et après chaque instruction du pro-
gramme suivant
push 3 1
push 5 2
push 8 3
pop eax 4
pop eax 5
pop eax
6
Que constatez-vous ?
Exercice 23. Donnez des suites d’instructions utilisant uniquement les instructions
mov, sub et add afin de simuler l’instruction.
push eax
1
9
8 Fonction
On peut déclarer une fonction fonc dans la zone segment .text (voir la section 1.1)
ainsi
fonc : 1
; code de la fonction 2
ret
3
Vous l’avez déjà fait avec les fonctions print_string et print_int. Voici un schéma mon-
trant le comportement d’un programme qui appelle une fonction nommée morceau :
Exercice 25. Faites une fonction qui réalise l’addition des registres eax et ebx. Votre
programme devra appeler votre fonction et afficher le résultat.
Vous avez passé à votre fonction les paramètres (les nombres à additionner) par les
registres eax et ebx. Cependant, votre fonction est susceptible d’appeler une autre fonction
(y compris elle-même) ou d’avoir plus que deux paramètres, expliquez quels problèmes
cela peut engendrer ?
Afin de palier à ces problèmes, nous allons utiliser la pile selon la convention suivante
(même convention que le langage C). Vos fonctions seront déclarées ainsi
fonc : 1
push ebp 2
mov ebp, esp 3
; code de la fonction 4
pop ebp 5
ret
6
et appelées ainsi
push ARG_N 1
... 2
push ARG_1 3
10
4
call fonc 5
6
add esp, 4*N
7
imul ebx 9
10
dec ebx 11
push ebx 12
call fonc 13
pop ebx 14
15
fin : 16
pop ebp 17
ret
18
Exercice 27. Expliquez les rôles de ebp et de l’instruction add esp, 4*N. Quelle est la
différence entre jmp et call ?
Exercice 28. Réalisez une fonction qui calcule la somme des n premiers termes
(n + (n − 1) + · · · + 2 + 1). Votre programme devra respecter la convention, appeler votre
fonction et afficher le résultat.
Programmez en respectant la convention la fonction d’Ackermann. Calculez A(2, 2), A(2, 3),
A(4, 1) et A(4, 3).
9 Problèmes
La bibliothèque asm_io (voir section 2) fournit également une fonction nommée read_int.
En l’appelant vous pouvez récupérer dans le registre eax l’entier que l’utilisateur aura tapé
au clavier. Essayez !
11
Problème 1. (Jeu de la vie en une dimension) L’utilisateur entre une configuration de
départ, c’est à dire un tableau binaire (ne contenant que des 0 et des 1), et un nombre
d’itération. À chaque itération, vous transformerez et afficherez le tableau ainsi :
si la case est dans l’une de ces deux conditions :
— la case à la même valeurs que ses voisines de gauche et de droite,
— la case vaut 0, sa voisine de gauche vaut 1 et sa voisine de droite vaut 0
alors elle est transformée (ou conservée) en 0. Dans tous les autres cas, elle est trans-
formée (ou conservée) en 1. On suppose que la case à gauche de la case d’indice 0 et
la case à droite de la case d’indice "taille du tableau" valent 0. Soignez l’affichage !
Problème 4. Faites un programme qui propose des grilles de Sudoku, permette d’y
jouer et propose une solution quand cela est possible.
12