S1825 - Travaux Pratiques

Introduction au langage d'assemblage x86 (IA-32)

Novembre 2009

Introduction

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 :

http://www.gnu.org/software/binutils

Le processus de compilation depuis un langage de haut-niveau (ici du C)

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 :

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.

Utiliser GDB

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.

Exercice n°1 : stack/frame

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

Exercice n°2 : paramètres de la ligne de commande

É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

Exercice n°3 : sscanf

L'appel système sscanf(argv[1], "%d", &i)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

Exercice n°4 : suite de Fibonacci

Ecrivez un programme qui calcule la fonction de Fibonacci F(n)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)