dimanche 22 octobre 2017

Chapitre 2 : affichage de la valeur d’un registre en binaire, hexadécimal et décimal.



Avant de commencer l’écriture d’un programme pour battre AlphaGo au jeu de go !!! nous allons écrire un programme d’affichage du contenu d’un registre. Pour cela nous devons écrire une routine qui va convertir la valeur du registre en caractères ascii qui pourront être affichés par les instructions vues dans le chapitre précédents.
Si nous faisons appel à la fonction printf du C, cela est très facile puisqu’il suffit de définir la chaine suivante : « Valeur du registre : %p \n » si nous voulons un affichage en hexadécimal et de passer dans le registre r0 l’adresse de cette chaine et dans r1, la valeur du registre à afficher.
Je vous laisse le soin de le faire puis d’effectuer l’affichage en décimal et en binaire.
En assembleur, nous allons écrire les 3 routines de différentes façons pour nous permettre de pratiquer plusieurs instructions.
Tout d’abord dans la partie .data, il nous faut décrire le message à afficher :

sMessageHexa: .ascii "Vidage hexa du registre : "
sZoneHexa: .space 8,' '
              .asciz "\n"
.equ LGMESSAGEHEXA, . -  sMessageHexa

Le message commence par le libellé non suivi d’un zéro (directive .ascii) puis par la réservation d’une zone de 8 octets pour stocker le résultat de la conversion des 32 bits du registre. Vous remarquerez que l’instruction .space 8 est suivi du caractère espace. En effet si nous ne le précisons pas, la zone est initialisée avec des zeros contrairement à ce que le nom .space laisse penser. Le message se termine par le caractère de retour ligne « \n «  suivi d’un zero final (directive .asciz). Ensuite nous trouvons l’instruction permettant de calculer la longueur du message.
Nous écrivons sur le même modèle les messages pour l’affichage binaire et décimal mais en réservant des zones de 32 bits et de 11 bits.
Mais attention, un registre peut contenir un nombre décimal signé ou non signé et donc nous prévoyons 2 types de messages et il nous faudra écrire 2 routines.
Pour chaque routine, nous utiliserons le registre r0 pour passer la valeur à afficher.  Au début de chacune d’elles, nous sauvegardons les registres fp et lr sur la pile en utilisant l’instruction push. En fait c’est la sauvegarde du registre lr qui est la plus importante car elle conserve en lieu sûr l’adresse de retour au programme principal. La sauvegarde du registre fp est effectuée car il est nécessaire de conserver un alignement de la pile sur 8 octets (soit 2 registres). Et par la suite, la sauvegarde de FP sera indispensable donc il est préférable de prendre tôt cette habitude.
Ensuite nous sauvegardons tous les autres registres utilisés par la routine. Je préfère cette solution car comme cela nous ne nous posons pas la question de quoi faire dans le programme principal (mais attention, ne pas oublier que lors d’un appel à tout autre fonction les registres r0 à r3 ne sont pas sauvegardés).
Bien sûr, en fin de chaque routine, il faudra restaurer tous les registres par l’instruction pop et dans l’ordre inverse.
Routine de l’affichage en binaire :
Dans cette routine nous allons déplacer 32 fois les bits vers la gauche avec l’instruction lsls. Suivant la valeur du bit éjecté à gauche et mis dans le flag carry nous positionnons le caractère 0 ou le caractère 1 dans la zone de reception du message. Quand les 32 bits du registre sont traités, nous affichons le message en appelant la routine afficheMess en lui passant dans le registre r0 l’adresse du message et dans r1 la longueur.
Nous rencontrons ici notre première boucle : nous avons d’abord l’initialisation d’un compteur de boucle :r2, puis un label de début de boucle. Ce label est numérique car il s’agit d’un label local à la routine. Dans un programme complet vous pouvez avoir plusieurs labels 1  :  mais vous ne pouvez avoir qu’un seul label alphanumérique.
Ensuite nous avons le corps de boucle avec les instructions qui seront effectuées à chaque tour de boucle pour terminer par une comparaison du compteur de boucle avec la valeur 31 (et comme le compteur a été initialisé à 0 , la boucle fera 32 tours) et suivant le résultat du test un branchement vers le label de début de boucle soit blt pour Branch Less Than  et 1b pour label 1 backward (cad le label 1 précèdent). Les codes pour les conditions à tester (eq,lt gt, le ,ge etc.) sont à connaitre par cœur ou il faut avoir la liste à portée de main.
Une autre précision : les instructions de test comme cmp ou tst mettent systématiquement à jour le registre d’état. Les autres instructions ne le font que si un s a été ajouté à la fin du mot clé de l’instruction. Par exemple ici lsl déplace bien chaque bit sur la gauche mais ne stocke pas le bit éjecté dans carry. Pour cela il faut utiliser lsls.
Dans la routine affichemess, nous retrouvons les instructions pour afficher une chaine de caractères dans la sortie standard à l’aide de l’appel système Write.
Vous avez pu lire dans les différents tutoriels qu’il est possible de passer les paramètres par la pile. Mais ici cela ne nous aurait rien fait gagner car nous n’avons que 2 paramètres et il n’est pas possible de pusher une valeur immédiate sur la pile. Donc il aurait fallu de toute façon utiliser un registre pour mettre la longueur avant de le pusher sur la pile.
Routine de l’affichage en hexa :
On retrouve les débuts et fin de routine comme l’affiche binaire. Puis on stocke un masque de 4 bits à 1 dans le registre r3. En effet en hexa chaque chiffre est codée sur 4 bits (soit 0 à 15 ou plutôt 0 à F).
Nous appliquons ce masque sur le nombre à convertir, et nous comparons le résultat à 9. S’il est inférieur nous ajoutons 48 pour le convertir en caractère Ascii et s’il est supérieur nous ajoutons 55 pour le convertir en lettre de Aà F. Nous stockons ce caractère à la position de la zone reservée du message plus l’indice de position. C’est l’instruction strb r0,[r1,r4]   str pour stockage registre, le b pour byte, r0 contient le caractère à stocker, r1 l’adresse de début de zone et r4 le déplacement (offset) cad l’indice de la position. Attention : il ne faut pas oublier le b derrière l’instruction str sinon vous allez stocker tout le registre soit 4 octets  et le résultat sera surprenant.
Ensuite, nous vérifions si nous avons traité les 8 chiffres et si oui nous allons afficher le message sinon nous déplaçons à nouveau 4 bits sur la droite pour recommencer la boucle. Comme chaque fois que nous appliquons le masque dans le registre r0, les autres bits sont détruits, nous sommes obligés de conserver la valeur dans le registre r2. Remarquez la forme de l’instruction mov r2,r2,lsr #4 pour effectuer le déplacement.
Routine de l’affichage en décimal non signé :
Ici pour convertir et afficher la valeur nous utilisons la méthode des divisions successives par 10.
Mais que quoi !!! il n’y a pas d’instruction de division pour ce type de processeur. Et bien oui, pour simplifier le processeur, les concepteurs du processeur RISC n’ont pas prévu la division. Et donc c’est au programmeur à l’écrire à partir des autres instructions. Heureusement, les algorithmes pour ARM se trouvent facilement sur Internet.
Et en effet dans le chapitre 15 du site http://thinkingeek.com/ nous trouvons une routine pour effectuer la division avec en plus le retour du reste dans un registre.
A chaque reste successif, nous ajoutons la valeur 48 pour le transférer en caractères ascii. Nous arrêtons la boucle lorsque le quotient est égal à zéro et nous complétons la zone à gauche avec des blancs. Cela permet d’effacer les caractères résiduels éventuels en cas d’appel successif de la routine. Puis nous affichons le message final.
Routine de l’affichage en décimal signé :
Dans cette routine, nous commençons par déterminer si la valeur est inférieure à zéro. Si oui nous positionnons le caractère ‘-‘ dans le registre r6 puis nous multiplions la valeur par – 1. Ensuite la conversion en caractères ascii est identique à la routine précédente sauf que lorsque le quotient est égal à zéro, nous stockons le signe contenu dans r6 à la position courante puis nous complétons la zone par des blancs.
Dans le programme principal (main) nous pouvons maintenant stocker des valeurs dans r0 et appeler nos 4 routines pour voir le résultat (ici nous mettons 15 puis -1 puis 0 pour tester) puis les contenus des différents registres pour avoir une idée de leur contenu. Nous remarquons que les données .data sont stockées après le code. Mais plusieurs points sont intrigants : l’adresse de la pile sur mon système est au delà des 512MO de ram de la mémoire et l’adresse du compteur d’instructions est vers 64KO ce qui me parait faible compte tenu de la taille d’un noyau linux. Il va falloir que j’approfondisse ces résultats !! (J'ai demandé sur le forum de Framboise314 et les réponses m'ont indiqué la documentation du BCM2835 qui explique la différence entre adresses réelles et adresses virtuelles).
Voici le lien vers le programme complet.
Revenons sur quelques points de ce programme : les instructions push et pop ne figurent pas dans les instructions de base de l’assembleur ARM. En fait ce sont des pseudo instructions qui remplacent les instructions STM et LDM. Les instructions push et pop sont beaucoup plus parlantes pour beaucoup de programmeurs. Comme je l’ai déjà dit, il faudra sauvegarder un nombre pair de registres et dans l’ordre croissant des registres (push {r2,r1} entraine une erreur de l’assembleur).
Pour l’instruction ldr, nous trouvons 2 formes ldr r0,=masque et ldr r0,masque : la première stocke dans r0 l’adresse de la variable masque, la seconde le contenu de cette variable. Si nous voulons avoir le contenu après l’utilisation de la première forme il faudra ajouter l’instruction ldr r0,[r0].
Pour la compilation et le link, vous avez peut être déjà écrit un script qui effectue ces 2 opérations et qui serait lancé par une simple saisie de compil nom_du_programme (sans le .s). Voici un exemple de script :

