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