TD : Assembleur ARM

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:

La maquette

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’adresse 0x20000000 que nous utiliserons pour le programme

  • sram2 à l’adresse 0x10000000 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 packagé 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.

La connexion

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
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 ici une liste de commandes usuelles de gdb

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 ld la position en mémoire en passant un argument. Pour des cas plus complexes, il est recommandé d’utiliser un script d’édition de lien (linkerscript).

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 (armv7-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 armv7-m
.cpu cortex-m4

.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 à as.

arm-none-eabi-as --target-help pour la liste des options spécifiques.

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 devez utiliser le fait que la chaîne se termine 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 gdb:

mon reset

Les commandes précédées par mon (ou monitor) sont destinées à l’intermédiaire JLink. Ici on lui demande de remettre à zéro la cible.

Fichier attachéTaille
Fichier Environnement de travail516 octets