dimanche 19 janvier 2020

Chapitre 78 : assembleur ARM 64 bits : lancement d'un thread


Je repars du programme assembleur 32 bits vu au chapitre 20 pour l’adapter à l’assembleur 64 bits. (source du programme).
Après les modifications des registres, des accès à la pile il ne reste plus qu’à trouver les codes des appels système en en 64 bits. Hélas l’appel fork ne semble pas exister en 64 bits et donc je modifie le programme pour utiliser l’appel clone.
Ces modifications refusant de fonctionner, j’effectue des recherches sur internet pour trouver la source de mon erreur. En fait, les paramètres de la fonction clone sont identiques à la fonction fork du 32 bits et donc ne correspondent pas à la première description que j’avais trouvée. En effet la documentation des appels système en 64 bits n’est pas très disponible.

J’adapte aussi les appels système pour les fonctions sleep et wait4 sans problème. Par contre je ne trouve pas de fonction pause en 64 bits.
Après recherche sur internet je trouve une solution en utilisant les appels système rt_sigprocmask et rt_sigsuspend.

Tout fonctionne parfaitement après quelques tests !!

mercredi 8 janvier 2020

Chapitre 77 : Assembleur ARM 64 bits : calculs en virgule flottante (instructions Float)


Je commence l’année par découvrir les instructions de calcul en virgule flottante. En 64 bits, ces instructions sont très différentes de l’assembleur 32 bits et à première vue sont plus simples d’utilisation. Pour effectuer des calculs en double précision, nous disposons de 32 registres de 64 bits dont le nom commence par d.

Comme nous n’avons pas de routines assembleur pour afficher des nombres en virgule flottante, il nous faut utiliser l’instruction printf du C en utilisant le formatage « %+09.25f\n ». Comme il y a peu d’exemple sur internet de programmation 64 bits arm en float, il nous faut découvrir comment passer les arguments à la fonction.
Après quelques essais, je trouve que la chaine de formatage doit être passée dans le registre x0 et les valeurs à afficher dans les registres d0 d1 etc. En effet en virgule flottante les registres à utiliser en double précisions sont des registres 64 bits qui commencent par d et nous pouvons utiliser xx registres. Attention : l’appel de la fonction printf détruit le contenu des registres d0 d1 et peut être d’autres (à vérifier). L’utilisation des instructions du langage C necessite aussi l’utilisation de gcc pour effectuer le link. 
Source du programme calculFloat1.s
Donc je commence à alimenter le registre d0 avec une valeur immédiate avec l’instruction fmov d0,2 et tout se passe bien avec plusieurs valeurs lorsqu’une erreur de compilation arrive pour la valeur 50 !!
La recherche dans les 5000 pages de la documentation ARM montre que les valeurs immediates possibles doivent pouvoir être codées sur 8 bits dont un exposant de 3 bits et une mantisse de 4 bits. La figure C2-2 de la page C2-136 donne les valeurs possibles : par exemple 1 2 4 8 16 mais aussi 0,25 ou 0,1796875 mais pas 50.
Bien ! maintenant passons une valeur dans le registre x0 puis dans le registre d0 et comme en 32 bit, il faut effectuer une opération de conversion en float avec l’instruction scvtf d0,d0.
Et enfin nous chargeons une valeur double stockée en mémoire par la simple instruction ldr.
Après affichage, nous la rechargeons dans le registre d0 puis la convertissons en entier non signé avec l’instruction fcvtzu et la transferrons dans le registre x0 en passant par le registre d1 pour varier un peu. L’affichage du registre x0 montre bien la valeur 3. Pour les valeurs signées il faut utiliser fcvtzs.
Nous effectuons quelques opérations de calcul, addition, multiplication, division, minimum, racine carrée et quelques comparaisons qui sont plus simples qu’en 32 bits.
Enfin nous vérifions le stockage d’une valeur en mémoire. Il existe encore plein d’instructions pour manipuler ces nombres (et pour des conversions en simple précision).
J’effectue aussi quelques tests pour voir les conversions 32 bits 64 bits.
Comme exemple je vais reprendre le programme écrit en 32 bits pour analyser la génération de nombres aléatoires. Voir le programme verifAlea.s. Celui çi ne pose pas de problème particulier sauf que la valeur immédiate zéro ne peut pas être mis dans un registre dn avec l’instruction fmov. Mais en utilisant le registre particulier xzr, cela fonctionne.
Vous remarquerez que j’utilise le registre d8 pour les calculs pour éviter la modification d’un registre comme d4 par le premier appel de printf.

