Simulation d'un programme avec mips-gdb
Sommaire
Préliminaires
Le reste de cette page suppose que les outils mips-gcc
, mips-as
, mips-ld
et mips-gdb
sont installés correctement sur votre machine.
Si vous voulez essayer sur votre machine personnelle, la page Installation des outils cross-compilés pour MIPS devrait vous aider. En ayant installé les binutils
et mips-gdb
, vous pourrez suivre la première moitié de cette page. Le reste vous demandera mips-gcc
Programme d'exemple
Nous commencerons avec un programme assembleur très simple :
_start:
.global _start
li $2,21
li $3,21
addu $4,$3,$2 # calcule 42 !
nop
nop
nop
Une particularité de ce programme est qu'il n'a besoin d'aucune bibliothèque, même pas la libc, ni de pile pour fonctionner.
Il définit lui-même le symbole _start
, qui est par convention le point d'entrée de l'exécutable (en général, il fait des initialisations et lance la fonction main
écrite par l'utilisateur. Ici, ce n'est même pas une vraie fonction).
Recopiez le programme dans un fichier, simple.s
.
Création d'un exécutable avec les outils existants
Assemblage
On va maintenant l'assembler. On pourrait utiliser l'assembleur du projet, mais pour vous permettre d'expérimenter avant d'avoir terminé le projet, voici la commande avec l'assembleur mips-as
:
mips-as simple.s -o simple.o
Édition de liens
Il reste à faire l'édition de liens. Pour une raison obscure, l'adresse de chargement de la section .text
par défaut ne convient pas à GDB. Il faut donc lui donner une adresse convenable avec l'option -Ttext
:
mips-ld simple.o -Ttext=0xA0020000 -o simple
pour créer l'exécutable simple
.

-
mips-ld
a fixé l'adresse de chargement de la section.text
(0xA0020000) et décidé des adresses de chargement des autres sections. - Il a relogé chaque section à ces adresses, calculé l'adresse finale de chaque symbole
- Il a marqué le nouveau symbole
_start
(qui doit apparaitre comme symbole global dans le fichier source) comme étant le point de départ de l'exécution du programme.
Exécution pas à pas avec mips-gdb
GDB contient un simulateur qui peut interpreter le code MIPS même si on ne dispose pas d'un processeur MIPS.
Sur notre exemple, on n'a ni entrée/sortie, ni valeur de retour pour la fonction main
, donc la seule manière de voir si le programme s'exécute bien est d'exécuter le programme pas-à-pas. Au prompt de gdb, on lance le simulateur avec target sim
, et on charge le programme avec load
:
$ mips-gdb simple GNU gdb 6.8 Copyright (C) 2008 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "--host=i686-pc-linux-gnu --target=mips-elf"... (no debugging symbols found) (gdb) target sim Connected to the simulator. (gdb) load Loading section .text, size 0x18 vma 0xa0020000 Start address 0xa0020000 Transfer rate: 192 bits in <1 sec.
On peut maintenant positionner un point d'arrêt sur la fonction _start
, et démarrer l'exécution du programme :
(gdb) b *0xa0020000 Breakpoint 1 at 0xa0020000 (gdb) r Starting program: /perms/moy/prive/Exemples/exemplesPhelma/simple Breakpoint 1, 0xa0020000 in _start ()
Attention, si c'est votre assembleur que vous avez utilisé, vous n'avez probablement pas la fameuse option -g
, donc pas de retour au source simple. Les deux principales conséquences :
- Il faut utiliser
nexti
oustepi
et nonnext
etstep
(on peut abréger enni
etsi
). - Pour savoir quelle instruction on exécute, il faut l'afficher explicitement avec
display /i $pc
Par exemple :
(gdb) b *0xa0020000 Breakpoint 1 at 0xa0020000 (gdb) display /i $pc (gdb) r Starting program: /perms/moy/prive/Exemples/exemplesPhelma/simple Breakpoint 1, 0xa0020000 in _start () 1: x/i $pc 0xa0020000 <_start>: li v0,21 (gdb) ni 0xa0020004 in _start () 1: x/i $pc 0xa0020004 <_start+4>: li v1,21 (gdb) ni 0xa0020008 in _start () 1: x/i $pc 0xa0020008 <_start+8>: addu a0,v1,v0 (gdb) info reg zero at v0 v1 a0 a1 a2 a3 R0 00000000 00000000 00000015 00000015 00000000 00000000 00000000 00000000 t0 t1 t2 t3 t4 t5 t6 t7 R8 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 s0 s1 s2 s3 s4 s5 s6 s7 R16 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 t8 t9 k0 k1 gp sp s8 ra R24 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 sr lo hi bad cause pc 00400004 00000000 00000000 00000000 00000000 a0020008 fsr fir 00000000 00000000 (gdb) ni 0xa002000c in _start () 1: x/i $pc 0xa002000c <_start+12>: nop (gdb) info reg zero at v0 v1 a0 a1 a2 a3 R0 00000000 00000000 00000015 00000015 0000002a 00000000 00000000 00000000 t0 t1 t2 t3 t4 t5 t6 t7 R8 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 s0 s1 s2 s3 s4 s5 s6 s7 R16 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 t8 t9 k0 k1 gp sp s8 ra R24 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 sr lo hi bad cause pc 00400004 00000000 00000000 00000000 00000000 a002000c fsr fir 00000000 00000000
On peut vérifier que le programme a calculé 42 (2A en hexa) dans le registre 4 (alias a0).
Quelques autres commandes utiles avec gdb
-
i reg
: pour voir le contenu de tous les registres, -
p $t0
: pour afficher le contenu du registret0
, -
x /10i $pc
: pour afficher (en les désassemblant) les 10 prochaines instructions à exécuter.
Compilation d'un programme écrit en C
À partir d'ici, on suppose que vous avez installé GCC et newlib. En effet, nous allons compiler du C, utiliser un linker script idt.ld
fourni avec newlib, mais qui fait appel à des fichiers appartenant à GCC, ...
Exemple
Voici un exemple écrit en C :
int main (void)
{
int x = 21;
int y = 21;
return x + y;
}
Cet exemple n'utilise toujours pas de bibliothèque, mais il définit une fonction et des variables locales. Il aura donc besoin d'une pile.
Compilation
Le programme peut être compilé avec mips-gcc
:
mips-gcc -S simple-pile.c -o simple-pile.s
On peut regarder l'assembleur généré :
cat simple-pile.s
Assemblage
mips-as
fait le travail, comme précédemment.
Édition de liens
Avec les options par défaut, l'éditeur ne lien ne permet pas d'utiliser la pile. Un « linker script », idt.ld
permet de donner les instructions nécessaires à mips-ld
pour le faire :
mips-ld -Tidt.ld simple-pile.o -o simple-pile
Éventuellement, il faut écrire le nom du chemin complet vers idt.ld:
mips-ld -T/usr/local/mips-projetc-asm/mips-elf/lib/idt.ld simple_pile.o -o simple_pile

