Assembleur MIPS32 / Travaux Pratiques
Travaux Pratiques : Début sur le Simulateur MARS
Préambule
Ce premier TP a pour but de vous familiariser avec l'outil MARS. Le simulateur MARS modélise un petit système composé d'un processeur MIPS32 et d'une mémoire, sans faire d'hypothèse sur l'architecture interne du processeur, qui est considéré comme une boîte noire, capable d'exécuter
séquentiellement (instruction par instruction) un code binaire stocké en mémoire.
MARS fait l'hypothèse qu'un programme utilise trois segments en mémoire :
- Le segment text, qui contient le code binaire exécutable du programme (c'est-à-dire les instructions).
- Le segment data, qui contient les données globales du programme (dont les valeurs peuvent être initialisées).
- Le segment stack, qui contient la pile d'exécution du programme (les variables locales aux fonctions).
L'interface graphique de MARS vous permet d'examiner les valeurs contenues dans chacun de ces trois segments.
Le logiciel MARS fournit en pratique trois services distincts :
- Editeur: il contient un éditeur de texte permettant de saisir et de sauvegarder sur disque un programme écrit en langage d'assemblage.
- Assembleur : à partir d'un fichier source en langage d'assemblage, il réalise l'assemblage, c'est-à-dire la génération du code binaire. Il effectue également le chargement en mémoire de ce code binaire dans les deux segments
textetdata. - Simulateur : après chargement du code binaire en mémoire, il simule l'exécution en mode pas-à-pas, en vous permettant de visualiser comment l'exécution de chaque instruction modifie l'état du système (c'est-à-dire les valeurs stockées dans la mémoire externe et dans les registres internes du processeur).
Le langage d'assemblage est décrit dans le document MIPS32 : Langage d'assemblage. MARS prend en entrée un fichier en langage d'assemblage possédant l'extension ".s". Pour MARS, la structure d'un fichier nom.s doit être la suivante :
.data # début de la section des données globales label1: .word 0x3F, 0x45 label2: .asciiz "b" .text # début de la section instructions (code) en mémoire .globl main # définition du point d'entrée du programme main: la $5, label1 # première instruction du programme lw $8, 4($5) ... ori $2, $0, 10 # appel système numéro 10 pour terminer le programme (exit (0)) syscall
1. Addition de deux valeurs stockées en mémoire
- Saisir, soit sous votre éditeur de texte favori, soit en utilisant l'éditeur intégré dans l'outil
MARS, le programme de calcul de la somme de deux nombres rangés en mémoire et sauvez le fichier sous le nomsum.s. - Définissez les valeurs des variables d'environnement UNIX utilisées par le logiciel
MARS, en suivant les recommandations du manuel de configuration?. - Vous pouvez maintenant lancer le simulateur
MARS, en tapant la commande :
$ mars &
Si le terminal vous répond qu'il ne trouve pas la commande mars, c'est que l'environnement du terminal n'a pas été correctement configuré. Si le problème persiste, demandez de l'aide à votre encadrant.
- Lancez l'assemblage et le chargement du programme
sum.s. - Prenez le temps de vérifier que le segment
textde la mémoire contient bien le code binaire correspondant au programme que vous avez écrit.XSPIMrange ce segment à l'adresse 0x00400000. À quoi correspond la première instruction du segmenttext? - Prenez le temps de vérifier que le segment
datade la mémoire contient bien les variables globales que vous avez définies et initialisées. À quelles adresses sont rangées les trois variablesvar1,var2etvar3? - Lancez l'exécution du programme en mode pas à pas (commande
step), en analysant pour chaque instruction quels registres du processeur et quelles cases de la mémoire externe ont été modifiées.
2. Somme des dix premiers entiers
- Saisir le programme de calcul de la somme des dix premiers nombres entiers. Sauvez le sous le nom
sum10.s. - Lancez le simulateur
MARS, lancez l'assemblage et le chargement du programmesum10.s, vérifiez que les segmentstextetdatasont sont correctement initialisés, puis lancez l'exécution et vérifiez le résultat obtenu.
Fonctions imbriquées et récursives
Préambule
Nous allons étudier comment les conventions définies pour les appels de fonctions permettent à une fonction d'en appeler une (ou plusieurs) autre(s), voire de s'appeler elle-même dans le cas des fonctions récursives.
1. Moyenne de N entiers stockés dans un tableau
On souhaite écrire en assembleur MIPS32 le programme C qui calcule la valeur moyenne de tous les entiers (non négatifs) stockés dans un tableau de dimension quelconque. Par convention, et pour pouvoir réutiliser la fonction sumtab(), on suppose que le dernier élément du tableau possède une valeur négative. Ce dernier élément n'est pas pris en compte dans le calcul de la moyenne.
Le programme principal appelle la fonction arimean() qui a pour seul argument un pointeur sur un tableau d'entiers. Elle renvoie la valeur de la moyenne arithmétique des éléments du tableau (tronquée à la valeur entière inférieure). La fonction arimean() appelle elle-même la fonction sizetab() qui prend pour seul argument le pointeur sur le tableau, et renvoie le nombre d'éléments non négatifs contenus dans le tableau. Elle appelle ensuite la fonction sumtab(), qui calcule la somme de tous les entiers non négatifs contenus dans le tableau. Elle effectue la division entière et renvoie le quotient au programme appelant.
L'ensemble du programme s'écrit en C de la façon suivante :
#include <stdio.h> /* variables globales initialisées */ int tab[] = {23, 7, 12, 513, -1} ; /* programme principal */ int main(void) { int x = arimean(tab); printf(" %h ", x); return 0; } /* cette fonction renvoie la moyenne arithmétique des éléments d'un tableau */ int arimean(int t[]) { int n = sizetab(t); int x = sumtab(t); return (x / n); } /* cette fonction renvoie le nombre d'éléments d'un tableau */ int sizetab(int t[]) { int index = 0; while (t[index] >= 0) { index++; } return index; } /* cette fonction renvoie la somme des éléments d'un tableau */ int sumtab(int t[]) { int accu = 0; int index = 0; while(t[index] >= 0) { accu = accu + t[index]; index++; } return accu; }
- Écrire le programme assembleur correspondant à ce programme C, et exécutez-le à l'aide du simulateur
MARS. Commencer, pour chaque fonction, à déterminer les valeursnr,nvet 'na`. Vous pouvez aussi dessiner l'état de la pile à l'entrée dans une fonction et après son prologue.
2. Fonctions récursives : calcul de la factorielle
On souhaite écrire en langage d'assemblage MIPS32 le programme de calcul de la factorielle d'un nombre entier positif. On rappelle que fact(n) se note n!, et vaut :
n! = n * (n-1) * (n-2) * .. * 2 * 1
On peut également définir la fonction factorielle par la relation de récursion suivante :
- n! = (n-1)! * n (si n > 0)
- 0! = 1
En utilisant cette définition récursive, on peut écrire un programme C qui calcule la factorielle d'un entier quelconque, en utilisant la fonction fact(n), définie ci-dessous en langage C :
/* fonction fact */ int fact(int n) { if (n == 0) { /* cas terminal */ return 1; } else { /* récursion */ return fact(n - 1) * n; } } /* programme principal */ int main(void) { printf("%h\n", fact(5)); return 0; }
- Écrire le code assembleur du programme principal et de la fonction
fact(). La fonctionfact()possède une seule variable locale et utilise deux registres. Exécutez ce programme sousMARS. Représenter graphiquement les valeurs contenues dans la pile après une exécution du programme lorsque la valeur de n vaut'3'. - Que se passe-t-il lorsque la valeur du paramètre
'n'est positive mais très grande (égale à 1000 par exemple) ? - Que se passe-t-il lorsque la valeur du paramètre
'n'est négative ? - Que peut-on faire pour résoudre ces difficultés ?
