jeudi 31 octobre 2019

Chapitre 68 : assembleur ARM 64 : affichage d’un registre en hexa.


Pour continuer la découverte de l’assembleur ARM 64 sur mon raspberry, je duplique les 2 routines précédentes et je les stocke dans un fichier routinesARM64.s que je compile séparément à la racine de mon répertoire ASM64. Je crée aussi le fichier constantesARM64.inc et je crée un fichier ficmacros64.s dans lequel j’écris (à partir des macros 32 bits) les premières macros : affichage d’un libellé (affichelib et affichage d’un registre en binaire affbintit).
Après quelques tests, ces macros fonctionnent correctement en 64 bits. Il faut penser à ajouter le fichier routinesARM64.o dans l’instruction ld du script de compilation.
Dans le programme affregistre.s, j’ajoute la routine affReg16 qui affiche le contenu du registre x0 en hexadécimal. Il n’y a pas de difficulté particulière dès que l’on a compris l’utilisation de la pile, l’absence des instructions conditionnelles. Je n’ai pas utilisé l’instruction ubfx pour extraire les 4 bits de chaque chiffre puisqu’elle n’est possible qu’avec des valeurs immédiates.
Je teste la routine avec différentes valeurs 0, -1 et je vérifie que l’adresse de la pile n’est pas altérée par les appels des routines. Tout va bien.
Ensuite je duplique le programme en affregistre1 et je le modifie pour appeler les 2 macros et les routines contenues dans le fichier objet.
J’en profite pour tester les instructions ldp et stp en utilisant le registre x5 comme base. Tout va bien !!
Je teste aussi les différents mov qui permettent d’insérer des valeurs de 16 bits dans un registre. Vous remarquerez que la valeur 010 s’affiche 8 !! Pourquoi ? parce que dans l’assembleur as une valeur qui commence par 0 est considéré comme octal. Donc attention 10 et 010 n’auront donc pas la même valeur dans un registre.
Il me reste à transformer cette routine d’affichage en hexa en macro. En 32 bits, j’affichais tous les registres dans la macro affregtit mais ici il va y avoir 31 registres à afficher ce qui va faire beaucoup. Je réfléchis à afficher que 6 registres sur une ligne mais en passant le n° du premier registre à afficher dans la macro. 
Ce sera l'objet du prochain article. 
Tous les sources se trouvent ici : https://github.com/vincentARM/ARMassembly64

mercredi 30 octobre 2019

Chapitre 67 : assembleur arm 64 : affichage d’un registre en base 2


Maintenant que nous savons afficher un texte, nous pouvons écrire une routine qui va afficher un registre en base 2, ce qui va nous permettre d’effectuer quelques vérifications sur l’assembleur ARM 64 bits.
Mais dans le précédent chapitre, j’ai oublié de mentionner la documentation sur l’arm64. Donc nous trouvons la bible ici (5000 pages !!!) : https://static.docs.arm.com/ddi0487/a/DDI0487A_j_armv8_arm.pdf?_ga=2.21860476.697819718.1572029197-1839848267.1555097605
Et une synthèse des instructions et quelques exemples : https://modexp.wordpress.com/2018/10/30/arm64-assembly/. Par contre je n’ai pas trouvé de synthèse au format pdf comme celle pour le 32 bits.
Et une autre précision : j’ai ajouté le script de compilation avec as et du link avec ld ici https://github.com/vincentARM/ARMassembly64/blob/master/Debut/compil64

 Revenons à notre routine. Nous trouvons une première instruction stp x0,lr,[sp,-64]! qui fait descendre l’adresse de la pile de 64 octets puis qui stocke les 2 registres r0 et lr aux adresses sp et sp + 8 (Ne pas oublier le ! qui indique que la pile doit être mise à jour). Puis une deuxième instruction stp x5,x6,[sp,48] qui stocke les 2 registres aux adresses sp+48 et sp+64 etc. Ces instructions remplacent les push et nous retrouvons en fin de routine les instructions ldp qui restaurent les valeurs d’origine des registres (en remplacement des pop).