#compilation assembleur
echo "Compilation de "$1".s"
as -o $1".o"   $1".s" -a >listing.txt
ld -o $1 $1".o"  -e main
ls -l $1* 
echo "Fin de compilation."


Exercices : Extraire les routines de conversion pour les utiliser de manière autonome en passant la valeur dans r0 et l’adresse de la zone de réception dans r1.
                 Souvent, le vidage est fait en hexa et en décimal signé, modifier le programme pour avoir sur la même ligne les 2 conversions : par exemple « Vidage du registre Hexa :                  Décimal :             « .
                Écrire une routine qui affiche tous les registres en hexa sur 4 lignes de 4 registres comme ceci
                Vidage des registres :
                r0 :               r1 :                     r2 :                      r3 :               
                r4 :              r5 : etc
                La routine doit sauvegarder et restaurer tous les registres (sauf le pc).

SOURCE DU PROGRAMME.

/* programme assembleur ARM pour Raspberry */
/* affichage d'un registre en hexa, binaire et décimal */
/********************************/
/*  Données initialisées        */
/********************************/
.data
sMessageHexa: .ascii "Vidage hexa du registre : "
sZoneHexa: .space 8,' '
              .asciz "\n"
.equ LGMESSAGEHEXA, . -  sMessageHexa /* calcul de la longueur de la zone precedente */
sMessageBin: .ascii "Vidage binaire du registre : "
sZoneBin: .space 32,' '
              .asciz "\n"
