CEP CTD Adressage ia32

De Ensiwiki
Aller à : navigation, rechercher

Laptop.png  Première Année  CDROM.png  Informatique 

Pendant cette séance, on va travailler sur les différentes modes d'adressage du processeur Pentium, c'est à dire les différents mécanismes offerts par le processeur pour accéder à des données en mémoire.

Le processeur Pentium étant un processeur CISC, il existe beaucoup de façon d'accéder à la mémoire. On ne verra que les plus couramment utilisées ici. On notera que les noms des différents modes d'adressage ne sont pas standardisés et peuvent donc varier d'un auteur à l'autre.

Modes d'adressage utilisés

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 comme vous l'avez vu en cours d'Architecture au premier semestre).

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

L'adressage direct

C'est le mode le plus simple, car il consiste à donner directement l'adresse mémoire de la valeur qu'on souhaite accéder.

Par exemple : movb 0x12345678, %al va chercher la valeur de l'octet à l'adresse 0x12345678 dans la mémoire et stocke cette valeur dans le registre %al.

En pratique, il est très rare qu'on donne une adresse explicitement, vu qu'on ne sait pas où le processus sera placé dans la mémoire. On utilise beaucoup plus souvent une étiquette pour représenter symboliquement l'adresse de la donnée à utiliser.

Par exemple le code suivant va aussi chercher la valeur de l'octet x pour le stocker dans %al :

    .text
    ...
    movb x, %al
    ...

    .data
x:  .byte 0x12

Attention au piège classique illustré dans le code suivant :

    .text
    ...
    movl y, %eax
    ....
    movl $y, %eax
    ...

    .data
y:  .long 12345678

Le premier mov copie la valeur de la variable y dans %eax. Par contre, le deuxième copie l'adresse de la variable y dans %eax. On rappelle que le signe $ désigne une constante en assembleur Pentium (ici, la constante est l'adresse de la variable y).

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, on peut s'en servir comme dans l'exemple ci-dessous :

/*
    int x;
    int *ptr;

    int main(void)
    {
        ptr = &x;
        *ptr = 5;
        return 0;
    }
*/

    .text
    .globl main
main:
    enter $0, $0
    //ptr = &x;
    movl $x, ptr
    // *ptr = 5;
    movl ptr, %eax
    movl $5, (%eax)
    leave
    // return 0;
    movl $0, %eax
    ret

.comm x, 4
.comm ptr, 4

Attention : la base doit toujours être un registre. Si vous écrivez quelque-chose du genre movl (ptr), %eax dans l'exemple ci-dessus, l'assembleur génèrera en fait l'instruction movl ptr, %eax, ce qui ne correspond pas à ce qu'on veut faire.

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

L'adressage indirect avec index et déplacement

Dans ce mode, l'adresse de la variable est répartie entre deux registres et le déplacement. On fait la somme des contenus des deux registres et de la constante pour calculer l'adresse de la variable.

Par exemple : movw $20000, 7(%ebx, %ecx) ou movl -3(%ebx, %ecx), %eax.

Si le déplacement est 0, on peut l'omettre et écrire directement par exemple : movl (%ebx, %ecx), %eax.

Le premier registre est souvent appelé « registre de base » et le deuxième « registre d'index ».

L'adressage indirect avec index typé et déplacement

Dans ce mode, l'adresse de la variable est calculée selon la formule : adresse = base + index * type + deplacement où :

  • la base est la valeur contenue dans le premier registre ;
  • l'index est la valeur contenue dans le deuxième registre ;
  • le type est une constante entière valant forcément 1, 2, 4 ou 8 ;
  • le déplacement est une constante entière quelconque.

Par exemple : movl %edx, -5(%ebx, %ecx, 4) copie la valeur contenue dans le registre %edx dans la case mémoire (de 32 bits) dont l'adresse est calculée en faisant la somme entre le contenu de %ebx, le contenu de %ecx multiplié par 4 et le déplacement qui vaut ici -5.

Ce mode d'adressage est très utile pour accéder à des tableaux : le registre de base contient l'adresse du début du tableau, le type représente la taille d'un élément du tableau et le registre d'index contient l'indice de la case à accéder.

Si le déplacement vaut 0, on peut l'omettre et écrire par exemple : movw (%ebx, %ecx, 2), %ax.

Il existe une forme dégénérée de ce mode dans laquelle on ne donne pas de registre de base (ce qui revient à considérer que la base vaut 0). Ce mode est souvent utilisé pour accéder à des tableaux définis comme des variables globales, en utilisant une étiquette à la place du déplacement.

