Présentation de l’environnement
Carte de développement
Pour tester votre code assembleur, vous utiliserez les cartes STM32L4 Discovery kit IoT node.
L’image suivante est une vue du dessus de cette carte:
Nous avons identifié les éléments suivants qui serviront au TD:
-
Le micro-contrôleur principal qui exécutera votre code (entouré en rouge).
-
Le micro-contrôleur de l’interface de debug (JLink pour ces cartes) qui permet de:
-
charger un programme,
-
contrôler l’avancement du programme,
-
examiner le contenu de la mémoire.
-
-
Le connecteur USB de l’interface de debug.
Pour ce TD nous utiliserons exclusivement la mémoire vive. Dans le micro-contrôleur cible il existe deux mémoires distinctes:
-
sram1
à l’adresse0x20000000
que nous utiliserons pour le programme -
sram2
à l’adresse0x10000000
que nous utiliserons pour des données
Le programme sera chargé par l’intermédiaire d’un outil de debug (gdb
) et disparaîtra à l’extinction de la carte.
Environnement logiciel
Pour assembler et désassembler le code nous utiliseront les outils de la toolchain gnu
.
Nous utiliserons plus particulièrement la toolchain adaptée au développement bare-metal sur les micro-contrôleurs cortex-M.
La toolchain, au préfixe arm-none-eabi
est déjà installée sur les machines des salles de TP.
Pour l’ajouter à votre PATH
:
export PATH=/comelec/softs/bin:$PATH
Si vous voulez l’installer sur votre PC personnel, la version que nous utilisons est accessible ici.
Il doit aussi exister une version pour votre distribution Linux, mais la version risque d’être différente (gcc-arm-none-eabi
et gdb-multiarch
par exemple sur Debian/Ubuntu).
Pour ce TD, nous utiliserons les outils suivants:
-
arm-none-eabi-as
: l’assembleur -
arm-none-eabi-objdump
: un outil permettant, entre autres, de désassembler.
Connexion à la maquette
L’interface (sonde) de debug se trouve sur la maquette. Pour s’y connecter à partir de votre PC, il faut utiliser un logiciel intermédiaire qui permettre de faire communiquer le debugueur (gd) avec la sonde.
Comme notre sonde est une sonde jLink
nous utiliserons JLinkGDBServer
qui se connectera à la sonde USB et attendra des ordres de gdb
.
Cet outil se comporte comme un serveur pour gdb
qui s’y connecte en TCP pour transmettre ordres et programmes.
Cette approche permet d’utiliser le même debugueur (gdb
) avec des sondes différentes, le protocole de communication de gdb
étant standard.
Si vous voulez installer l’outil sur votre PC personnel, il est accessible ici.
Session de debug
Dans l’archive suivante vous trouverez un Makefile pour assembler, créer un exécutable, se connecter à la maquette et lancer une session de débugage.
PREFIX=arm-none-eabi-
AS = $(PREFIX)as
LD = $(PREFIX)ld
GDB = $(PREFIX)gdb
# si vous sutilisez gdb-multiarch remplacer par
# GDB = gdb-multiarch
ASFLAGS = -g
LDFLAGS = -g -Ttext 0x20000000
ASFILES = $(wildcard *.s)
ELFFILES = $(patsubst %.s,%.elf,$(ASFILES))
all: $(ELFFILES)
%.o:%.s
$(AS) $(ASFLAGS) $< -o $@
%.elf:%.o
$(LD) $(LDFLAGS) $< -o $@
.PHONY: connect debug clean
connect:
JLinkGDBServer -device STM32L475VG -endian little -if SWD -speed auto -ir -LocalhostOnly
debug:
$(GDB) -x gdbcmd
clean:
rm -f $(ELFFILES)
Ce Makefile assemblera un fichier assembleur dont l’extension est .s
.
Un exécutable est ensuite créé avec le programme positionné à l’adresse 0x20000000
.
Il aura le même nom que le fichier assembleur avec l’extension .elf
.
La cible connect
permet de lancer l’outil de communication avec la sonde de debug.
La cible debug
lancera gdb
en lui demandant d’exécuter les commande contenues dans le script gdbcmd
pour configurer la session, se connecter au serveur de debug et stopper la cible.
Dans gdb
on peut charger un exécutable:
file fichier.elf
load
step
...
Vous trouverez une liste de commandes usuelles de gdb
ici
Travail préliminaire
Premier code assembleur
Écrire le code suivant dans un fichier (test.s
):
mov r0, #33
ldr r0, =33
ldr r0, =0xC01dCafe
L’assembler
arm-none-eabi-as test.s -o test.o
Si vous avez déjà téléchargé l’archive contenant le Makefile, make test.o
devrait produire le même résultat.
L’option -g
permet juste de générer des symboles pour le debugueur que nous utiliserons plus tard.
Désassemblez et observez le résultat:
arm-none-eabi-objdump -d test.o
Comparez comment ont été transformées les différentes directives et instructions. Remarquez aussi que les instructions font toutes 32 bits.
Les étiquettes
Complexifions, le programme:
mov r0, #33
ldr r0, =33
ldr r0, =0xC01dCafe
/* initialisation du compteur */
mov r0, #10
loop:
subs r0,r0,#1
bne loop
end:
b end
Désassemblez et identifiez comment ont été transformées les étiquettes. Vous pouvez par exemple ajouter des instructions dans la boucle pour voir comment le binaire évolue.
Les objets et les exécutables
Les fichiers .o
obtenus jusqu’ici ne sont pas vraiment positionnés en mémoire.
Les instructions commencent systématiquement à l’adresse 0
.
Les fichiers .o
sont dits relogeable (relocatable).
Pour construire un exécutable correctement positionné en mémoire:
make test.elf
Note
|
Ici nous avons un seul fichier, nous pouvons préciser à l’outil d’édition de lien |
Désassembler, comparez.
Vous avez dû obtenir le message d’avertissement suivant:
warning: cannot find entry symbol _start; defaulting to 0000000020000000
Sans script d’édition de lien, ld
s’attend à ce qu’on lui précise le point d’entrée du programme (la première instruction à exécuter).
Par défaut, l’outil s’attend à trouver un symbole nommé _start
que nous allons donc ajouter.
.global _start
_start:
mov r0, #33
...
Note
|
Nous verrons par la suite que le point d’entrée peut être changé dans le script d’édition de liens. |
Est-ce le bon processeur?
Dans le code désassemblé, toutes les instructions font 32 bits alors que le processeur cible est un Cortex-M4 qui ne supporte que le mode thumb.
Il faut donc préciser le modèle de processeur et/ou son architecture (armv7e-m
pour le Cortex-m4).
Comme nous ne voulons pas modifier le code, nous précisons que nous utilisons la syntaxe unifiée.
.syntax unified
.arch armv7e-m
.cpu cortex-m4
.thumb
.global _start
_start:
mov r0, #33
...
Assemblez, désassemblez et comparer.
Note
|
Nous aurions pu préciser le modèle et l’architecture en passant les bons arguments à
|
Exercices
Exercice 1 : Initialisation d’une zone mémoire
Écrire un programme qui permet de remplir une zone mémoire avec un motif prédéfini.
-
Le motif
0xdeadbeef
-
La zone mémoire commencera à l’adresse
0x10000000
-
La taille de la zone mémoire 256 octets (64 mots)
Exercice 2 : Copie du contenu d’une zone mémoire
Ajouter au programme précédent de quoi copier la zone pré-remplie à une autre position.
-
L’adresse de la nouvelle position
0x10000300
-
Taille de la zone à copier 256 octets
Le programme de copie ne doit pas faire d’hypothèse sur le motif ayant servi à initialiser la première zone. Il devra donc lire puis écrire chaque élément.
Exercice 3 : Procédures réutilisables
Nous voulons pouvoir réutiliser le code d’initialisation et de copie avec des motifs, adresses et tailles de zones différents.
Modifier le code précédent pour avoir une procédure principale qui fait appel au code d’initialisation puis de copie, plusieurs fois, avec des paramètres différents.
Exercice 4 : Manipulation d’une chaine de caractère
En utilisant une directive, inclure à la fin d’un programme la chaine de caractère "Bonjour le monde!"
.
Comme en C, il faudra garantir qu’en mémoire, la chaine se terminera par un octet nul.
Écrire un programme qui copie cette chaine plusieurs fois à différents endroits en mémoire.
Ce programme sera structuré comme suit:
-
une procédure principale où on récupère l’adresse de la chaine ainsi que l’adresse de destination,
-
une procédure dans laquelle s’effectue la copie qui sera appelée plusieurs fois à partir de la procédure principale.
-
ici vous ne connaissez pas forcément la taille de la zone à copier, vous devez utiliser le fait que la chaine se termine par un octet nul.
-
vérifiez que vous n’allez pas au-delà du nombre d’octets de la chaine et que la copie se termine elle aussi par un octet nul.
-
Note
|
Si la carte ne répond plus correctement ou si vous observer des comportements erratiques, vous pouvez remettre à zéro le micro-contrôleur cible à partir du shell de
Les commandes précédées par |