Ensuite le programme saisieFloat1.s donne une routine de conversion d’une chaine de caractères (abc,def ou abc.def) en nombre float. La chaine est passés d’abord dans la ligne de commande puis est ensuite saisie en utilisant l’appel système read.
Je voulais utiliser l’instruction de conversion     scvtf d0,x0, n bits   mais le position de la virgule par n bits ne peut pas être la valeur d’un registre. Donc j’ai utilisé une autre technique, je recopie la chaine de caractère en supprimant la virgule ou le point et en conservant leur position. Ensuite je convertit en décimal la chaine résultante et convertit en float le résultat.
Avec la position de la virgule, je calcule la puissance de 10 necessaire pour effectuer la division en float pour trouver le résultat correct. Exemple :
Soit la saisie de 12345,67 qui donne la chaine 1234567 convertit en float pour donner le dividende.
La virgule est en position 5 à partir de la gauche et 2 à partir de la droite . Il faut calculer la puissance 2 de 10 soit 100 qui convertit en float donnera le diviseur.
Il ne reste plus qu’à effectuer la division en float de 1234567 par 100 pour avoir un nombre float résultant correct dans le registre d0.

mardi 17 décembre 2019

Chapitre 76 : assembleur ARM 64 bits : utilisation de X11


Je n’ai pas encore trouvé de solution pour la résolution de l’absence de /dev/mem dans le container 64 bits. De plus je n’ai pas réussi à installer le périphérique pour lire les compteurs relatifs à la mesure de performance. En effet il faut créer un module kernel en utilisant un package contenant les headers linux et il n’existe pas pour la version 64 bits !!. 

Donc nous allons voir l’utilisation de X11 pour générer des fenêtres graphiques. Pour cela nous reprenons la démarche utilisée pour le 32 bits : installation sur le raspberry du package X11   , installation d’un serveur X11 sur le PC (par exemple Xming), création d’une connexion au raspberry avec le top X11 dans putty. Pour plus de précisions je vous renvoie au chapitre 30 de ce blog.
J’effectue un premier test en 32 bits avec un des programmes anciens pour vérifier si la fenêtre s’affiche bien sur le PC : ok pas de problème.
Je reprends le programme de création d’une simple fenêtre creafen641.s. Il faut déjà recalculer les positions des données dans les structures car les définitions des zones doivent avoir une longueur de 8 octets. Enfin pas toutes !! Il faut donc reprendre chaque structure, passer tous les pointeurs et les entiers longs en 8 octets et laisser les entiers en 4 octets. Mais il y a des réalignements à faire s’il y a un nombre d’entiers impairs, il faut ajouter une zone de 4 octets pour que la zone pointeur suivante soit toujours bien alignée.
En conclusion, les structures du fichier defstruct64.inc sont toutes à revoir de très prés avant toute utilisation sérieuse.

Ensuite le passage des paramètres à la fonction XCreateSimpleWindow doit être revu. Nous pouvons passer les 8 premiers paramètres dans les registres x0 à x7 et le dernier est à passer par la pile. Celle çi doit toujours être alignée, c’est la raison de l’instruction str x8,[sp,-16]! Avant l’appel et de l’instruction add sp,sp,16 après l’appel.
Après modification du script de compil pour intégrer les librairies X11, exécution de la compilation et correction des erreurs !! lancement du programme.
Et bing erreur : PuTTY X11 proxy: Unsupported authorisation protocol
Longues recherches sur internet pour trouver l’origine de l’erreur, sans succès mais une piste apparait en explorant les logs : une ligne indique que le port 22 est déjà utilisé par un autre service !! Et en effet à la réflexion j’ai déjà une connexion entre Raspberry et Putty en 32 bits !! Donc il faut modifier le fichier /etc/ssh/sshd_config dans le container 64 bits  pour mettre un autre port par exemple 3222 au lieu de 22. Et cela fonctionne !

Maintenant, nous allons passer à un programme plus complet. En 32 bits, j’avais écrit au chapitre 62 un programme qui affichait des images jpeg et donc suivant le même principe je décide d’écrire un afficheur 64 bits des images png !! source du programme afficheurPNG.s
Pour cela il faut charger les packages X11 (X11-common,X11-apps etc.) et le package libpng-dev qui permet d’avoir accès à la librairie libpng dont on va se servir pour convertir les images PNG. En effet comme pour le jpeg, il n’est pas question de réécrire toutes les fonctions nécessaires en assembleur.