.equ LGMESSAGEBIN, . -  sMessageBin
sMessageDeci: .ascii "Vidage décimal non signé du registre : "
sZoneDeci: .space 11,' '
              .asciz "\n"
.equ LGMESSAGEDECI, . -  sMessageDeci
sMessageDeciS: .ascii "Vidage décimal signé du registre     : "
sZoneDeciS: .space 11,' '
              .asciz "\n"
.equ LGMESSAGEDECIS, . -  sMessageDeciS
szMessageT1: .asciz "Vidage registre d'état\n"
.equ LGMESSAGET1, . -  szMessageT1
szMessageT2: .asciz "Vidage adresse masque\n"
.equ LGMESSAGET2, . -  szMessageT2
/********************************/
/*  Code section                */
/********************************/
.text   
.global main      /* point d'entrée du programme  */
main:             /* Programme principal */
    mov r0,#15      /* pour test */
    bl affichage2
    bl affichage16
    bl affichage10
    bl affichage10S
     mov r0,#-1      /* pour test  nombre negatif */
    bl affichage2
    bl affichage16
    bl affichage10
    bl affichage10S
     mov r0,#0      /* pour test  nombre zero */
    bl affichage2
    bl affichage16
    bl affichage10
    bl affichage10S
       /* Fin standard du programme */
    mov r0,#0  /* code retour r0 */
    mov r7, #1 /* code pour la fonction systeme EXIT */
    swi 0      /* appel system */
   
