mercredi 4 avril 2018

Chapitre 24 : et les macros ?


Une macro permet d’inclure dans un programme assembleur, une série d’instructions qui seront compilées en même temps que le programme (donc différent d’un objet qui est inclus au moment du link).
Les macros évitent de retaper plusieurs fois les mêmes instructions et il est possible de leur passer des paramètres.
Par exemple dans les premiers chapitres nous avons écrit une routine pour afficher un message dans la console, routine que nous avons inclus dans un autre programme contenant toutes les routines. Mais nous aurions pu écrire cette fonction sous forme de macro :

.macro  afficheMessage          @ définition de la macro
@ r0 contient l’adresse du message et il faut d’abord calculer sa longueu
              mov r1,r0   @ save adresse
    mov r2,#0   @ compteur de caractère
    1\@:         @ boucle de comptage de caractères
    ldrb r3,[r1,r2]   @ lecture un octet du message
    cmp r3,#0          @ c'est la fin ?
    addne r2,#1       @ non ajout de 1 au compteur
    bne 1\@b        @ et boucle
                @ appel systeme fonction affichage
    mov r0, #1                  @ r0 ← 1
    mov r7, #4                  @ write code 4  
    swi #0    
.endm   @ fin de la macro

L’appel de la macro dans le programme s’effectue simplement par le nom : afficheMessage
Les instructions utilisées sont les mêmes que l’assembleur sauf le label 1\@ qui est particulier. En effet une macro peut être insérée plusieurs fois et si nous avions mis un label comme 1 : ou Boucle1 : , la compilation aurait produit une erreur car elle aurait trouvé plusieurs labels identiques. Vous remarquerez que nous utilisons des registres sans les sauvegarder donc il faudra faire attention et les sauvegarder si nécessaire
Nous pouvons passer un ou plusieurs paramètres à une macro : dans la macro comparVal nous passons 2 adresses de message et une valeur à tester dont la valeur par défaut sera 0 (en respectant la syntaxe #0). L’utilisation de ces données dans le corps de la macro s’effectue en mettant un \ devant le nom. Nous pouvons appeler une autre macro dans la macro, ici en fonction du résultat de la comparaison le message affiché sera différent.
L’appel de la macro s’effectue simplement par comparVal r1, r2, #4 , Les registres r1 et r2 contiendront les 2 adresses nécessaires et la valeur de test sera passée comme une valeur immédiate.
Remarque : les macros ne sont pas à utiliser pour des actions trop compliquées !! Mais si vous aimez mes macros, vous pouvez vous constituer une collection et les grouper dans un fichier externe que vous appellerez avec l’instruction .include « nomdufichier ».
Voici un autre exemple : pour l’affichage des registres que j’utilisais depuis le début, j’affichais comme libellé l’adresse de l’instruction appelante car il n’est pas possible avec cet assembleur de récupérer le N° de ligne du source ce qui aurait été bien pratique. Avec une macro je peux donc récupérer un libellé comme paramètre, le stocker sur la pile pour l’afficher sans modifier les valeurs des registres. Bon il faut un peu bidouiller avec un registre puisque le push exige l’utilisation d’un registre. Voici le corps de la macro :
/* macro d'enrobage du vidage des registres  avec étiquette */
.macro vidregtit str
    push {r12}    @ save r12
                mrs r12,cpsr  /* copie du registre d'état  dans r12 */
                push {r12}   @ save du registre d’état sur la pile
                ldr r12,=lib1\@  @ recup libellé passé dans str
                push {r12}    @ passage argument sur la pile
                ldr r12,[sp,#8]   @ on remet en état r12 pour l'afficher correctement
                bl affregistres  @ affichage des registres  (cette routine restaure l’état de la pile)
                pop {r12}     @ on récupére le save du registre d’état
                msr cpsr,r12    /*restaur registre d'état */
                pop {r12}         @ on restaure R12 pour avoir une pile réalignée
                b smacro1vidregtit\@   @ pour sauter le stockage de la chaine.
lib1\@:  .asciz "\str"            @ stockage dans le corps du programme du libellé passé en paramètre
.align 4                       @ il faut aligner l’étiquette suivante car le libellé précedent n’est pas un multiple de 4
smacro1vidregtit\@:     @ pour continuer le reste du programme
.endm   @ fin de la macro
J’en ai aussi profité pour sauvegarder le registre d’état avant l’appel de la routine de vidage et le restauré à la fin pour que cette routine reste neutre.
L’appel se fait simplement par vidregtit  debut   ou vidregtit  Après_appel_compar_3
Et le résultat est le suivant :


Vous remarquerez que le registre r2 contient 0xD alors que l’on s’attendrait à trouver une adresse comme dans r1.
Et d’ailleurs r0 contient aussi 0xD !!  Donc il faut remonter les appels pour s’apercevoir que ComparVal utilise en dernier afficheMessage qui utilise r2 pour calculer la longueur du dernier message  donc ici 12 caractères (ne pas oublier de compter le \n final !!!). Et l’appel système Write (code 4) retourne dans le registre r0 la longueur du message affiché (revoir la doc des call system Linux). Donc c’est logique !!!

Remarque avril 2018: en utilisant cette macro sur un gros programme, le compilateur signale une erreur sur l'instruction  ldr r12,=lib1\@  @ recup libellé passé dans str . En effet dans un gros programme l'instruction de chargement d'une adresse avec le signe égal ne fonctionne pas. J'ai eu du mal à comprendre pourquoi puisque l'adresse du libellé ne se trouve que quelques instructions plus loin dans la même section. En fait l'instruction ldr r12,=lib1 est une pseudo instruction qui crée en fin du code programme, une variable supplémentaire contenant l'adresse de lib1. Et cette variable pour un gros programme peut se trouver au delà des 4K mots fatidiques. Donc j'ai remplacé cette instruction par l'autre pseudo instruction : adr r12,lib1   (et sans le =) qui fonctionne dans tous les cas. Cette instruction signifie de charger dans r12 l'adresse du libellé lib1.

Chapitre 23 : Opérations arithmétiques entières


Au chapitre précédent, nous avons vu quelques exemples d’additions, voyons maintenant quelques exemples de soustractions (ne pas oublier le s pour mettre à jour les drapeaux):   cas 1 calcul de 4 – 10, cas 2 calcul de 10 – 6 cas 3 calcul de valeur mini négatif – 5 (anomalie : c’est une valeur positive et le drapeau overflow est bien à 1).  Source du programme



Puis voici la multiplication simple mul et la multiplication avec ajout d’un registre mla.
Vous constatez dans ce cas qu’aucun drapeau ne peut être examiné, ce qui est curieux !! En effet comment savoir si le résultat n’est pas en overflow. Mais heureusement l’assembleur propose l’instruction umull qui permet d’avoir le résultat dans 2 registres, un contiendra les 32 bits bas et l’autre les 32 bits hauts. En testant si les 32 bits hauts sont différents de zéro, on peut savoir si la multiplication non signée est en overflow. Pour la multiplication signée il faut si l’égalité dans les instructions suivantes est fausse il y a overflow:
umulls r0,r1,r2,r3   @ r0 contiendra la partie basse et r1 la partie haute
                asr r2,r0,#31
                cmp r2,r1
Il existe d’autres instructions de multiplication mais je n’en ai pas trouvé l’usage.
Et la division ?  comme je l’ai déjà indiqué, sur mon raspberry avec le processeur ARM1176JZF-S la division n’est pas disponible. Il faut donc utiliser les routines décrites au chapitre 15 de http://thinkingeek.com et dont voici quelques variantes dans ce programme. J’ai aussi testé le temps d’exécution et j’ai trouvé que le temps de la routine faisant intervenir la division en virgule flottante donne de bons résultats. Dans le chapitre en question, l’auteur donne aussi des exemples pour diviser par une constante : par exemple 3 ou 5 ou 10 en utilisant un nombre magique.(voir le site http://www.hackersdelight.org/ qui donne des extraits d’un livre avec des idées intéressantes).
 Cette voie est très intéressante et il faut que je vérifie le temps d’exécution.
Dans ce deuxième programme, nous effectuons quelques tests de multiplications et de divisions pour voir les résultats en non signée et signée ; Notez l’utilisation de l’instruction rsb pour multiplier une valeur par une puissance de 2 – 1 (par exemple 7,31,63 etc )
Aparté : beaucoup d’articles sur internet concernent l’optimisation des programmes en assembleur. Hormis des routines particulières qui doivent être exécutées des millions de fois, il n’y a pas lieu dans nos exemples de chercher à optimiser le temps d’exécution. Pareil pour la taille des programmes qui se chiffre ici en kiloOctets et que l’on dispose de mégaoctets de mémoire. Il me parait plutôt judicieux d’optimiser le nombre de registres utilisés dans une routine (car il n’y a que 11 registres disponibles) et de réduire le risque d’erreur en écrivant des routines lisibles et compréhensibles.

mardi 3 avril 2018

Chapitre 22 : Manipulations de bits, affichage des flags


A la réflexion, le programme précèdent n’est pas un bon exemple pour apprendre l’assembleur sauf à montrer ce qu’il ne faut pas faire sur un long programme : routines trop longues, mauvaise utilisation des registres, etc.
Puis après 6 mois de programmation sur le Raspberry, j’ai changé sur la perception de certaines instructions. Et donc dans ce chapitre, je vais repartir sur les instructions de base de manipulation des registres.
Pour cela, dans un programme de test, nous écrivons 2 routines, une pour afficher les 32 bits du registre r0, et l’autre pour afficher l’état de chaque drapeau du registre d’état (Flag Z zéro N négatif C carry et V overflow). Dans le programme nous appelons 2 fois ces routines pour vérifier leur neutralité. Remarquez que pour assurer la sauvegarde restauration du registre d’état, nous utilisons les instructions mrs et msr peu fréquentes. Vous remarquerez aussi que la routine d’affichage des drapeaux n’utilise aucun branchement mais seulement des instructions conditionnelles.
Ensuite nous alimentons le registre r0 avec la valeur 0, 1 et -1 en utilisant l’instruction movs, le s servant à positionner les drapeaux (son oubli est une erreur fréquente !!!). Pour la valeur zéro nous voyons bien le flag Z passer à 1, pour la valeur -1 le flag N passer à 1, pour la valeur 1 les 2 flags sont à zéro. Reste le cas du carry qui reste à 1 quelle que soit la valeur !!  L’instruction mov ne doit pas le faire intervenir. Vous remarquerez que la valeur -1 est représenté par les 32 bits à 1 ce qui représente bien cette valeur en complément à 2 pour les valeurs signées. Si ces 32 bits représentent une valeur non signée, c’est à vous de le savoir. 


Puis nous mettons la valeur maximum 2 puissance 31 – 1 dans r0 puis la valeur positive maximum 2 puissance 30 -1. Mais attention, l’instruction ldr ne met pas à jour les flags, il faut pour cela effectuer une comparaison cmp r0,#0.
Nous remarquons dans le premier cas que la valeur est identique au – 1 précédent et le flag N à 1. Et pour la valeur maximum positive, le flag N est bien à 0. Ajoutons 1 aux deux valeurs. Dans le premier cas le registre contient zéro et le flag Z est à 1 et le flag N à 0. Pour l’autre cas, le registre ne contient que des bits à 1 donc la valeur -1 et le flag V overflow est passé à 1 ce qui indique bien qu’il y a un débordement (puisque la valeur positive maxi + 1 ne peut pas être négative !!).

Et tiens le carry est passé à 0 !! En effet le carry est touché lors des opérations arithmétiques (et bien sur par les comparaisons et les déplacements/rotations de bits) sur les valeurs non signées. Dans le premier cas, on ne voit pas qu’il est passé à 1 car il l’était déjà. Il passe à 1 car la valeur qui était à la valeur maximum non signée passe à 0 après l’ajout du 1 ce qui est anormal et le carry le signale. Dans le 2ième cas il passe à 0 car en non signée, l’addition est correcte. Nous pouvons revérifier en recommençant l’ajout de 1 à la valeur maxi FFFFFFFF et le carry passe bien de 0 à 1.
Quelquefois il est intéressant de récupérer le signe d’un nombre dans un registre. Il suffit d’utilise pour cela l’instruction asr r0,#31   et si la valeur est positive r0 contiendra 0 et -1 si la valeur est négative :

Voyons maintenant l’utilisation de l’instruction mvn. Elle transfère tous les bits d’un registre en les inversant (0 devient 1 et 1 devient 0). Cela permet de l’utiliser pour convertir une valeur de positif à négatif et inversement en ajoutant 1 au résultat.

Après, nous regardons les instructions and (ET), orr (OU), eor(OU exclusif), et bic qui permet de mettre un bit à zero.
Et bien sûr, si nous voulons savoir si le résultat est égal à 0 ou négatif, il faut ajouter un s à chaque instruction 
Déplacement des bits :  pour déplacer les bits dans un registre nous utilisons lsl vers la gauche et lsr vers la droite en indiquant le nombre de déplacement soir par une valeur immédiate soit par un registre. Mais c’est bizarre dans mon premiers cas, ce n’est pas  2 bits que j’ai déplacé mais les bits 1011  . Mais c’est bien sûr j’ai mis 11 dans le registre r0 alors que j’aurais dû mettre 3 soit 11 en binaire (ou j’aurais dû mettre 0b11 ce qui est plus compréhensible).
Le programme montre différents déplacements dont les derniers avec test des flags pour voir l’alimentation du carry. Ces instructions servent aussi à effectuer des multiplications par des puissances de 2 (lsl) ou des divisions par des puissances de 2 (lsr).Par exemple prenons 3 soit 11 en binaire et si nous le déplaçons d’un bit à gauche par lsr r0,#1 il devient 110 en binaire ce qui correspond à 6. Mais attention cela ne marche que pour les valeurs non signées. Pour les valeurs signées, il n’y a que l’instruction asr qui permet d’effectuer des déplacements vers la droite.
Enfin il existe une possibilité de modifier le registre servant de 2ième opérande avant l’opération principale en utilisant les instructions lsl,lsr (barrel shifter en anglais) Par exemple, multiplier le 2ième opérande par 2 avant de le mettre dans le registre r0 :  mov r0,r1,lsl #1 . Nous avons déjà vu cette possibilité lors des accès à un tableau en mémoire : ldr r0,[r1,r2, lsl #2] qui permet d’accéder au poste d’indice R2 et situé à 4 * r2 octets du début de la table donnée par r1. Cela aussi permet d’effectuer des multiplications rapides pour les valeurs supérieure de 1 à une puissance de 2.
Par exemple pour multiplier par 3  la valeur de r1 il faut saisir add r0,r1,r1 lsl #1  ou par 5 add r0,r1,r1 lsl #2. 
Puis nous essayons les différentes possibilités de modification d’un ou plusieurs bits. Ces manipulations permettent d’avoir dans un seul registre 32 valeurs logiques ce qui occupe peu de place et avec peu d’instructions.
Enfin nous terminons par des exemples des instructions tst et tsq.
Exercice :  Sur un site internet, j’ai vu qu’en plus de l’état des flags, la routine affichait l’état (0 ou 1) de chaque condition (eq, ne, gt, lt, etc), faire pareil pour les 15 conditions (inutile pour al et ne).

Le deuxième programme teste les instructions de rotation : soit ror pour effectuer des rotations vers la droite du nombre de bits indiqués et rrx qui effectue une seule rotation à droite, et le contenu du Carry passe dans la position 31 et le bit 0 passe dans le Carry (et il faut ajouter s pour le trouver dans les flags). Il n’y a pas de rotations vers la gauche (mais une rotation à gauche équivaut à 32 – 1 rotations à droite).

Enfin voici des exemples pour stocker et récupérer 4 octets dans un registre. En effet quelquefois, les données à gérer ne dépassent par la valeur 255 et donc il est possible de stocker 4 valeurs dans un registre ce qui économise les registres. Danc cette exemple, nous voyons comment stocker les octets un par un sans toucher aux autres octets du registre et les récupérer sans dégrader le registre d’origine.
Enfin nous terminons le programme par quelques instructions particulières : comptage des zéros à gauche dans un registre.

lundi 12 mars 2018

Chapitre 21 : IA Raspberry : petit système expert



Après avoir vu différents aspects de développement en assembleur sur le raspberry, nous allons écrire un programme plus complet  comme divertissement !!!!
Ce programme est fortement inspiré du livre de MME  Virginie MATHIVET « L’intelligence artificielle pour les développeurs »  publié aux éditions ENI.
Dans ce livre les exemples de programmes sont écrits en Java et j’ai donc dû les adapter au langage assembleur. Quoi !! Réécrire un programme en langage objet de haut niveau en langage assembleur !! hé bien pourquoi pas !! et je vais peut-être en décevoir certains mais un objet ce n’est rien d’autre qu’un pointeur associé à une structure de données et une méthode, ce n’est rien qu’une routine à laquelle on passe un pointeur de la structure à traiter.
Pour la théorie des systèmes experts, je vous renvoie au livre en question.
Donc j’ai déjà adapté les règles pour les mettre en français et j’ai adapté le programme pour lire ces règles à partir d’un fichier texte facile à saisir. La syntaxe d’une règle doit être impérativement respectée et est de la forme :
Nomdelaregle :  SI  (nompremisse1=valeur (question)) ET nompremisse2)  ALORS  conclusion
Les prémisses et la conclusion sont considérées comme des faits. La prémisse 1 est de type entier (une valeur entière doit être saisie), la prémisse2 est de type booléen. On peut mettre le caractère ! devant le nom pour indiquer que la prémisse de type booléen doit être fausse. Voici un exemple de règle :
R2 :  SI (Triangle ET Angle_Droit (La figure a-t-elle au moins un angle droit ?)) ALORS Triangle_rectangle
Voici le fichier des règles que j’ai utilisé pour ce développement (et toujours inspiré du livre cité plus haut).
Dans le programme, nous commençons par décrire les structures qui seront utilisées : une pour les faits et une pour les règles ainsi que toutes les constantes.
Dans la section data, nous décrivons les libellés à utiliser et dans la section bss, nous réservons la place pour la base des régles et sa copie, la base des faits, ainsi que les compteurs et pointeurs associés. Dans ce programme, nous n’allons pas utiliser des listes chainées pour stocker les données mais des tableaux. C’est pourquoi nous reservons la place en multipliant la taille de chaque structure (donnée par fait_fin par exemple) par le nombre d’élements estimés.
Dans la section du code, nous commençons par récupérer le nom du fichier des règles passé dans la ligne de commande et nous appelons une routine pour lire ces règles et les stocker dans un buffer. Puis nous initialisons les tableaux, pointeurs et compteurs de chaque entité.
Ensuite nous devons analyser le buffer pour éclater les règles en nom, prémisses et conclusion en fonction des mots clés SI ET ALORS. Nous avons 2 solutions : soit écrire une véritable analyse syntaxique de chaque règles cad découper chaque règles en phrases et mots et définir leur type ou alors analyser chaque caractère du buffer et effectuer un découpage en fonction des caractères rencontrés ? C’est cette dernière solution que j’ai développé ici car plus simple à mettre en œuvre.
Donc dans la routine analyseRegles, nous balayons un à un les caractères du buffer pour eliminer les blancs, trouver le nom de la règle puis le délimiteur : puis le SI pour chercher le nom de la première prémisse, déterminer si c’est une variable entière ou booleenne, convertir sa valeur et préparer l’affichage de la question éventuelle. Dans ce programme, nous ne recopions pas les noms dans un tas ou dans d’autres zones de mémoire, nous nous contentons de conserver le pointeur de début du nom et nous forçons un zéro de fin de chaine lorsque nous arrivons à un délimiteur de fin. Ici cela est possible car le buffer de lecture n’est pas réutilisé pour un autre usage dans ce programme. Les pointeurs sont stockés dans les tableaux définis précédemment en fonction de leur type : règles, prémisses, faits et conclusion. Remarque, l’analyse est succincte et donc de nombreuses erreurs possibles de saisies des règles ne sont pas détectées, il faudrait être plus strict !!  Mais pour assurer un contrôle, nous affichons les règles après leur prise en compte ce qui permet de vérifier si cette partie a bien fonctionné :
Ensuite nous trouvons la partie résolution, qui commence par effectuer une copie de la base des règles, une remise à zéro de la base des faits puis une boucle qui va examiner chaque règle, et en fonction des prémisses, poser les questions nécessaires à la résolution du problème.
Les résultats doivent être affichés triés ce qui est effectué par une routine de tri par insertion.
Voici un exemple de fonctionnement :

Il est très facile d’ajouter de nouvelles règles sans avoir à modifier le programme. Et il est possible de créer plusieurs fichiers de règles sur des domaines différents.

Exercices : réecrire le programme en utilisant des listes chainées pour le stockage des règles et des faits.
                écrire un programme pour implanter un moteur d'inférence en chainage arrière.
                écrire un fichier des règles sur un autre sujet (et pourquoi pas sur la configuration du raspberry !!!).