Dans mes routines, je sauvegarde tous les registres pour que celles-ci soient le plus neutres possible !!!
 Ensuite il faut tester chaque bit du registre x0 pour afficher sa valeur 0 ou 1. Hélas il n’existe plus d’instruction lsrs qui permettait de décaler le registre d’une position à gauche et de récupérer la valeur du bit dans le registre carry. Il a fallu trouver une autre solution : déplacer un bit 1 en fonction du contenu du registre puis faire un tst pour tester la valeur du bit du registre x0 se trouvant à cette position. En fonction du résultat on copie le caractères 0 ou 1 dans un registre 32 bits pour le stocker dans la zone à afficher. Attention, les instructions lsr, lsl, asr et ror ne s’appliquent qu’avec des valeurs immédiates. Pour mettre un registre comme valeur de shift,il faut utiliser les instructions lsrv, lslv, asrv et rorv.
Vous remarquerez qu’il n’est plus possible d’utiliser les instructions sous la forme add x2,#1 mais qu’il faut utiliser add x2,x2,#1.

Cette routine sert à tester que le registre x1 est bien sauvegardé dans les routines, à vérifier que le registre w0 n’est que la partie basse d’un registre 64 bits et attention il faudra toujours s’en souvenir pour éviter des anomalies !!
Je teste aussi l’instruction ubfx qui permet d’extraire une série de bits situé à une position donnée (hélas il me semble que le nombre et la position ne peuvent être que des valeurs immédiates).
Et je teste aussi le résultat d’une nouvelle instruction eon.

En lisant la documentation sur les nouvelles instructions, je trouve l’instruction csel qui remplace une partie des instructions conditionnelles. Dans le programme affbinaire1.s : https://github.com/vincentARM/ARMassembly64/blob/master/Debut/affbinaire1.s je modifie les instructions qui mettent 1 ou 0 suivant le bit à l’aide de branchement pour les remplacer par l’instruction csel  x6,x7,x8,eq. Cela évite les branchements mais il faut utiliser 2 registres supplémentaires x7 x8 pour stocker les valeurs 48 et 49 !!

lundi 28 octobre 2019

Chapitre 66 : Programmer en assembleur ARM 64 bits avec le raspberry pi 3B+


Il y a plusieurs mois, j’avais acheté un Raspberry pi 3B+ en pensant pouvoir apprendre l’assembleur arm 64 bits. J’avais échoué à le faire fonctionner avec un Linux 64 bits suite à de nombreux problèmes de configuration. Par exemple avec une version Barnani améliorée, le Wifi ne fonctionnait pas !!.
Mais je gardais un œil sur les évolutions des OS 64 bits pour le Raspberry et il y a quelques jours j’ai découvert une version https://github.com/sakaki-/raspbian-nspawn-64. Il m’a fallu moins de  2 heures pour faire fonctionner celle çi sur le Raspberry puis d’arriver à compiler un programme assembleur en 64  bits mais il m’a fallu plus de temps pour que le programme se termine sans erreur !!

Voyons la démarche : tout d’abord, j’ai suivi la documentation (en anglais) pour télécharger l’image sur un pc windows 10  et la flasher grâce à Etcher sur une carte SD de 16GO.
Ensuite pour éviter les problèmes rencontrés avec mon écran LCD et mon petit clavier, j’ai relié le Raspberry au PC grâce au câble de liaison TTL /USB. Dans le fichier config.txt accessible depuis le PC dans la partition boot j’ai ajouté l’option enable_uart=1. Puis j’ai inséré cette nouvelle carte dans le Rapsberry qui a booté sans problème. Dans le logiciel Putty du PC, j’ai vu apparaitre tous les messages d’installation et la demande du user et du mot de passe (j’ai saisi ceux par défaut). J’ai modifié le fichier /etc/wpa_supplicant/wpa_supplicant.conf pour prendre en compte ma livebox et rebooter. Hélas, j’avais oublié d’autoriser le ssh et donc il a fallu à partir de la console Putty série redémarrer le raspberry après avoir autoriser le ssh (dans sudo raspi-config) et créer un fichier ssh dans la partition boot. Il est aussi préférable de modifier le mot de passe de l'utilisateur pi.

