jeudi 19 octobre 2017

Chapitre 1 : affichage dans la console



Notre premier programme ne faisait pas grand-chose et nous a servi à tester nos outils. Maintenant nous allons afficher une chaine de caractères dans la console.
Nous avons plusieurs manière de le faire : soit utiliser une fonction du système linux (comme l’EXIT précédente) soit appeler une fonction du C. Cette dernière solution permet de profiter de toutes les fonctionnalités des instructions du C mais cela oblige à connaitre quelques règles de fonctionnement du C et l’utilisation des librairies. Par la suite j’essaierais d’utiliser au minimum les fonctions du C ( car alors autant programmer directement en C !!!).
Voici le source du programme qui fait appel à la fonction printf du C pour afficher le « Bonjour le Monde » habituel.

/* programme hello  avec la fonction printf du C */
/********************************/
/*  Données initialisées        */
/********************************/
.data
szMessage1: .asciz "Bonjour le Monde.\n"
/********************************/
/*  Code section                */
/********************************/
.text   
.global main      /* point d'entrée du programme  */
main:             /* Programme principal */
    ldr r0, =szMessage1      /* adresse du message en r0 */
  
    bl printf          /* appel de la fonction du C */
    mov r0,#0  /* code retour r0 */
                mov r7, #1 /* code pour la fonction systeme EXIT */
    swi 0      /* appel system */

Vous remarquez que chaque instruction est commentée : c’est une bonne habitude à prendre pour comprendre leur utilité. Tout d’abord nous trouvons une section .data qui contient les variables initialisées. Ici nous avons le contenu du message défini par la directive  .asciz. Celle çi génèrera une chaine de caractères en mémoire terminée par 0 (voir les chaines en C). Le nom de la zone commence par sz. C’est une convention habituelle pour indiquer que la zone correspond à une chaine (string en anglais) terminée par zero.
Puis nous trouvons la section .text qui contiendra les instructions assembleur. La directive .global indique au monde extérieur que le point d’entrée du programme est main. Cela permet au système de savoir où commence le programme lorsqu’il sera lancé. Ensuite nous trouvons l’instruction ldr qui va stocker l’adresse du message dans le registre r0 puis l’appel de la fonction printf. Les paramètres des fonctions sont passés dans les registres r0 à r3. Les autres registres doivent être sauvegardés par la fonction appelée si elle les utilise. Mais vous pouvez faire ce que vous voulez à l’intérieur de vos programmes mais c’est plus clair de respecter quelques conventions.
Rappel sur les registres :
r0 à r3 : registres de travail, ou servant pour passer les paramètres : ne sont pas sauvegardés
r4 à r10 : registres de travail. Ils sont sauvegardés
r11 ou fp : pointeur de contexte (ou frame pointer) : peut servir de registre de travail localement : normalement il est sauvegardé.
r12 :pointeur pour les sauts longs : peut servir de registre de travail localement. Il n’est pas toujours sauvegardé.
r13 ou sp : registre de pile où seront sauvegardés les registres ou stockées des données temporaires.
r14 ou lr: contient l’adresse de retour d’une fonction, d’un sous programme.
r15 ou pc : compteur d’instructions
Registre d’état ( ou de flags) : évolue à tout moment lors des instructions de test. N’est jamais sauvegardé.
Revenons à notre programme : il se termine par un appel à la fonction système Exit. Après compilation par as, le link par ld se termine par une anomalie car la fonction printf n’est pas trouvée dans les librairies par défaut du linker. Dans ce cas il faut utiliser gcc pour linker notre programme avec les fonctions contenues dans les librairies C.
Le programme affiche bien la chaine prévue avec un saut à la ligne grâce au caractère \n placée en fin de la chaine.
Nous pouvons nous poser quelques questions : comment passer les paramètres à la fonction si nous avons plus de 4 paramètres ?, Quel est l’état de la pile au retour de la fonction ? les registres sont-ils bien sauvegardés comme prévus ? Pour verrons cela plus tard.
Deuxième solution d’affichage par appel de la fonction système write (voir la documentation sur les call système Linux sur la page : http://syscalls.kernelgrok.com/).
Voici le programme :

/* programme hello  avec l'appel systeme Write de Linux */
/********************************/
/*  Données initialisées        */
/********************************/
.data
szMessage1: .asciz "Bonjour le Monde.\n"
.equ LGMESSAGE1, . -  szMessage1 /* calcul de la longueur de la zone précédente */
/********************************/
/*  Code section                */
/********************************/
.text   
.global main      /* point d'entrée du programme  */
main:             /* Programme principal */
    mov r0,#1      /* code pour écrire sur la sortie standard Linux */
    ldr r1, =szMessage1      /* adresse du message en r1 */
    mov r2,#LGMESSAGE1       /* longueur du message */
    mov r7, #4                  /* code de l'appel systeme 'write' */
    swi #0                      /* appel systeme */
    mov r0,#0  /* code retour r0 */
    mov r7, #1 /* code pour la fonction systeme EXIT */
    swi 0      /* appel system */
Lien vers le source du programme

Après la déclaration du message, nous trouvons une pseudo instruction .equ qui permet de calculer la longueur du message en effectuant une soustraction de l’adresse courant (le point .) avec l’adresse du début du message et qui sera stockée dans la constante LGMESSAGE1 (autre convention ).
Dans le code du programme, nous appelons la fonction système write (code 4 dans le registre r7) en lui passant 3 paramètres : dans r0 un code 1 qui correspond à la sortie standard Unix (un code 2 correspond à la sortie des erreurs : voir la doc de base Unix)
Dans r1, nous chargeons l’adresse du message et dans r2 sa longueur. Vous remarquerez que nous utilisons un mov pour la longueur et un ldr pour l’adresse. L’instruction mov permet de charger dans un registre une valeur immédiate signalée par # ou la valeur d’un autre registre par exemple mov r0,r1. Chaque instruction du processeur RISC a une longueur constante de 4 octets soit 32 bits et le code opération, les codes des registres, les bits de condition utilisent 20 bits ce qui ne laisse que 12 bits pour une valeur immédiate (8 bits pour la valeur et 4 bits pour effectuer un déplacement des bits). Pour certaines valeurs qui ne peuvent être prises en compte par ce système, le compilateur vous signalera une erreur.
Et comme les adresses en mémoire sont aussi de 4 octets, on ne peut pas les charger par l’instruction mov. Par contre l’instruction ldr permet ce chargement car elle calcule un déplacement relatif. Le tutorial du site Thinkingeek vous explique cela dans le détail.
Vous remarquerez aussi que le code 4 correspondant au WRITE est stocké dans le registre r7, ce qui est curieux !!!
Ici aussi, il faudra vérifier si les registres sont bien sauvegardés comme prévu et quel est l’état de la pile après l’appel système.
Maintenant après ces débuts je vous laisse explorer les chapitres de Thinkingeek pour étudier toutes les instructions de base de l’assembleur. Si vous ne connaissez pas l’anglais, vous trouverez sur Internet des cours en français sur l’assembleur ARM  souvent plus généraux que celui de Thinkingeek consacré au seul Raspberry.

Exercices : ajouter un deuxième message dans le programme précèdent.
                 Mettre les valeurs pour EXIT(1) WRITE (4) et le code de la sortie standard sous la forme de constantes en début du programme (instruction .equ).
                Enlever ces constantes du programme et les isoler dans un fichier constantesARM.inc. Inserer ce fichier dans le programme (instruction include).

1 commentaire: