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        

3 commentaires:

  1. Dans blogger, je m'aperçois que l'on ne peut pas ajouter de pieces jointes. C'est donc difficile de joindre le source des programmes !!

    RépondreSupprimer
  2. Bonjour Vincent
    juste stupéfait de la quantité d'infos disponibles !
    je partage l'info sur les réseaux sociaux de framboise314
    BRAVO !
    amitiés
    François

    RépondreSupprimer
  3. Bonsoir François.
    Merci de vos encouragements. Je regarde souvent le site framboise314 pour toutes les informations sur le raspberry. En effet je trouve que c'est un outil idéal pour expérimenter et apprendre tous les volets de l'informatique.

    RépondreSupprimer