idt.ld
a fait ajouter à mips-ld
du code pour initialiser l'environnement d'exécution
avant la fonction main
(initialisation du pointeur de pile), et
quitter proprement après le retour de main. Au final, on a une
fonction _start
qui emballe notre fonction
main
.
Utilisation de la libc
Un programme peut aussi faire des entrées/sorties, en utilisant printf/scanf. Dans ce cas, la compilation et l'assemblage se font de la même manière, mais à l'édition de liens, il faut spécifier :
mips-ld -Tidt.ld programme.o -lc -lnullmon -lc -lgcc -o programme

-l
permet d'ajouter des bibliothèques auxquelles se lier :
-
-lc
se lie avec la bibliothèque standard C qui définit la plupart des primitives de base ; - Mais les 'vraies' entrée/sorties sur une plateforme physique se feraient en écrivant quelque part dans les registres d'un composant d'entrée-sortie, ou en faisant des appels systèmes. Les quelques instructions assembleur pour interagir physiquement avec l'environnement sont définies dans un moniteur. L'option
-lnullmon
prend un moniteur qui ne fait pas de vraies entrées/sorties (mais GDB saura tout de même afficher lesprintf
à l'écran pour nous) ; - Il faut répéter
-lc
pour résoudre des dépendances circulaires ; - l'option
-lgcc
permet de se lier à une petite bibliothèque propre à GCC qui contient quelques fonctions arithmétiques simples utilisées par des fonctions commeprintf
.
Exécution directe avec mips-run
On peut appeler ce simulateur depuis Gdb, ou bien directement depuis la ligne de commande :
mips-run ./simple-pile
et on peut vérifier que le return
a bien renvoyé la bonne valeur :
$ echo $? 42
Si le programme contient des appels à printf/scanf ou équivalents, mips-run
et mips-gdb
vont faire les entrées/sorties directement depuis le terminal courant.
Un compromis entre mips-gdb
et mips-run
est l'option --trace
de mips-run
, qui affiche chaque exécution exécutée :
$ mips-run --trace simple 2>&1 | head -n 5 insn: 0xa0020000 addiu r2, r0, 21 - ADDIU insn: 0xa0020004 addiu r3, r0, 21 - ADDIU insn: 0xa0020008 addu r4, r3, r2 - ADDU insn: 0xa002000c nop - SLLa insn: 0xa0020010 nop - SLLa
(sur cet exemple, nous n'avons pas de fin propre du programme, donc mips-run
fait un peu n'importe quoi après la dernière instruction)