4MMPS x86

De Ensiwiki
Aller à : navigation, rechercher
AttentionCette page est maintenue uniquement par les enseignants. Afin de ne pas perturber le déroulement des cours, elle n'a pas vocation à être modifiée par les élèves. Mais si vous avez des modifications à proposer, merci d'en discuter ou d'envoyer un e-mail aux auteurs de la page (cf. historique)

Mycomputer.png  Deuxième Année  CDROM.png  Informatique 

Fleche gauche.png
Fleche haut.png
Fleche droite.png

Cette page contient des rappels rapides concernant l'assembleur des processeurs Intel Pentium 32 bits. Elle n'a pas pour but d'être un cours complet d'assembleur x86, mais juste ce qu'il est nécessaire de savoir pour le mini-projet. Des bases d'architecture des ordinateurs et d'assembleur (quelque-soit le processeur) sont nécessaires pour la programmation système.

Rappels généraux sur le processeur Pentium

Le Pentium est un processeur « 32 bits » : cela signifie que le mot de donnée de base et les adresses en mémoire sont codés sur 4 octets.

Le Pentium contient un nombre limité de registres 32 bits. On détaille ci-dessous ceux qui nous serviront en pratique :

Registres du processeur Pentium
Registre 32 bits bits de 31 à 16 bits de 15 à 8 bits de 7 à 0 Nom courant
%eax %ax accumulateur
%ah %al
%ebx %bx index de base
%bh %bl
%ecx %cx compteur
%ch %cl
%edx %dx registre de données
%dh %dl
%esi %si index source
%edi %di index de destination
%esp %sp pointeur de pile
%ebp %bp pointeur de base
%eip %ip pointeur d'instruction
%eflags %flags registre des indicateurs

Les registres du Pentium sont fractionnables. Par exemple, le nom %ax peut-être utilisé dans un programme assembleur pour accéder aux 16 bits de poids faibles de %eax (les 16 bits de poids forts ne portent pas de nom particulier). De plus, on peut accéder aux 8 bits de poids faibles de %ax (et donc aussi de %eax) en utilisant le nom %al. Le nom %ah permet d'accéder aux 8 bits de poids fort de %ax.

Les 4 registres généraux (%eax, %ebx, %ecx et %edx) peuvent être utilisés pour effectuer des calculs quelconques. Les registres %esi et %edi ont des rôles particuliers lorsqu'on utilise certaines instructions, mais dans notre cas on les utilisera comme des registres de calcul généraux.

Les registres de pile (%esp et %ebp) ont des fonctions particulières que l'on détaillera plus tard. On ne doit jamais les utiliser pour effectuer des calculs.