Par exemple : movl $100000, tab(, %ecx, 4) copie la constante 100000 à l'adresse calculée en faisant la somme de l'adresse représentée par l'étiquette tab et le contenu de %ecx multiplié par 4. Ca a un sens si tab représente un tableau d'entiers (sur 4 octets chacun) dont on veut accéder à la « ecx^{ieme} » case.

Exercices

Petit rappel préliminaire

Supposons qu'on a en C le code ci-dessous :

#include <stdio.h>

char *x = "abc";
char y[] = "abc";

int main(void)
{
    puts(x);
    puts(y);
    x++;
    puts(x);
//     y++;
    puts(y);
    return 0;
}

Quelle est la différence entre x et y ? Pourquoi ne peut-on pas compiler si on décommente // y++; ? Ecrivez la zone data correspondant à la définition de x et y.

Exercice 1

Soit le programme assembleur ci-dessous :

    .globl main
main:
    enter $0, $0
    //
    movl $4, i
bcl:
    //
    cmpl $0, i
    jl fin_bcl
    //
    movl x, %eax
    movl i, %ecx
    movl %eax, tab(, %ecx, 4)
    //
    subl $1, x
    //
    subl $1, i
    jmp bcl
fin_bcl:
    leave
    //
    movl $0, %eax
    ret

    .data
x:  .long 15

.comm tab, 5 * 4
.comm i, 4

Essayez de deviner ce que fait ce programme (en déroulant son exécution dans GDB) et écrivez un programme C équivalent. Les commentaires // délimitent les lignes de C correspondant aux instructions assembleur.

On rappelle que vous pouvez afficher le contenu des registres et variables intéressantes avec les commandes suivantes :

  • display $eax pour afficher le contenu du registres %eax ;
  • display x pour afficher le contenu de la variable globale x ;
  • display /5xwd (int[])tab pour afficher en décimal les 5 mots de 32 bits stockés à partir de l'adresse de la variable globale tab.

Que pouvez-vous dire concernant le type des variables globales x, i et tab ?

Exercice 2

Même questions avec le code ci-dessous :

    .globl main
main:
    enter $0, $0
    //
    movl $chaine, %eax
    movl %eax, ptr
bcl:
    //
    movl ptr, %eax
    cmpb $0, (%eax)
    je fin_bcl
    movb car, %dl
    cmpb %dl, (%eax)
    je fin_bcl
    //
    addl $1, ptr
    jmp bcl
fin_bcl:
    leave
    //
    movl $0, %eax
    ret

    .data
car:    .ascii "c"
chaine: .asciz "abcdcba"

.comm ptr, 4

Dans GDB, vous pouvez afficher :

  • le caractère avec la commande p /c car ;
  • la chaine avec la commande x /8c (char[])chaine ;
  • le pointeur en hexadécimal avec la commande display /x (char*)ptr ;
  • le registre %eax en hexadécimal avec la commande display /x $eax ;
  • et le caractère contenu dans %dl avec la commande display /c $edx.

Manipulation de chaînes de caractères

Dans cet exercice, on va écrire des petits programmes de manipulation de chaînes de caractères.

Soit le squelette de programme assembleur ci-dessous, à copier-coller dans un fichier chaines.s :

    .globl main
main:
    // debut de la fonction
    enter $0, $0

// completer ici avec le calcul de la longueur de la chaine

// completer ici avec l'inversion de la chaine

    // fin de la fonction
    leave
    // return 0
    movl $0, %eax
    ret

    .data
// char chaine[] = "azerty";
chaine:    .asciz "azerty"
senti:     .ascii "X"

Note : le caractère à l'étiquette senti ne fait pas partie du programme, il s'agit juste d'une aide pour vous éviter une erreur fréquente...

Calcul de la longueur de la chaine

Compléter le squelette fourni pour calculer dans une variable la taille de la chaine. On rappelle qu’une chaine de caractères est terminée par le caractère ’\0’ (c’est à dire l’octet zéro) et qu’on ne compte pas ce caractère de fin dans la taille de la chaine. Attention, les caractères sont codés sur 8 bits : on ne manipule pas ici des entiers.

Dans cet exercice, vous utiliserez le mode d'adressage indirect avec index : par exemple, (%edx, %ecx) pour accéder aux caractères de la chaine.

En C, le code demandé s’écrirait par exemple :

char chaine[] = "azerty";
unsigned taille;
...
int main(void)
{
    for (taille = 0; chaine[taille] != 0; taille++);
    ...
}

