Le but de ces travaux pratiques est de vous familiariser avec les outils nécessaires à la programmation en langage d'assemblage x86 sous un environnement de type Unix (Linux, MacOS X, Cygwin). Vous commencerez également à étudier le langage d'assemblage x86 lui-même et l'ISA (Instruction Set Architecture) des processeurs Intel et AMD. Dans ce document, nous appelons langage d'assemblage x86 (IA-32) le langage d'assemblage des processeurs Intel/AMD en 32 bits. Ce langage est utilisable sur les processeurs 80386 et ultérieurs (processeurs Intel 486, Pentium, Pentium II, Pentium III, Pentium 4, Core, Core 2, Xeon; processeurs AMD Athlon et Opteron). Vous trouverez l'intégralité des manuels pour l'architecture IA-32 Intel à l'URL suivante :
http://www.intel.com/products/processor/manuals.
Parmi ces manuels, le premier à lire est
http://download.intel.com/design/processor/manuals/253665.pdf
qui décrit l'architecture IA32 en détails. Vous pouvez vous contenter de le survoler dans un premier temps puisqu'il fait quand même plusieurs centaines de pages.
Un guide succinct avec quelques instructions assembleur est disponible ici.
Les outils nécessaires sont présents sur quasiment tout système Unix: un
éditeur de texte (par exemple vi
ou emacs
), l'outil
d'assemblage GNU as
(appelé également l'assembleur) et l'éditeur
de lien GNU ld
. Ces deux outils font partie de la collection
d'outils binutils
dédiés à la manipulation de code binaire. Le
manuel de ces outils se trouve à l'URL ci-dessous :
Avant de nous attaquer au langage d'assemblage, nous allons examiner les différentes étapes nécessaires à la compilation et l'exécution d'un programme écrit dans un langage de haut-niveau afin de voir comment ces différentes phases s'articulent et de déterminer la frontière entre le matériel et le logiciel.
Commençons tout d'abord par notre exemple qui est une programme écrit en
langage C. Pratiquement tous les systèmes d'exploitation (Linux, Windows, Mac
OS X, Solaris) sont écrits en langage C avec des sous parties écrites
directement en langage d'assemblage. Notre exemple se trouve dans un fichier
appelé 'hello.c' et ne fait qu'une seule chose, afficher
la chaîne de caractères Hello, world!
dans le terminal :
#include <stdio.h> main() { printf("Hello, world!\n"); }
Ce programme est très connu car c'est le premier exemple du premier manuel du langage C écrit par ses créateurs Brian W. Kernighan et Dennis M. Ritchie (The C programming language).
Ce programme, pour être exécuté, doit tout d'abord être transformé en langage binaire pour l'ISA IA-32. Plusieurs outils concourent à cette transformation :
cpp
qui va
transformer le code source en un autre code source après traitement des
macros : dans l'exemple précédent cpp
va remplacer la
directive #include <stdio.h>
par le contenu du fichier
stdio.h
qui se trouve dans un répertoire prédéterminé
(habituellement '/usr/include
'). Voici le résultat du traitement
de hello.c
par cpp
: hello.cpp
. Cette phase transforme du code
source ASCII en code source ASCII; le compilateur cc1
de la suite GCC qui va transformer
le code source C traité par cpp
en langage d'assemblage x86. Voir
le fichier hello.s
, destiné à un système Linux, reproduit ci-dessous :
.file "hello.c" .section .rodata .LC0: .string "Hello, world!\n" .text .globl main .type main, @function main: pushl %ebp movl %esp, %ebp subl $8, %esp andl $-16, %esp movl $0, %eax subl %eax, %esp movl $.LC0, (%esp) call printf movl %ebp,%esp # nettoyage de la pile de la fonction popl %ebp # esp pointait sur la valeur sauvée de ebp ret .size main, .-main .section .note.GNU-stack,"",@progbits .ident "GCC: (GNU) 3.3.5 (Debian 1:3.3.5-13)"
Il est à noter que cette étape transforme encore du code source ASCII en un autre code source ASCII, ce qui implique que jusqu'à cette étape, les programmes sont lisibles par un humain ;
as
va transformer le code source écrit en langage
d'assemblage en un code écrit en langage machine de l'ISA x86. Ce code est appelé du
code "objet" et a comme particularité d'utiliser des adresses "virtuelles",
c'est-à-dire que le programme considère que son espace d'adressage démarre à 0
et que toute la RAM est disponible. C'est le matériel qui lors de l'exécution
va dynamiquement transformer ces adresses virtuelles en adresses physiques.
Voir ici le contenu du fichier binaire
hello.o
(obtenu avec la commande od -ax hello.o
qui
permet d'afficher en hexadécimal et en ASCII le contenu d'un fichier binaire)
produit par as
depuis le fichier hello.s
. Ce fichier
binaire n'est pas juste une suite d'instructions de l'ISA x86. Ce fichier
binaire est en format ELF qui signifie Executable
and Linkable Format. Ce format, en plus des instructions du programme,
contient des informations sur la table des symboles, sur l'ISA utilisé par les
instructions binaires, sur les fonctions externes utilisées (par exemple
printf
). Ce format permet au système d'exploitation d'exécuter le
programme en le liant aux bibliothèques dont il a besoin et en s'assurant que
l'ISA utilisée est la bonne ;hello.o
n'est pas exécutable
directement. En effet, il faut savoir où se trouve le point d'entrée dans le
programme et avoir fait le lien avec les codes de bibliothèque utilisés.
Concrètement, il faut que le code spécifie que le programme commence par la
fonction main
et il faut que la fonction printf
soit
identifiée et trouvée dans une bibliothèque existant sur le système. Cette
phase s'appelle l'édition de liens et est prise en charge par l'outil
ld
ou collect
qui est un outil de la suite GCC.
Après cette phase, le code binaire est exécutable. Un utilisateur n'a pas besoin de connaître toutes ces commandes. La
commande gcc
est un raccourci pour effectuer toutes ces phases
automatiquement. Par exemple, en partant du programme source
hello.c
, la commande
gcc -v -o hello hello.c
permet d'obtenir un fichier executable au format ELF hello
en
exécutant l'une après l'autre toutes les étapes décrites ci-dessus.
La documentation de gdb
se trouve à l'URL ci-dessous :
http://www.gnu.org/software/gdb
Voir également http://dirac.org/linux/gdb/ pour un tutoriel sur GDB.
Repartez de l'exemple hello.s et supprimez la
ligne popl %ebp
. Compilez (le plus simple est de faire:
gcc -o hello hello.s
), exécutez votre programme et essayez
de comprendre ce qui se passe (par exemple en utilisant gdb
).
Écrivez un programme qui affiche le nombre d'arguments passés
sur la ligne de commande. Ce nombre d'arguments se trouve
quelque part sur la pile et est le premier argument de la
fonction main
. En langage C, on écrirait
printf("%d\n", argc)
pour afficher le nombre
d'arguments.
Si vous êtes perdus, vous pouvez essayer d'écrire le
programme en C et de le compiler avec
gcc -S print.c
pour obtenir un fichier
print.s
qui vous donnera une
solution. Attention, cette solution n'est pas forcément la seule
et la plus lisible. Essayez d'obtenir la même chose par
vous-même.
Corrigé : exo2.s
sscanf
L'appel système sscanf(argv[1], "%d", &i)
où
i
est une variable entière (un entier signé sur 32
bits) lit la chaîne de caractère argv[1]
, la
convertit en nombre et met ce nombre dans
i
. Modifiez le programme précédent pour qu'il
calcule le nombre passé en argument plus un. Par exemple:
$ ./monprog 34 35 $ ./monprog -3 -2
Corrigé : exo3.s
Ecrivez un programme qui calcule la fonction de Fibonacci
F(n) où n est le premier argument (donc représenté
sous la forme d'une chaîne de caractères dans
argv[1]
) et qui affiche sa valeur avec
printf
. On rappelle que F(0)=1,
F(1)=1, F(n+1)=F(n)+F(n-1) pour
n>=1. Pour indication, F(10)=89,
F(40)=165580141 et F(50)=20365011074.
Vous écrirez pour cela une fonction
fib
, en rajoutant à la fin de la fonction
main
les lignes:
fib: movl 4(%esp), %eax
et en complétant. Notez que movl 4(%esp), %eax
récupère dans le registre %eax
la valeur du
paramètre de la fonction fib
, c'est-à-dire le
n dont on veut calculer F(n). Le résultat
F(n) sera mis dans %eax
avant de revenir
de fib
.
Finalement, modifiez main
pour qu'il appelle
successivement sscanf
pour lire la valeur de
l'argument, fib
pour calculer F(n) et
printf
pour afficher la valeur calculée.
Faîtes deux versions de votre programme: une version récursive et une version itérative.
Testez vos programmes en calculant la valeur de F(n) jusqu'à n=50. Quelles conclusions pouvez-vous en tirer ?
Corrigé : exo4.s (version récursive)
Corrigé : exo4b.s (version itérative)