/******************************************************************/
/*     affichage registre en binaire                              */
/******************************************************************/
/* r0 contient le registre */
affichage2:
    push {fp,lr}    /* save des  2 registres */   
    push {r0,r1,r2,r3} /* sauvegarde des registres */
    ldr r1,adrzonemess  /* adresse de stockage du résultat */
    mov r2,#0    @ compteur de position

1:   /* début de boucle */
    lsls r0,#1   /* deplacement du bits de gauche dans la zone carry */
    movcc r3,#48  /* Carry Clear  : le bit est à zero */
    movcs r3,#49  /* Carry Set    : le bit est à un */
    strb r3,[r1,r2]   /* stockage du caractères ascii à l'adresse (r1) plus la position (r2) */
    add r2,r2,#1      /* incrementation du compteur */
    cmp r2,#31        /* 32 bits testés ? */
    ble 1b          /* non donc suite boucle  */
   
    /* affichage du résultat */
    ldr r0,=adrzonemessbin
    ldr r0,[r0]            /* adresse du message */
    mov r1,#LGMESSAGEBIN  /* longueur du message */
    bl affichageMess
   
    /* fin de la procedure */   
    pop {r0,r1,r2,r3}  /* restaur des registres */
    pop {fp,lr}        /* retour procedure */
    bx lr   
adrzonemess: .int sZoneBin      
adrzonemessbin: .int sMessageBin     
/******************************************************************/
/*     affichage registre en binaire                              */
/******************************************************************/
/* r0 contient l'adresse du message */
/* r1 contient sa longueur */
affichageMess:
    push {fp,lr}    /* save des  2 registres */
    push {r2,r7}    /* save des autres registres */
    mov r2,r1         /* longueur du message */
    mov r1,r0        /* adresse du message en r1 */
    mov r0,#1      /* code pour écrire sur la sortie standard Linux */
    mov r7, #4                  /* code de l'appel systeme 'write' */
    swi #0                      /* appel systeme */
    pop {r2,r7}     /* restaur des autres registres */
    pop {fp,lr}    /* restaur des  2 registres */
    bx lr            /* retour procedure */
/***************************************************/
/*   Affichage d'un registre en hexa               */
/***************************************************/
/* r0 contient le registre   */
affichage16:
    push {fp,lr}    /* save des  2 registres frame et retour */
    push {r0,r1,r2,r3,r4,r5}   /* save autres registres  */   
    mov r2,r0   /* save du registre */
    ldr r1,adrzonemessH
    //ldr r1,[r1]
    ldr r3,Masque     /* chargement de 0F soit 1111 dans r3 */
    mov r4,#7        /* dernière position du résultat */