Le registre %eip pointe en permanence sur la prochaine instruction à exécuter (c'est le compteur programme vu en architecture). On ne peut pas le manipuler directement dans un programme assembleur.

Enfin, le registre des indicateurs %eflags contient notamment les indicateurs Z, C, etc. communs à la majorité des processeurs.

Les adresses sont codées sur 32 bits : la taille maximale de la mémoire adressable est donc 2^{32} = 4 GiO.

Le bus de données est aussi sur 32 bits, mais on peut accéder à des données sur 32, 16 et 8 bits : on devra donc systématiquement préciser la taille des données manipulées via un suffixe de taille ajouté à la fin des instructions :

  • l (long) : pour les données sur 32 bits ;
  • w (word) : pour les données sur 16 bits ;
  • b (byte) : pour les données sur 8 bits.

Le Pentium est un processeur little-endian, ce qui signifie que dans un mot mémoire, les bits de poids faibles sont stockés en premier. Par exemple, si la valeur (sur 32 bits) 0x12345678 est stockée à l'adresse 0x1000, on trouve en mémoire :

Adresses 0x1000 0x1001 0x1002 0x1003
Valeurs 0x78 0x56 0x34 0x12

Instructions courantes

L'instruction de copie

mov src, dst copie une valeur d'un endroit à un autre :

  • d'un registre vers un autre registre : movl %eax, %ebx : copie le contenu du registre 32 bits %eax dans le registre %ebx ;
  • d'un registre vers la mémoire : movb %al, 0x1234 : copie le contenu du registre 8 bits %al dans la case mémoire 8 bits d'adresse 0x1234 ;
  • de la mémoire vers un registre : movw 0x1234, %bx : copie le contenu de case mémoire 16 bits d'adresse 0x1234 dans le registre %bx ;
  • une constante vers un registre : movb $45, %dh : copie la valeur sur 8 bits 45 dans le registre %dh ;
  • une constante vers la mémoire : movl $0x1234, 0x1234 : copie la valeur sur 32 bits 0x00001234 dans la case mémoire 32 bits d'adresse 0x1234.

Il n'est par contre pas possible de copier une valeur de la mémoire vers la mémoire.

Opérations arithmétiques

Il existe de nombreuses opérations arithmétiques, dont on détaille les plus courantes :

  • Addition : add, par exemple addw %ax, %bx calcule %bx := %bx + %ax ;
  • Soustraction : sub, par exemple subb $20, %al calcule %al := %al - 20 ;
  • Négation : neg, par exemple negl %eax calcule %eax := -%eax ;
  • Décalage à gauche : shl, par exemple shll $1, %eax décale la valeur sur 32 bits contenue dans le registre %eax d'un bit vers la gauche, en insérant un 0 à droite ;
  • Décalage arithmétique à droite : sar, par exemple sarl $12, %ebx décale la valeur sur 32 bits contenue dans le registre %ebx de 12 bits vers la droite, en propageant le bit de signe à gauche ;
  • Décalage logique à droite : shr, par exemple shrl $4, %ebx décale la valeur sur 32 bits contenue dans le registre %ebx de 4 bits vers la droite, en insérant des 0 à gauche ;
  • Conjonction logique : and, par exemple andw $0xFF00, %cx calcule un « et bit-à-bit » %cx := %cx et 0xFF00 ;
  • Disjonction logique inclusive : or, par exemple orb $0x0F, %al calcule %al := %al ou 0x0F ;
  • Disjonction logique exclusive : xor, par exemple xorl %eax, %eax calcule %eax := %eax ou-exclusif %eax ;
  • Négation logique : not, par exemple notl %ecx calcule l'opposé bit-à-bit de %ecx.

Comparaisons

On utilise les comparaisons avant un branchement conditionnel :

  • Comparaison arithmétique : cmp, par exemple cmpl $5, %eax compare %eax avec 5, en effectuant la soustraction %eax - 5 sans stocker le résultat ;
  • Comparaison logique : test, par exemple testb $0x01, %bl effectue un et bit-à-bit entre la constante 1 et %bl, sans stocker le résultat.

Les comparaisons ne stockent pas le résultat de l'opération effectuée, mais mettent à jour le registre des indicateurs %eflags, qui est utilisé par les branchements conditionnels.

Branchements

Le branchement le plus simple est le branchement inconditionnel : jmp destination.

Pour préciser la destination d'un branchement, on utilise une étiquette :

    movl $0, %eax
    jmp plus_loin
    movl $5, %edx
plus_loin:
    addl $10, %eax

Les branchements conditionnels se basent sur l'état d'un ou plusieurs indicateurs contenus dans le registre %eflags pour déterminer si le saut doit être effectué ou pas :

  • je etiq saute vers l'étiquette etiq ssi la comparaison a donné un résultat « égal » ;
  • jne etiq saute ssi le résultat était différent (not equal) ;

Les indicateurs à tester sont parfois différents selon si on travaille sur des entiers signés ou naturels. Pour les naturels, on utilisera :

  • ja etiq saute ssi le résultat de la comparaison était strictement supérieur (jump if above) ;
  • jb etiq saute ssi le résultat de la comparaison était strictement inférieur (jump if below) ;

Et pour les entiers signés :

  • jg etiq saute ssi le résultat de la comparaison était strictement supérieur (jump if greater) ;
  • jl etiq saute ssi le résultat de la comparaison était strictement inférieur (jump if less) ;

On peut composer les suffixes, et obtenir ainsi plusieurs mnémoniques différents pour la même instruction : jna (jump if not above) est strictement équivalent à jbe (jump if below or equal).

Le tableau ci-dessous résume les différents branchements conditionnels qu'on utilisera :

Branchements conditionnels
Comparaison Entiers naturels Entiers signés
> ja, jnbe jg, jnle
jae, jnb jge, jnl
< jb, jnae jl, jnge
jbe, jna jle, jng
= je, jz
jne, jnz

Adressages mémoire

Les modes d'adressage sont les différents mécanismes fournis par le processeur pour accéder à la mémoire. On ne détaille ici que ceux utiles dans le projet.

L'adressage immédiat

Ce n'est pas un accès mémoire à proprement parler, car il sert à mettre une constante dans un registre (la constante étant codée directement dans l'instruction).