La documentation de la libpng peut se trouver ici :

Dans un premier temps, je repars du programme d’affichage jpeg 32 bits que je transforme en 64 bits à l’aide des consignes du chapitre précédent. J’effectue aussi les modifications des appels des fonctions X11 et je crée la même image des 3 cercles colorés utilisée pour jpeg au format png avec myPaint.
A l’aide de la documentation de la librairie, j’écris la partie de chargement de l’image jpeg, ouverture du fichier, lecture des 8 premiers octets pour vérifier la signature du fichier ( la fonction à appeler est différente de celle de la doc car il y a un paramètre supplémentaire qui est masqué par la redéfinition de la fonction dans png.h). Ensuite création des structures nécessaires, puis liaison entre ces structures et le FD du fichier à lire, puis lecture des données.
Ensuite le décodage de l’image est un peu plus complexe car il faut créer une table de pointeurs qui pointe chaque début de ligne de l’image en mémoire. Un exemple m’indique que l’image png est inversée, donc les pointeurs doivent être initialisés en ordre inverse (mais mes tests montrent que cela n’est pas exact ou alors j’ai fait une erreur lors des affichages X11).
Puis j’adapte le programme pour prendre l’image convertie en mémoire et l’afficher dans une fenêtre X11.
Après quelques tests et corrections des erreurs, je tombe sur l’erreur classique dans la fonction de lecture png_read_image que je n’arrive pas à résoudre. Quelques jours après, je me rappelle que le mélange des fonctions d’ouverture de fichier en assembleur ne fait pas bon ménage avec les fonctions de lecture du C utilisées par les librairies. Je récris donc les ouvertures/ fermetures pour appeler les fonctions du C et le programme fonctionne correctement.
Enfin presque, car les couleurs de l’image sont inversées dont j’ajoute une fonction d’inversion pour chaque pixel de l’image.

En lisant la documentation, je découvre qu’il est possible de convertir des images png à partir d’un buffer en mémoire.
Dans le deuxième programme afficheurPNG2.s, je remets l’ouverture et la lecture du fichier par des appels system en assembleur et je passe l’adresse du buffer à la fonction de décodage. Il faut la modifier pour prendre en compte la lecture depuis le buffer en créant une sous fonction de recopie png_read_from_memPNG. J’en profite pour modifier les zones réceptrices (pointeurs et pixels images) en allouant leurs places nécessaires sur le tas.
La lecture et le décodage se passe bien mais l’affichage de l’image entraine une erreur X11. Si je remets la zone des pixels de l’image en mémoire et pas sur le tas, l’affichage se passe bien !! L’allocation de la table des pointeurs elle ne génère pas d’erreurs.
Donc quelques jours après (la réflexion est lente !!) il me vient que le mélange de l’allocation de place entre ma fonction allocPlace et les fonctions de la libraire pour créer les structures doivent être incompatibles surtout que dans l’enchainement je libère la place des structures par png_destroy_read_struct avant d’afficher l’image X11.
Donc je déporte l’allocation de place de la zone des pixels de l’image dans la fonction de lecture du fichier car nous pouvons trouver au début du buffer lu les dimensions en pixel de l’image puis je passe l’adresse de la zone crée à la fonction de conversion par l’intermédiaire du registre x23 : c’est pas terrible comme programmation !! c’est à améliorer mais pour les tests, cela fonctionne bien : voir le programme afficheurPNG3.s
Que reste-il à faire ?  améliorer la programmation entre les différentes fonctions !! regarder d’un peu plus prés certaines fonctions annexes de conversion d’images png particulières (niveau de gris) et vérifier le bon fonctionnement de plusieurs lectures successives d’images différentes.
Il faudrait aussi vérifier la libération de la place du buffer de lecture, des zones des pointeurs et des pixels dans le cas ou il faudrait charger un grand nombre d’images. Çà me fait penser que j’avais écrit un programme de jeu qui se passait dans un labyrinthe et où les murs et le personnage étaient plus que succincts. Il faudrait que je le réécrive en utilisant des textures et des images png : du boulot pour l’année 2020 !!!

jeudi 12 décembre 2019

Chapitre 75 : Assembleur ARM 64 bits : lecture écriture de fichiers


Avec la version Buster et le container 64 bits que j’utilise, je n’ai pas pu faire fonctionner les accès au GPIO car il manque la description des peripheriques /dev/mem et /dev/gpiomem. Ces périphériques sont necessaires car Linux interdit les accès directs aux registres du gpio. En attendant qu’une version de Buster corrige ces problèmes, je vous propose de voir les lectures/écritures des fichiers en assembleur 64 bits.