1:
    and r0,r0,r3     /* application du masque sur le nombre */
    cmp r0,#9        /* comparaison par rapport à 9 */
    addle r0,r0,#48  /*inferieur ou egal c'est un chiffre  */
    addgt r0,r0,#55  /* sinon c'est une lettre en hexa */
    strb r0,[r1,r4]    /* on le stocke dans le caractère de la position */
    subs r4,#1         /* on enleve 1 au compteur */
    blt 1f           /* inferieur à 0 fin de la conversion */
    mov r2,r2,lsr #4  /* sinon on deplace 4 bits du nombre sur la droite */
    mov r0,r2         /* et copie dans r0 */
    b 1b              /* puis boucle */
1:   
     /* affichage du résultat */
    ldr r0,=adrzonemesshexa
    ldr r0,[r0]
    mov r1,#LGMESSAGEHEXA
    bl affichageMess
   
   /* fin standard de la fonction  */
       pop {r0,r1,r2,r3,r4,r5}   /*restaur des autres registres */
       pop {fp,lr}   /* restaur des  2 registres frame et retour  */
    bx lr                   /* retour de la fonction en utilisant lr  */   
Masque: .word 0x0F        
adrzonemessH: .int sZoneHexa      
adrzonemesshexa: .int sMessageHexa     
/***************************************************/
/*   Affichage d'un registre en décimal non signé  */
/***************************************************/
/* r0 contient le registre   */
affichage10:
    push {fp,lr}    /* save des  2 registres frame et retour */
    push {r0,r1,r2,r3,r4,r5}   /* save autres registres  */   
    ldr r5,=adrzonemessD
    ldr r5,[r5]
    mov r4,#10
    //add r5,r5,r4  /* on ajoute la longueur de la zone pour commencer par la fin */
    mov r2,r0
    mov r1,#10   /* conversion decimale */
1:    /* debut de boucle de conversion */
    mov r0,r2    /* copie nombre départ ou quotients successifs */
    bl divisionEntiere /* division par le facteur de conversion */
    add r3,#48   /* car c'est un chiffre */   
    strb r3,[r5,r4]  /* stockage du byte au debut zone (r5) + la position (r4) */
    sub r4,r4,#1   /* position précedente */
    cmp r2,#0      /* arret si quotient est égale à zero */
    bne 1b   
    /* mais il faut completer le debut de la zone avec des blancs */
    mov r3,#' '   /* caractere espace */   
2:   
    strb r3,[r5,r4]  /* stockage du byte  */
    subs r4,r4,#1   /* position précedente */
    bge 2b        /* boucle si r4 plus grand ou egal a zero */
    /* affichage du résultat */
    ldr r0,=adrzonemessdeci
    ldr r0,[r0]
    mov r1,#LGMESSAGEDECI
    bl affichageMess
   
   /* fin standard de la fonction  */
       pop {r0,r1,r2,r3,r4,r5}   /*restaur des autres registres */
       pop {fp,lr}   /* restaur des  2 registres frame et retour  */
    bx lr                   /* retour de la fonction en utilisant lr  */   
       
adrzonemessD: .int sZoneDeci      
adrzonemessdeci: .int sMessageDeci       

/***************************************************/
/*   Affichage d'un registre en décimal   signé  */
/***************************************************/
/* r0 contient le registre   */
affichage10S:
    push {fp,lr}    /* save des  2 registres frame et retour */
    push {r0,r1,r2,r3,r4,r5,r6,r7}   /* save autres registres  */   
    ldr r5,=adrzonemessDS
    ldr r5,[r5]     /* chargement debut zone */
    mov r6,#'+'     /* par defaut le signe est + */
    cmp r0,#0       /* nombre négatif ? */
    bge 0f
    mov r6,#'-'     /* oui le signe est - */
    mov r4,#-1
    mov r2,r0       /* et on multiplie le nombre par -1 */
    mul r0,r2,r4
0:   
    mov r4,#10   /* longueur de la zone */
    mov r2,r0    /* nombre de départ des divisions successives */
    mov r1,#10   /* conversion decimale */