Ensuite j’ai pu me connecter par une connexion ssh wifi avec Putty et supprimer le câble de liaison TTL. J’ai donc vérifié quelques commandes unix, crée les répertoires de travail, transféré un source assembleur 32 bits puis le compiler avec as et l’exécuter sans problème. Donc je pourrais toujours écrire et tester des programmes 32 bits.
Ensuite j’ai écrit un premier programme qui utilise les registres x0 et w0 pour le 64 bits.  Évidement la compilation habituelle génère des erreurs. Mais comment compiler en assembleur 64 bits ? Une relecture complète de la documentation de cette version m’indique qu’il faut utiliser un container spécial pour lancer des programmes 64 bits. Il faut donc taper ds64-shell pour lancer debian-buster-64.
Les commandes unix habituelles se déroulent sans problème mais pas le compilateur as !! Après recherche, je m’aperçois qu’il faut installer les packages 64 bits en restant dans ce container. Le compilateur as se trouve dans le package binutils qu’il suffit d’installer comme les autres packages 32 bits (sudo apt-get install binutils).
Cette fois ci la compilation as accepte bien les nouveaux registres mais rejette les commentaires qui commencent par @. Je les remplace par les // et la compilation se termine sans erreur.
Lancement de l’exécution et bien entendu l’erreur bien connue Segmentation fault apparait.
Evidemment, il n'y a aucune visibilité pour savoir d’où vient le problème mais en supprimant toutes les instructions une à une, je trouve qu'il s’agit d’une erreur de l’appel système svc pour terminer le programme. Bien sûr, les codes des call system Linux sont différents entre le 32 bits et le 64 bits. Sur Internet je trouve différentes tables dont les codes pour le EXIT sont différents et ne fonctionnent pas. Puis la recherche sur le fichier unistd.h du raspberry ne donne rien dans le container 64 bits.
Après plusieurs heures de recherche et de test, je trouve enfin un site qui me donne le bon code 93 et j’arrive enfin à terminer normalement le programme. L’affichage d'un message standard devient un jeu d’enfant.
Vous pouvez aussi vérifier que le code retour est bien 5 avec la commande echo $?
Voici le programme ici.
On remarque déjà que les valeurs immédiates ne sont plus précédées du signe #, que les adresses ne sont plus de la forme .int mais .quad (double soit 64 bits ce qui est normal).
Le code du call système doit être passé dans le registre A8 (et non plus r7 comme en 32 bits).
Maintenant il nous reste à découvrir tout le reste !! Mais déjà je découvre que nous avons 31 registres de 64 bits et 31 registres de 32 bits à notre disposition. On va pouvoir s’amuser !!   Non,non Grosse erreur : il n’y a que 31 registres, les registres 32 bits ne sont que la partie basse des registres 64 bits (cf chapitre suivant où j’ai effectué la vérification).

Ah, j’oubliais tous les messages sont en anglais et l’heure n’est pas bonne. Il faut aller à nouveau dans raspi-config pour mettre en place le bon paramétrage local !!

Pour continuer, dans le programme affTexte, nous allons créer une routine pour calculer la longueur de la chaine avant de l’afficher. Je découvre que les instructions push et pop n’existent plus et même les instructions stm et ldm. On ne peut utiliser que des ldr et ldp, cette dernière permettant de stocker 2 registres d’un coup. Il faut de plus gérer l’avancement (ou plutôt le retrait) de la pile donc ici 2 fois 8 octets.
Un petit mot sur les registres : ceux de 64 bits commencent par la lettre x, ceux de 32 bits par la lettre w. Le registre lr est le x30, et le fp est le x29. La pile sp est un registre supplémentaire. Pour les appels de routines, les registre x0 à x7 servent pour le passage des paramètres et ne sont pas conservés. Les registre x8 à x18 servent pour les calculs temporaires et ne sont pas sauvegardés. Les registres x19 à x28 sont sauvegardés. Tout cela est encore à vérifier !!

Autre surprise lors de l’écriture de la routine, l’instruction ldrb n’est valide que si le registre récepteur est un registre 32 bits. Il n’est plus possible de mettre les instructions de calcul de manière conditionnelle (addeq n’est pas autorisée) mais les branch peuvent l’être.
Le retour de la sous procédure se fait simplement par l’instruction ret qui utilise le registre lr (r30) comme adresse de retour.