Tester ce programme avec gdb sur les chaines "azerty", "aze", "a" et "".

Inversion de chaine

Ajouter au squelette fourni le code permettant d’inverser la chaine de caractère (e.g. "azerty" donne "ytreza"). On utilisera la taille de la chaine calculée précédemment pour écrire ce code. Le code C correspondant pourrait être par exemple :

int dep;
char *ptr;
char tmp;
...
int main(void)
{
    ...
    dep = taille - 1;
    ptr = chaine;
    while (dep > 0) {
        tmp = *ptr;
        *ptr = ptr[dep];
        ptr[dep] = tmp;
        dep = dep - 2;
        ptr++;
    }
    ...
}

Mettre au point le programme en utilisant GDB et en affichant la chaine résultat à la fin, sur les chaines "azerty", "aze", "a" et "". On rappelle que si par exemple %al contient un caractère, on peut facilement afficher son contenu grâce à la commande display /c $eax. Pour afficher la chaine au fur et a mesure de son inversion, le plus simple est d'utiliser la commande display /7bc (char[])chaine (si la chaine contient 7 caractères).

On utilisera l'adressage indirect pour cette question, avec ou sans registre d'index selon les cas.

On rappelle que si on veut effectuer une traduction systématique du code C, on doit traiter chaque ligne séparément l'une des autres. Par exemple, si le code C était :

    ...
    x = y;
    z = y;
    ..

alors on devrait écrire en assembleur quelque-chose du genre :

    ...
    // x = y;
    movl y, %eax
    movl %eax, x
    // z = y;
    movl y, %eax
    movl %eax, z
    ...

même s'il peut paraitre inutile de recopier y dans %eax la deuxième fois vu qu'on sait qu'il y est déjà (mais un compilateur non-optimisant ne le saurait pas lui).

Manipulation de tableaux d'entiers

On va maintenant travailler sur des tableaux non-vides d’entiers strictement positifs. On part du squelette de code ci-dessous à sauvegarder dans un fichier tableaux.s :

    .globl main
main:
    // debut de la fonction
    enter $0, $0

// completer avec la recherche du maximum

    // fin de la fonction
    leave
    // return 0
    movl $0, %eax
    ret

    .data
// unsigned tab[] = {1, 4, 7, 5, 9, 1, 3, 2, 4, 8};
tab:    .long 1, 4, 7, 5, 9, 1, 3, 2, 4, 8
// unsigned taille = 10;
taille: .long 10

Recherche du maximum

Écrire le code assembleur cherchant l'indice de l’entier maximum du tableau tab.

Le code C correspondant pourrait être :

unsigned ix_max;
unsigned i;
unsigned tab[] = {1, 4, 7, 5, 9, 1, 3, 2, 4, 8};
unsigned taille = 10;

int main(void)
{
    for (ix_max = 0, i = 1; i < taille; i++) {
        if (tab[i] > tab[ix_max]) {
            ix_max = i;
        }
    }
    return 0;
}

On utilisera dans cette question l’adressage indirect avec index typé et déplacement : par exemple, tab(, %edx, 4) pour accéder à un élément du tableau. On demande d’écrire le code de façon naïve, en accédant autant de fois que nécessaire à la case courante du tableau. Tester le code avec gdb, et vérifier aussi qu’il fonctionne avec un tableau de taille 1 et 2.

Tri par recherche du maximum

Reprendre le même squelette initial pour cette fois écrire un programme triant le tableau par ordre décroissant selon l'algorithme de recherche du maximum, c'est à dire en C :

unsigned i;
unsigned j;
unsigned ix_max;
unsigned tmp;
unsigned tab[] = {1, 4, 7, 5, 9, 1, 3, 2, 4, 8};
unsigned taille = 10;

int main(void)
{
    for (i = 0; i < taille - 1; i++) {
        for (ix_max = i, j = i + 1; j < taille; j++) {
            if (tab[j] > tab[ix_max]) {
                ix_max = j;
            }
        }
        tmp = tab[i];
        tab[i] = tab[ix_max];
        tab[ix_max] = tmp;
    }
    return 0;
}

Si vous avez fini en avance

Dans tous les exercices précédents, on a traduit littéralement chaque ligne de C séparément des autres, comme le ferait un compilateur non-optimisant. Reprenez maintenant ces exercices en cherchant à optimiser le code en minimisant les accès à la mémoire (donc en stockant des variables directement dans des registres).