Simulation d'un programme avec mips-gdb

De Ensiwiki
Révision de 15 septembre 2015 à 15:34 par Moym (discussion | contributions) (Préliminaires : telesun n'est plus)

Aller à : navigation, rechercher

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.

Remarque pour utilisateur avancéEn fait, il s'est passé pas mal de choses :
  • 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 ou stepi et non next et step (on peut abréger en ni et si).
  • 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 registre t0,
  • 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
Remarque pour utilisateur avancéCe 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 :

telesun:~>mips-ld -Tidt.ld programme.o -lc -lnullmon -lc -lgcc -o programme
Remarque pour utilisateur avancéL'option -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 les printf à 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 comme printf.


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 :

telesun:~> 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 :

telesun:~>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)