1:    /* debut de boucle de conversion */
    mov r0,r2    /* copie nombre départ ou quotients successifs */
    bl divisionEntiere /* division par le facteur de conversion */
    add r3,#48   /* car c'est un chiffre */   
    strb r3,[r5,r4]  /* stockage du byte en début de zone r5 + la position r4 */
    sub r4,r4,#1   /* position précedente */
    cmp r2,#0      /* arret si quotient est égale à zero */
    bne 1b   
    /* stockage du signe à la position courante */
    strb r6,[r5,r4]
    subs r4,r4,#1   /* position précedente */
    blt  3f         /* si r4 < 0  fin  */
    /* sinon il faut completer le debut de la zone avec des blancs */
    mov r3,#' '   /* caractere espace */   
2:   
    strb r3,[r5,r4]  /* stockage du byte  */
    subs r4,r4,#1   /* position précedente */
    bge 2b        /* boucle si r4 plus grand ou egal a zero */
   
3:   
    /* affichage du résultat */
    ldr r0,=adrzonemessdeciS
    ldr r0,[r0]
    mov r1,#LGMESSAGEDECIS
    bl affichageMess
   
   /* fin standard de la fonction  */
       pop {r0,r1,r2,r3,r4,r5,r6,r7}   /*restaur des autres registres */
       pop {fp,lr}   /* restaur des  2 registres frame et retour  */
    bx lr                   /* retour de la fonction en utilisant lr  */   
       
adrzonemessDS: .int sZoneDeciS      
adrzonemessdeciS: .int sMessageDeciS       

/**********************************************/     
/* division entiere non signée                */
/* routine trouvée sur Internet               */
/* auteur  Roger Ferrer Ibáñez                */
/* site : http://thinkingeek.com/
/**********************************************/
/* attention ne sauve que les registre r4 et lr */     
divisionEntiere:
    /* r0 contient Nombre */
    /* r1 contient Diviseur */
    /* r2 contient Quotient */
    /* r3 contient Reste */
    push {r4, lr}
    mov r2, #0                 /* r2 ? 0 */
    mov r3, #0                 /* r3 ? 0 */
    mov r4, #32                /* r4 ? 32 */
    b 2f
1:
    movs r0, r0, LSL #1    /* r0 ? r0 << 1 updating cpsr (sets C if 31st bit of r0 was 1) */
    adc r3, r3, r3         /* r3 ? r3 + r3 + C. This is equivalent to r3 ? (r3 << 1) + C */

    cmp r3, r1             /* compute r3 - r1 and update cpsr */
    subhs r3, r3, r1       /* if r3 >= r1 (C=1) then r3 ? r3 - r1 */
    adc r2, r2, r2         /* r2 ? r2 + r2 + C. This is equivalent to r2 ? (r2 << 1) + C */
2:
    subs r4, r4, #1        /* r4 ? r4 - 1 */
    bpl 1b            /* if r4 >= 0 (N=0) then branch to .Lloop1 */

    pop {r4, lr}
    bx lr        

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).

Les outils



Comme indiqué, les sources seront saisis avec Notepad++ puis transférés sur le Raspberry pour être compilés avec l’assembleur gnu as et linkés avec ld ou avec gcc. Une connexion ssh sera ouverte à partir du logiciel puTTY (remarque avec les dernières versions de Jessie, l’autorisation d’une connexion ssh n’est pas active. Il faut l’activer dans le menu de configuration du Raspberry : voir la doc de base).
Sur Notepad++, il faut aussi installer la coloration syntaxique pour l’assembleur ARM avec l’extension .s car celle-ci n’est pas prévue par défaut.
Il est possible d’effectuer une compilation sur l’ordinateur maitre (par exemple sous Windows, voir par exemple le logiciel gcc-arm-none-eabi-6-2017-q2-update-win32.exe ) mais cette solution ne me satisfait pas car il peut y avoir des différences entre les diverses librairies utilisées par les 2 systèmes. Si vous préférez cette solution et si vous transférez le programme exécutable sur le Raspberry, n’oubliez pas d’effectuer un chmod 777 nomduprogramme pour le rendre exécutable sous linux. (Je suppose que vous avez une bonne connaissance des commandes Linux de base !!!).
Normalement les logiciels as, ld et gcc sont installés en standard avec la version Raspbian. Vous pouvez vérifier la présence des logiciels par as toto  ou ld toto ou gcc toto. Si en retour vous avez le message –bash as (ou ld ou gcc) commande introuvable c’est que ces logiciels ne sont pas installés ou non accessibles. Il vous faudra télécharger les paquets nécessaires et les installer sur votre système.
Vérifions le bon fonctionnement en tapant le premier programme indiqué dans le premier chapitre sur le site de Thinkingeek :