Prochaine étape : affichage d’un registre.

samedi 12 octobre 2019

Chapitre 65 : mesure de performance du Cortex m53 (pi3b+) avec PERF_EVENT_OPEN


Dans les chapitres concernant le développement baremetal, j’avais utilisé les instructions spèciales mcr et mrc pour afficher le compteur du nombre de cycles (voir chapitre ..) mais cette solution ne peut pas être utilisée avec Linux car ces instructions sont interdites !! même en utilisant sudo.
En cherchant sur internet, des informations sur le processeur CORTEX M53 qui équipe mon raspberry pi3b+, je suis tombé sur un article en anglais qui expliquait la possibilité d’utiliser le périphérique PERF_EVENT disponible sous Linux pour effectuer toute une série de mesures. J’ai donc creusé un peu plus la question à partir de la documentation disponible sous ce lien :
Bien sûr, documentation en anglais mais très complète et qui se terminait par un programme en C. J’ai donc repris ce programme, pour le réécrire en assembleur ARM et voir si celui-ci donnait des résultats exploitables.
Le premier programme m’a donc servi à effectuer une série de tests pour utiliser perf_event_open sur le raspberry.
Il a donc fallu retrouver la structure de paramétrage nécessaire (perf_event_attr),et les constantes nécessaires.
La structure et les compteurs résultats sont en 64 bits, ce qui complique un peu la tâche.
Dans le fichier /usr/include/arm-linux-gnueabihf/asm/unistd.h j’ai retrouvé le code de l’appel système à utiliser : 364 pour le perf_event_open.
Le programme commence par la mise en place des paramètres dans la structure : PERF_TYPE_HARDWARE dans le type, la longueur de la structure puis les bits de contrôle : disabled à 1 pour lancer la mesure à l’aide d’un appel ioctl, et exclusion des instructions du kernel et du superviseur.
L’adresse de la structure sera passée dans le registre r0, puis le pid ou zéro dans le registre r1 puis le n° de la cpu à suivre (ici nous demandons toutes les cpus) dans r2 puis -1 dans r3 (distingue un leader dans un groupe) puis le compteur désiré dans le registre r4. Nous allons compter le nombre d’instructions (ou de cycles compteur qui est en commentaire) Puis nous effectuons l’appel système 364 et s’il n’y a pas d’erreur, nous récperons le File Descriptor qui sera associé au périphérique.
Pour lancer et arrêter la mesure, nous utilisons l’appel système ioctl tout d’abord pour effectuer un reset puis pour autoriser la mesure. Puis nous effectuons les instructions à mesurer et nous arrêtons la mesure par un nouvel appel ioctl.
Cela permet de ne mesurer que les instructions que nous voulons. Le résultat est lu par un simple appel système read auquel nous passons le FD crée plus haut et l’adresse du buffer. Le résultat se trouve dans les 8 premiers octets du buffer. Pour l’instant nous nous contentons de convertir les 4 premiers octets en chaine de caractères que nous affichons par la procédure habituelle.
Si les paramètres ne sont pas ceux attendus, les appels génèrent des erreurs pas toujours faciles à trouver !!
Commençons par compter les instructions pour un simple mov. Le résultat est 9 ce qui semble correspondre à toutes les instructions entre les 2 svc. Remplaçons le mov par un bl appelProc pour appeler une procédure qui effectue le minimum : nous trouvons 15 ce qui correspond bien au nombre d’instructions.
Remplaçons  le compteur par celui de mesure des cycles et relançons le programme plusieurs fois, le résultat n’est pas identique à chaque lancement. J’avais déjà constaté ce phénomène lors des tests hors linux mais je n’ai toujours pas d’explication. Je suppose que la présence de mémoire cache, l’anticipation des instructions, la présence de 4 cœurs font que le nombre de cycles est variable  pour les mêmes 15 instructions !! Donc cela ne sera pas facile d’optimiser une routine particulière. Il faudra peut être effectuer des milliers d’appel de la routine et de calculer des moyennes.
Maintenant, vous pouvez tester d’autres compteurs et sélectionner un cœur particulier ( ou cpu)  !! Voir la documentation bien fournie sur toutes les possibilités. Je n’ai par contre pas pu faire fonctionner le compteur PERF_COUNT_HW_REF_CPU_CYCLES mais il faut peut être ajuster d’autres paramètres.
Dans cette doc, j’ai trouvé qu’il était possible de regrouper plusieurs compteurs pour effectuer les mesures sur la même entité. C’est l’objet du deuxième programme.
Dans ce programme, nous créons un leader comme dans le premier programme en ajoutant le paramètre PERF_FORMAT_GROUP pour grouper les résultats puis nous créons 2 types de mesure fils en passant le FD du leader dans le registre r3 (pour le leader, nous avons laissé -1). Par ailleurs nous mettons le bit diseabled à zéro car comme cela c’est le démarrage du leader qui lancera les 2 fils.
Le compteur du leader mesurera le nombre d’instructions, le fils1 mesurera le nombre de cycles et le fils2 le temps d’exécution (PERF_COUNT_SW_TASK_CLOCK). Attention pour ce compteur il faut mettre le paramètre type à PERF_TYPE_SOFTWARE.
Ensuite comme précédemment, nous lançons la mesure avec uniquement le FD du leader à l’aide des appels système IOCTL puis nous récupérons les résultats en effectuant un seul read. Les tests indiquent qu’il faut une longueur de 8 * 4 = 32 octets pour le buffer de lecture. La documentation indique que la première zone contient le  nombre de compteurs (ici nous trouvons 3) puis les 3 compteurs stockés dans les 3 zones de 8 octets suivantes. Nous convertissons ces zones en appelant la routine de conversion d’un double de 64 bits pour être rigoureux.
Remarque : pour le paramètre read_format nous pouvons ajouter PERF_FORMAT_TOTAL_TIME_ENABLED et/ou PERF_FORMAT_TOTAL_TIME_RUNNING. Dans ce cas, il est ajouté après les 8 octets du nombre de compteurs, une ou deux zones de 8 octets contenant les temps d’exécution. Un test montre que ces temps sont équivalent au compteur PERF_COUNT_SW_TASK_CLOCK pour des instructions ou procédures courtes.
Après quelques tests, le nombre d’instructions semblent stables alors que le nombre de cycles et le temps sont très variables. Pour certaines procédures, le nombre de cycles est inférieur au nombre d’instructions ce qui me parait bizarre.