Par exemple : movl $5, %eax charge la constante (sur 32 bits) 5 dans le registre %eax.

L'adressage indirect

Le principe de ce mode d'adressage est simplement de lire dans un registre l'adresse de la variable à accéder. Par exemple, si le registre %eax contient l'adresse d'une case mémoire 32 bits, on peut écrire movl $5, (%eax) pour copier la valeur 5 dans la case mémoire.

L'adressage indirect avec déplacement

Dans ce mode, on ajoute une constante entière quelconque à l'adresse contenue dans le registre.

Par exemple : movw -8(%ebx), %dx va chercher un entier sur 16 bits à l'adresse calculée en enlevant 8 au contenu de %ebx, et stocke cet entier dans %dx.

Le registre contenant l'adresse est souvent appelé « registre de base ».

Une utilisation possible est l'accès à des entiers dans un tableau. Par exemple, si %edx pointe sur l'adresse de base du tableau int t[10];, alors movl 12(%edx),%eax permet de mettre la valeur de t[3] dans le registre %eax. En effet, on rappelle que la taille d'un int est de 4 octets, donc le contenu de t[3] se trouve à l'adresse %edx+3*4.

Appels de fonctions

On doit respecter un certain nombre de conventions de gestion de la pile d'exécution lorsqu'on veut appeler des fonctions C depuis du code assembleur, ou l'inverse. On détaille celles qui nous seront utiles dans le cadre du projet, où on n'aura à écrire que des « fonctions » très particulières.

Les pointeurs de pile

Le Pentium comprend 2 registres dont le rôle est lié à la pile d'exécution : %esp et %ebp.

%esp est le pointeur de pile : il contient en permanence l'adresse de la dernière case occupée dans la pile d'exécution. On ne le manipule pas directement en général pour éviter de risquer de déséquilibrer la pile.

%ebp est le pointeur de base : il contient l'adresse de la base du contexte d'exécution de la fonction en cours d'exécution. Dans notre cas, il pointera en permanence sur la case dans laquelle on a sauvegardé le %ebp précédent. On se sert de %ebp pour accéder aux variables locales et aux paramètres de la fonction en cours d'exécution, via un adressage indirect avec déplacement (e.g. -4(%ebp), 8(%ebp), etc.).

Instructions d'appel et de retour

L'instruction call permet d'appeler une fonction : on l'utilise en précisant l'étiquette correspondant au début de la fonction à appeler, par exemple call pgcd. Cette instruction a le même comportement qu'un branchement inconditionnel (jmp) mais en plus, elle empile automatiquement l'adresse de retour dans la pile d'exécution. L'adresse de retour est simplement l'adresse de l'instruction suivant le call dans la fonction appelante.

L'instruction réciproque s'appelle ret : elle dépile l'adresse de retour et revient donc à l'instruction suivant le call. C'est l'équivalent de l'instruction return qu'on connait déjà en C.

Instructions de sauvegarde et restauration dans la pile

On utilise l'instruction push pour empiler une valeur, c'est à dire :

  • déplacer le pointeur de pile %esp qui pointe sur la dernière case occupée ;
  • copier la valeur dans la case mémoire pointée maintenant par %esp.

Par exemple, pushl %eax est équivalent à :

    subl $4, %esp
    movl %eax, (%esp)

L'instruction réciproque s'appelle pop et elle permet de dépiler la valeur en sommet de pile. Par exemple, popl %eax est équivalent à :

    movl (%esp), %eax
    addl $4, %esp

Gestion des registres