/* programme 1  */
/* pour vérification compilation et link  */
.text             /* -- Code section */
.global main      /* point d’entrée du programme */

main:             /* programme principal */
       mov r0,#2  /*  on met 2 dans le registre r0  qui sera le code retour du programme*/
       bx lr        /* fin du programme par retour à l’adresse contenu dans lr*/
Lien vers le source du programme à télécharger

Sauvegarder le programme sous pgm1.s (l’extension .s est réservée pour les programmes en assembleur ARM) et transférer le sous un répertoire de travail du Raspberry.
Taper dans la session ssh ouverte la commande de compilation pour créer l’objet :
as –o pgm1.o pgm1.s
Vous pouvez avoir un ou plusieurs messages d’erreur (et ici ce sont surtout des erreurs de saisie faciles à corriger) sinon  vérifier par ls –l si le fichier pgm1.o vient d’être crée.
Puis lancer la commande pour le link  qui va créer le programme exécutable:
ld –o pgm1  pgm1.o –e main
Le paramètre –e indique que le point d’entrée du programme est main. Cela doit correspondre avec l’instruction .global main de notre source.
Si pas de message d’erreur, vérifier par ls –l que le programme pgm1 est bien crée et le lancer en tapant la commande : ./pgm1
Bon sang : le message « erreur de segmentation » apparait dans toute sa simplicité. Notre programme n’a que 2 instructions et nous avons déjà une erreur incompréhensible. Heureusement le chapitre 4 de l’assembleur ARM sur Thinkingeek nous explique le fonctionnement du débogueur GDB. Après quelques manipulations j’arrive à vider le contenu des registres et je vois que le registre lr sensé contenir l’adresse de retour contient 0 ce qui ne nous aide pas beaucoup.
En relisant les commentaires du chapitre 1 sur Thinkingeek, je vois qu’un correspondant propose de terminer le programme en appelant la fonction système exit. Voici le code à mettre à la place du bx lr :
   mov r7, #1 /* code fonction système EXIT */
   swi 0         /* appel fonction système */
Sauvegardons ce programme sous pgm2.s puis transfert vers le Raspberry puis compilation par as puis link par ld et lancement par ./pgm2.
Cette fois ci il n’y a pas d’erreur mais rien n’apparait !! Ah oui il faut ensuite taper echo $ ? pour afficher le code retour du dernier programme exécuté. C’est Ok, le chiffre 2 apparait bien.
Cette erreur m’intrigue et j’imagine toute sorte de raisons de cet écart avec les instructions du site : Raspberry différent, version linux différente etc..
Mais en relisant le premier chapitre, je me rends compte que le link du programme est fait avec gcc (cad le compilateur linker pour le langage C). Donc je lance le link sur le pgm1.o avec la commande :
gcc –o pgm1 pgm1.o
Puis l’exécution du programme par ./pgm1 et là miracle plus d’erreur de segmentation et le programme fonctionne.
Nos outils de travail sont maintenant prêts et nous allons continuer la découverte de l’assembleur dans les posts suivants.
Remarque : l’erreur de segmentation est l’erreur qui va revenir le plus souvent lors des tests de programme. Nous voyons qu’elle correspond à une adresse invalide contenu dans un registre et le plus souvent cela sera 00000000.
Exercices : lisez le chapitre 4 sur le deboggeur GBD et essayer toutes les commandes sur ce petit programme.
                Lisez la documentation sur l’assembleur as, le linker ld et compilateur C gcc.
                Placer des valeurs différentes dans 2 registres, faites la somme, ou  la différence ou la multiplication et sortir le résultat comme ci-dessus.