Dans le programme lectfic1.s nous effectuons l’ouverture et la lecture d’un fichier simple. Pour cela il nous faut rechercher le code de l’appel système linux Open mais hélas qui n’existe pas en 64 bits. Il faut se rabattre sur le code OPENAT valeur 56 mais qui necessite une adaptation par rapport à l’Open 32 bits. Il faut transmettre dans le registre x0, une constante AT_FDCWD pour indiquer que le nom du fichier (avec son répertoire) s’effectuera à partir du répertoire courant. Ceci implique que nous ne pouvons pas ouvrir des fichiers avec un nom fixe partant de la racine comme /home/pi/toto.txt. Par contre, nous ouvrirons un fichier de la forme ../../rep1/rep2/toto.txt. Il doit y avoir une solution mais je n’ai pas encore trouvé.
Ensuite la lecture (code 63) et la fermeture (code 57) du fichier ne posent aucun problème

Le programme ecritfic2.s monte l’écriture dans un fichier. La seule petite difficulté provient de l’attribution des droits lors de l’ouverture du fichier. En effet il faut préciser dans le paramètre mode le masque à attribuer. La documentation sur les droits indique par exemple qu’il faut mettre 644 pour autoriser le propriétaire à lire et à écrire sur le fichier ainsi crée et les autres ne sont autorisés qu’en lecture. Sauf que ce code doit être mis en octal sinon les résultats sont bizarroïdes et donc il doit être écrit 0644 (avec un zéro devant !!) pour que l’assembleur as le comprenne.

Le programme lectecrfic.s reprend l’exemple donné au chapitre 8 de ce blog et écrit en assembleur 32 bits. Ici je l’ai adapté pour le 64 bits. Cette adaptation m’a permis de dresser une liste des opérations à effectuer pour passer d’un programme ARM 32 bits à 64 bits :

    Changer le symbole des  commentaires :   @ -> //
    Modifier tous les registres r en x
    Remplacer tous les push et pop  par des stp, str ldp ldr.
    Remplacer le r7 par x8 pour les call system
    Remplacer swi par svc pour les call system
   Vérifier le passage des paramètres pour les call system (x0 à x7 puis les autres sur la pile).
   Remplacer les .int par des .quad pour les définitions d’adresse
   Vérifier et modifier les descriptions de données dans la .data et la .bss
   Changer le nom du fichier des constantes et celui des macros.
   Changer les instructions conditionnelles : moveq  par exemple
   Modifier les instructions de type add x1,#1   en add x1,x1,1
   Modifier les déplacements:   lsl x1,#2   en lsl x1,x1,2

Chapitre 74 : Assembleur ARM 64 : tri d’octets avec les instructions SIMD


J’ai donc persisté pour écrire des routines avec les instructions SIMD pour tenter de voir toutes les possibilités. En regardant sur internet, j’ai vu qu’il y avait possibilité d’écrire un tri fusion en utilisant des instructions de ce type.
Alors allons y et commençons par écrire une routine qui trie 4 octets contenus dans un registre. Dans le programme rechtri4SIMD.s la routine tri4octets prend en entrée dans le registre x0, les 4 valeurs à trier.
Le principe de ce tri est de partir de 2 octets triés puis de les fusionner pour arriver à 4 octets triés.
Appelons ces 4 octets a,b,c,d et nous allons les découper en 2 parties a,b et c,d que nous allons comparer.
Pour cela nous mettons la valeur de x0 dans le registre vecteur s0 que nous découpons par tranche de 2 octets avec les instructions mov v1.h[0],v0.h[1] et mov v2.h[0],v0.h[0]. Nous nous retrouvons avec les valeurs a,b dans le registre v1 et c,d dans le registre v2.
La comparaison des 2 registres s’effectue par cmgt v3.8b,v1.8b,v2.8b (gt pour greather : plus grand) et le registre v3 contiendra pour chaque octet comparé la valeur FF si plus grand et 00 si plus petit.
Suivant le résultat, il faut inverser les 2 valeurs des 2 registres pour avoir les valeurs les plus petites dans un registre et les plus grandes dans l’autre. C’est le rôle des instructions :
    mov v4.8b, v1.8b
    mov v5.8b, v2.8b
    bit v4.8b,v2.8b,v3.8b     // échange en fonction du résultat
    bit v5.8b,v1.8b,v3.8b 