Le compilateur GCC classe les registres généraux du processeur dans 2 catégories :

  • les registres scratch sont des registres « de calcul » qui peuvent être utilisés librement dans les fonctions en assembleur ;
  • les registres non-scratch sont des registres « précieux » dans lesquels le compilateur peut stocker des valeurs importantes.

Sur Pentium :

  • les registres scratch sont %eax, %edx, %ecx et %eflags;
  • les registres non-scratch sont %ebx, %esi, %edi, %ebp et %esp.

Il s'agit d'une convention arbitraire : on doit cependant la prendre en compte lorsqu'on veut interagir avec du code généré par GCC, ce qui est notre cas dans le projet.

Cette distinction est importante lorsqu'on appelle une fonction C depuis du code assembleur :

  • la fonction en assembleur ne doit pas laisser de valeurs importantes dans les registres scratch sans les sauvegarder avant d'appeler la fonction C : en effet, l'appel à la fonction C pourrait parfaitement détruire le contenu de ces registres, car GCC considère qu'il peut s'en servir comme bon lui semble ;
  • la fonction en assembleur peut par contre laisser des valeurs importantes dans les registres non-scratch car on est sur qu'ils ne seront pas modifiés par l'appel à la fonction C.

Conventions pour le cadre de pile

Lorsqu'on écrit un programme complet en assembleur, en mélangeant des fonctions C et assembleur qui s'appellent les unes les autres, on doit respecter des conventions de gestion de la pile. Dans le cadre de ce projet on n'aura pas besoin d'écrire de fonction entièrement en assembleur, mais on détaille tout de même les conventions classiques sous Unix pour information.

La pile est la zone mémoire contenant le contexte d'exécution des fonctions du programme. A chaque fois que le programme commence l'exécution d'une fonction, on doit mettre en place un cadre de pile (stack frame en anglais) qui contiendra notamment les variables locales de la fonction, les paramètres passés à la fonction, etc. Un cadre de pile a la structure suivante :

3MM1LDB pile32.png

Dans ce schéma, les adresses sont croissantes vers le bas mais les valeurs sont empilées en haut car il s'agit d'une pile au sens algorithmique du terme. Les différentes zones sont détaillées ci-dessous :

  • les paramètres sont ceux passés à la fonction en cours d'exécution, ils sont mis par l'appelant de la fonction (par des instructions push) ;
  • l'adresse de retour est l'adresse de l'instruction suivant l'appel de la fonction en cours d'exécution dans la fonction appelante (i.e. c'est là qu'on revient quand on exécute return à la fin de la fonction), cette adresse est mise dans la pile automatiquement lors de l'appel de la fonction avec l'instruction call ;
  • le « %ebp précédent » est la sauvegarde du pointeur de base de la fonction appelante, il est sauvegardé à l'entrée de la fonction par l'instruction pushl %ebp, puis %ebp est mis à jour tout de suite après avec l'instruction movl %esp, %ebp ;
  • les variables locales de la fonction en cours d'exécution, si elles existent, sont localisées dans son cadre de pile, à des adresses fixes par rapport à %ebp, on peut réserver l'espace nécessaire en déplaçant le pointeur de pile %esp de la taille à réserver en utilisant l'instruction subl après avoir sauvegardé l'ancien %ebp ;
  • on peut éventuellement réserver de la place dans le cadre de pile pour sauvegarder des registres non scratch si besoin.

Exemple de code en assembleur

Le fichier contenant le code assembleur doit avoir l'extension .S. Voilà un exemple de code :

// directive .text indiquant que ce qui est suit 
//est dans la section réservée pour le code 
    .text 
// directive .globl indiquant que l'étiquette f est publiée 
//dans le fichier objet généré pour l'édition de lien 
    . globl f
//Etiquette (doit être en début de ligne et suivi de : )  
f: 	
//code assembleur 
//doit être précédé d'espaces ou de tabulations 
//pour être considéré comme tel
    movl 4(%esp),%eax 
    addl %eax,%eax
    ret

Documentations officielles Intel

Vous trouverez ci-dessous les documentations officiels d'Intel : bien entendu, elles dépassent très largement le périmètre de ce cours et vous n'avez pas besoin de les lire en détail pour réussir le projet. Attention : elles font chacune plusieurs centaines de pages, ne les imprimez pas !!.