wiki:SujetTD2

ALMO TD n°2 - Utilisation de la pile : Appels de fonctions

Préambule

Ce TD a pour but de vous familiariser avec les trois utilisations principales de la pile d'exécution en mémoire :

  • stockage en mémoire des variables locales aux fonctions
  • sauvegarde / restauration des registres.
  • passage des arguments de la fonction appelante vers la fonction appelée.

Les calculs réalisés sont volontairement très simples, car on s'intéresse principalement aux conventions d'utilisation de la pile lors des appels de fonctions. Pour résoudre les exercices de ce TD vous aurez besoin du fascicule Langage d'assemblage MIPS (en particulier, la section n°8 : "Conventions pour les appels de fonctions").

1. Fonctions sans argument

On souhaite écrire en assembleur MIPS32 le programme C ci-dessous. Le programme principal main() appelle la fonction sum10() qui calcule la somme des 10 premiers entiers, puis il effectue un appel système printf() pour afficher en hexadécimal le résultat retourné par la fonction sum10(), et se termine par l'appel système exit().

/* programme principal */
void main()
{
    printf("%d", sum10());
    exit(0);
}

/* fonction sum10() */
int sum10()
{
    int accu = 0;
    int index;
    for (index = 10; index > 0; index--)
    {
            accu = accu + index;
    }
    return accu;
}

1.1 Ecriture de la fonction sum10()

On demande dans un premier temps d'écrire en assembleur MIPS32 le code de la fonction sum10(). On supposera évidemment que la fonction sum10() ne sait pas par qui elle est appelée, pour permettre que cette fonction puisse être utilisée dans différents contextes.

1.1.1 Corps de la fonction

Le corps de la fonction est le code qui effectue le calcul. On utilise deux registres de travail pour stocker les deux variables accu et index : par exemple, $16 contiendra l'index de boucle, et $17 sera le registre accumulateur contenant la somme partielle. Il faut commencer par initialiser les deux registres, puis écrire la boucle d'accumulation, et terminer en rangeant le résultat dans le registre $2 pour respecter la convention de retour du résultat.

1.1.2 Prologue

Pour respecter les conventions d'utilisation de la pile, le corps de la fonction doit être précédé par un "prologue", qui déplace le pointeur de pile $29 pour réserver le contexte d’exécution de la fonction, cela sert à :

  • sauvegarder dans la pile les valeurs contenues dans les registres persistants utilisés par la fonction sum10(), y compris le registre $31 ;
  • réserver la place pour les variables locales de la fonction sum10() ;
  • réserver la place maximale pour les arguments de toutes les fonctions appelées par la fonction sum10().

La fonction sum10() utilise 2 registres persistants, on a donc nr = 3 en comptant $31. La fonction sum10()a 2 variables locales. Pour des raisons de simplicité et de régularité, on réserve systématiquement cette zone de variables locales, même si les variables correspondantes sont stockées dans des registres, on a donc nv = 2. Enfin, la fonction sum10()est terminale, elle n'appelle pas de fonction, on a donc na = 0.

  • Il faut donc décrémenter le pointeur de pile de (nr + nv + na) mots, c'est-à-dire de 5 mots, ou encore 20 octets.

1.1.3 Épilogue

Pour respecter les conventions d'utilisation de la pile, le corps de la fonction doit être suivi par un "épilogue" qui restaure les valeurs des registres qui ont été sauvegardées dans la pile, re-déplace le pointeur de pile pour lui redonner la valeur qu'il avait lors de l'entrée dans la fonction, et se branche à l'adresse de retour sauvegardée dans le registre $31.

1.2 Écriture du programme main()

Écrire en assembleur MIPS32 le code du programme main(). Ce programme doit en principe contenir un appel à la fonction sum10(), suivi d'un premier appel système pour afficher le résultat sur la console, puis d'un deuxième appel système pour terminer le programme. Remarquer que ce programme n'utilise pas de variable, ce qui est un cas très peu fréquent...

1.3 Représentation graphique de la pile

Représenter graphiquement l'état (contenu) de la pile au moment où le processeur exécute pour la première fois le code de la boucle située dans la fonction sum10(). On rappelle que, par convention, le pointeur de pile pointe sur le sommet de la pile, c'est à dire sur la dernière case occupée de la pile au moment où on entre dans une fonction. On rappelle également que, par convention, la pile s'étend vers les adresses décroissantes. Il faut donc décrémenter le pointeur de pile pour empiler de nouvelles valeurs.

2. Fonctions avec arguments

On souhaite maintenant écrire en assembleur MIPS32 le programme C qui calcule et affiche la somme de tous les entiers plus grands que p et plus petits ou égaux à q (en supposant p < q). Comme dans l'exercice précédent, le programme principal appelle la fonction sumpq(), puis effectue un appel système printf() pour afficher en hexadécimal le résultat retourné par la fonction et se termine par l'appel système exit(). Les valeurs des deux paramètres p et q sont des variables globales déclarées et initialisées en dehors du programme principal.

/* variables globales initialisées */
int p = 5;
int q = 12;

/* programme principal */
void main()
{
    printf("%d", sumpq(p, q));
    exit(0);
}

/* fonction sumpq */
int sumpq(int p, int q)
{
    int accu = 0;
    int index;
    for (index = q; index > p; index--)
    {
        accu = accu + index;
    }
    return accu;
}

2.1 Écriture de la fonction sumpq()

Modifier le code de la fonction de l'exercice n°1 pour prendre en compte les paramètres p et q. Il faut modifier les trois parties (le corps de la fonction, le prologue, et l'épilogue).

2.1.1 Corps de la fonction

La fonction utilise maintenant trois registres. Le registre $16 est utilisé pour stocker la valeur du premier argument p. Le registre $17 est initialisé avec la valeur du deuxième argument q et servira d'index de boucle. Le registre $18 est utilisé comme accumulateur. Le registre$8 pourra être utilisé pour le calcul de la condition du for.

On suppose que le prologue va ranger les arguments pet q reçus dans les registres $4 et $5 dans la pile à la place que la fonction main() leur a réservée. Les premières instructions du corps de la fonction va consister à lire ces arguments depuis la pile, avec une instruction comme lw $16, X ($29) où X est un déplacement positif qui ne peut, en principe, être connu que lorsque l'on connaît précisément le nombre de cases allouées dans la pile par le prologue. Dans notre cas simple, on peut connaître ce déplacement en déterminant les valeurs nr, nvet na.

Quelles seront les valeurs de nr, nvet na ? En déduire le déplacement du pointeur de pile et la place réservée pour les deux arguments pet q de sumpq() relativement au pointeur $29.

Écrire maintenant le corps de la fonction sumpq().

2.1.2 Prologue

La fonction utilise 3 registres persistants et il y a toujours 2 variables locales dans la pile. Écrire le prologue de la fonction sumpq(), vous devez sauver les arguments p et q dans la pile à la place que la fonctionmain()leur a réservée.

  • Dessiner l'état de la pile après l'exécution du prologue
  • Puis écrire le prologue:

2.1.3 Épilogue

Il y a maintenant 3 registres à restaurer (en plus du registre $31).

2.2 Écriture du programme main()

On suppose que le registre $29 contenant le pointeur de pile a été initialisé lors de l'exécution de la séquence de reset. Écrire le code de la fonction main en précisant les valeurs de nr, nv et na et en distinguant le prologue, le corps et l'épilogue de la fonction main().

Last modified 5 years ago Last modified on Aug 24, 2019, 9:37:24 AM