qui échangent dans les registres v4 et v5 les valeurs de v1 et v2 en fonction du résultat contenus dans v3.
Donc maintenant nous avons a<c et b<d, et nous allons fusionner ces 2 petites listes triées en comparant a,c et b,d (pour la théorie voir les livres ou article des algorithmes de tri-fusion parallèles). Nous devons mettre les bons octets de v4 et V5 à la bonne place dans v1 et v2. Nous recomposons d’abord la séquence complète a c b d dans le registre v0 avec l’instruction zip et nous le redécoupons comme précédemment dans v1 et v2. Nous effectuons à nouveau la comparaison et inversons les valeurs comme précédemment.
Il faut effectuer une dernière comparaison pour comparer b et c et donc il nous faut mettre les valeurs a et b dans le registre v1 et d et c dans le registre v2. Pour cela nous utilisons les instructions de déplacement d’un octet    mov v2.b[0],v5.b[1] et mov v2.b[1],v5.b[0].
Après cette comparaison, nous remettons les 4 octets triés dans le bon ordre dans le registre v0  puis dans le registre x0 pour le retour vers le programme appelant.
Ce tri fonctionne donc avec 3 comparaison seulement mais il faut quand même beaucoup d’instructions de déplacement.
Dans le programme suivant rechtri4SIMD1.s, nous allons voir quelques points d’amélioration puis nous adapterons la routine pour trier 4 octets se trouvant dans la mémoire.
Nous pouvons améliorer les inversions après comparaisons en utilisant l’instruction bsl (bits select) mais je n’ai rien trouvé de mieux pour la découpe.
Pour la recomposition finale, j’utilise l’instruction tbl qui à partir d’un vecteur guide permet de reconstituer les données en combinant celles de 2 vecteurs. Attention, je me suis fait piéger et j’ai cherché un moment la cause d’un dysfonctionnement. En effet l’accès aux octets du deuxième registre se fait avec des index supérieurs à 15 et la valeur doit être en hexa si la définition des données est en hexa. Ne pas mettre 16 et 17 par exemple pour accéder aux 2 premiers octets du 2 ième registre mais 10 et 11 !!! Attention il faut aussi que les 2 registres origines se suivent (v3 puis v4).
La routine tri4OctetsMem attend en entrée dans x0 l’adresse mémoire de 4 octets à trier et dans X1 l’adresse en mémoire de stockage du résultat. La routine de tri est identique à celle précédente mais il a fallu modifier l’ordre de recomposition finale pour avoir en mémoire l’ordre correct. Le registre x0 retourne aussi les octets triés.
Cette routine est-elle plus performante qu’un tri classique ? Je n’ai pas fait de tests pour l’instant car son application pratique est nulle !!!
Maintenant, que nous vu les principes sur 4 octets, nous allons voir le tri de 8 octets puis celui de 16 octets.
Pour le tri de 8 octets, il faut effectuer 6 comparaisons dans le programme rechtri8SIMD.s : tout d’abord nous coupons les 8 valeurs en 2 vecteurs de 4 octets que nous comparons pour obtenir 4 listes de 2 valeurs triées. Puis nous les fusionnons pour obtenir 2 listes de 4 valeurs triées et que nous fusionnons pour obtenir 1 liste des 8 octets triés.  Pour les inversions, j’ai repris la première méthode vue pour les 4 octets et pour les éclatements, j’ai laissé les instructions de déplacement octet par octet. Il est donc possible de gagner quelques instructions avec les méthodes vues dans le 2ième programme de tri de 4 octets.
Pour le tri de 16 octets, il faut effectuer 10 comparaisons dans le programme rechtri16SIMD.s . Nous reprenons la même logique que les autres programmes en utilisant au maximum l’instruction tbl pour réorganiser les données après chaque inversion.
Comme exercice, je vous laisse le soin de trier des données en mémoire en passant à la routine les adresses à lire et à écrire. Je vous laisse aussi réfléchir à la façon de trier des listes de dizaines d’octets avec cette routine !!!!
Moi je m’arreterai là car ces routines ont peu d’intérêt pratique. En effet on ne peut trier que des clés de 8 bits et il faut ajouter des instructions pour mouvementer les articles associés aux clés car un tri ne concerne pas que des clés !!!
Pour aller au delà des clés de 8 bits, on peut utiliser des instructions concernant 16 bits (v0 .4h) mais les comparaisons vont s’effectuer sur 4 clés au lieu de 8 et donc un gain plus limité.