A titre d’exemple le programme instSpecgrM1 montre la métrologie effectuée sur une procédure de conversion en ASCII d’un nombre de 64 bits (double). Nous partons de celle proposée dans le chapitre 54 et nous l’appellerons 1000 fois dans une sous-routine.
Tout d’abord nous effectuons une première mesure en mettant les appels en commentaires.  Sur mon raspberry pi3+ nous trouvons 8009 instructions ce qui correspond aux 8 instructions à l’intérieur de la boucle exécuté 1000 fois (y compris les instructions propres à la boucle) et 9 instructions hors boucle et compris entre les 2 svc de mesure.
Effectuons une mesure avec la première procédure et nous trouvons 14 941 894 instructions exécutées : waouhh !!
Une première optimisation est faite en remplaçant la division générale d 'un mot de 64 bits par un mot de 32 bits par la division d’un mot de 64 bits par une constante et qui n’utilise que des décalages et une multiplication pour le calcul du reste. Nous trouvons 8 263 009  instructions soit un gain de 44%/
Une deuxième optimisation pour simplifier les décalages de plusieurs positions à droite, supprimer les tests d’overflow et remplacer la multiplication de 64 bits par une simple instruction de multiplication de la partie basse (car il s’agit de calculer le reste qui ne peut être qu’inférieur à 32 bits). Nous trouvons 1 745 342 soit un gain de 88% . Pas mal non !! 
Conclusion : il faut commencer à chercher le meilleur algorithme pour résoudre le problème particulier puis optimiser ce qui peut l'être plutôt que de commencer par optimiser un algorithme non performant !!!