Introduction



Ce blog a été créé pour raconter mon expérience sur la programmation en assembleur avec le Raspberry pi et pour guider les personnes débutantes intéressées par l’assembleur ARM sur ce matériel.
J’ai acquis récemment un Rasperry modèle 1B (512MO de ram) et je me suis demandé ce que je pouvais en obtenir en matière de formation à la programmation. Connaissant déjà le langage C et des langages de haut niveau comme Java, l’apprentissage d’un nouveau langage comme Python ne me semblait pas intéressant. Par contre en étudiant les caractéristiques du processeur du Raspberry, je me rends compte qu’il s’agit d’un processeur de type RISC avec un jeu d’instruction réduit. Cela me changeait des processeurs Intel avec leurs centaines d’instructions Assembleur et en plus dans un environnement Windows.
Je décidais donc de me lancer dans la programmation en assembleur pour voir quelles étaient les limites de son utilisation sur un Raspberry dans un environnement LINUX. De plus conscient de la faible puissance du processeur et de la taille limitée de l’espace mémoire du Raspberry, je supposais que des programmes écrits dans ce langage seraient plus performants que des programmes écrits en langages de haut niveau.
Mais ce blog ne sera pas un cours formel sur l’assembleur ARM mais plutôt l’utilisation de celui-ci dans l’environnement Linux du Raspberry. Pour une bonne présentation des instructions de assembleur, je vous conseille (hélas en anglais) le site suivant :  http://thinkingeek.com/arm-assembler-raspberry-pi/
Attention :  apprendre l’assembleur ne sert que pour connaitre les principes de base du fonctionnement d’un processeur, pour des besoins de formation ou d’écriture d’algorithmes performants. Si vous voulez écrire des programmes complets rapidement et proches de la machine, je vous conseille plutôt d’utiliser le langage C .
Pour beaucoup, l’assembleur parait difficile à apprendre mais en fait il n’y a que peu d’instructions à apprendre, 15 registres de 32 bits à gérer, 4 drapeaux à tester et de la mémoire composée d’octets indifférenciés dont les adresses sont sur 32 bits.
Autant que possible, je donnerais des exemples de programmes en indiquant les difficultés que j’aurais pu rencontrer. Mais attention, ces programmes ne seront pas forcément optimisés et ne seront pas la meilleure solution à des problèmes. Actuellement ils fonctionnent sur un Raspberry avec la  version Raspbian Jessie et sur le modèle B.
Pour l’utilisation, j’ai préféré utiliser une connexion ssh à partir d’un ordinateur sous Windows 10 où j’ai installé le logiciel puTTY. Cela me permet d’avoir les sources en double et d’utiliser un éditeur plus perfectionné que l’éditeur nano du Raspberry, en l’occurrence Notepad++ ( en plus celui ci permet le transfert des sources directement sur le Raspberry avec l’extension NppFtp).
Pour les prérequis nécessaires, je suppose que vous connaissez un minimum des commandes Linux, un minimum de programmation (tests, boucles, fonctions, sous programmes, tables etc ) et les représentation des données (hexa, binaire, décimal signé ou non signé, caractères et chaines de caractères).
En ce qui me concerne, vous voudriez bien me pardonner et me corriger de toutes les bévues que je pourrais raconter dans ce blog. En effet je découvre seulement les problèmes quelques chapitres avant leur diffusion et donc j’ignore toutes les subtilités de cette programmation.

Remarque : ce blog me permet aussi de me familiariser avec les outils de publication de blog.