mardi 31 mars 2020

Chapitre 85 : assembleur ARM 32 et 64 bits : affichage des variables d'environnement


Dans ce chapitre nous verrons dans 2 petits programmes comment récupérer les variables d’environnement qui peuvent être utiles dans un programme (par exemple la variable USER ou le répertoire courant comme PWD).
En fait la récupération est très simple puisque l’adresse de ces variables sont stockées sur la pile par LINUX avant l’appel à notre programme.

Dans le programme 32 bits varEnv.s  nous commençons par stocker dans le registre fp (frame register) la pile puis nous stockons plusieurs adresses de la pile dans les registres pour examiner leur contenu après l’affichage par la macro vidregtit.
Comme déjà vu, l’adresse 0 contient le nombre de paramètres passés à notre programme dans la ligne de commande. Ici nous avons un seul paramètre (le nom du programme) dont l’adresse est passée à +4. En +8, nous trouvons l’adresse de retour qui est ici à zéro puisque nous revenons au système d’exploitation.
En +12, nous trouvons l’adresse de la première variable d’environnement. Et il nous suffit donc de remonter toutes les adresses jusqu’à en trouver une à zéro pour afficher une à une les variables.
Dans la deuxième partie du programme, je place les différentes adresses des sections dans des registres pour regarder leur emplacement.
Et enfin, nous effectuons une recherche d’une variable pour afficher son contenu. Sur mon raspberry, la variable USER contient pi. Pour démarrer la recherche à la bonne adresse quelque soit le nombre de paramètres passés dans la ligne de commande, l’indice de recherche est initialisé avec le nombre de paramètre + 2.

Le programme varEnv64.s effectue la même chose en 64 bits, sauf que les adresses sont sur 8 octets et non plus sur 4 octets.
Peut être vous posez vous la question du pourquoi du save du registre x1 et lr en début de la routine de recherche alors que ces 2 registres ne sont pas modifiés dans la routine. Et bien c’est pour les avoir toujours en exemple et pour éviter une anomalie si je suis obligé d’ajouter une routine d’affichage en cas de problème. Vous remarquerez que dans cette routine j’utilise les registres r12 etc donc il n’est pas nécessaire de les sauver (cf norme d’appel des routines).

dimanche 22 mars 2020

Chapitre 84 : assembleur ARM64 : système multi-agents : application à un banc de poisson


Pour ce chapitre je repars encore du livre de Virginie MATHIVET « l’intelligence Artificielle pour les développeurs en Java ».
L’adaptation en assembleur ARM pose quelques problèmes intéressants puisqu’il faut adapter toute la partie graphique pour la porter sous X11. De plus, il faut aussi réaliser une animation pour déplacer le banc de poisson dans son océan.
Source du programme mAgent3.s 

Les calculs de vitesse suivant les axes X et Y font intervenir des cosinus et sinus et une racine carrée, ce qui nous oblige à trouver et à écrire les algorithmes nécessaires.
En ce qui concerne la partie graphique, je suis reparti du programme écrit au chapitre XX pour l’afficheur des images PNG. Je ne garde que les routines de connexion, de  chargement des couleurs, polices et la création de la fenêtre principale, des contextes graphiques et de la gestion des événements.

Pour le calcul du cosinus et sinus, je trouve l’algorithme CORDIC que j’adapte pour l’assembleur ARM 64 bits. Cela nécessite l’utilisation des instructions de traitement des nombres en virgule flottante et la création dans la .data d’une table de valeurs servant au calcul.
Pour la racine carré, le calcul s’effectue en nombre entier avec un algorithme que j’avais déjà écrit pour le 32 bits.

Ah oui, vous remarquerez que chaque routine comporte le commentaire INFO : avec le nom de la routine. J’ai besoin de cela car j’utilise un plugin de notepad++ qui me permet pour les gros programmes d’avoir une liste des routines dans une fenêtre séparée, ce qui facilite le déplacement d’une routine à l’autre.

Après ces routines de base, nous trouvons les routines d’affichage d’un poisson et d’un obstacle. Comme le poisson se déplace il faut aussi écrire une routine identique pour effacer son ancienne position en le redessinant mais avec la même couleur que le fond bleu, ce qui le fait disparaitre. Son dessin ne consiste qu’en une simple ligne blanche.
Pour les obstacles, le dessin est un cercle mais curieusement X11 ne propose pas de routine de dessin d’un cercle complet mais seulement des arcs. Pour avoir un cercle complet il faut indiquer le même rayon sur l’axe des X et des Y puis indiquer l’angle de départ et celui d’arrivée en degré multiplié par 64.
Dans l’appel à XDrawArc, il est nécessaire de passer 8 paramètres et donc la nome d’appel nous oblige à passer ce 8ième paramètre sur la pile (et toujours alignée sur 16 octets). Et dans ce cas, il ne faut pas oublier de réaligner la pile après l’appel.

Pour les routines de calcul d’évitement des murs, des poissons des obstacles etc. j’adapte celles données dans le livre.  Il faut faire attention aux instructions car les vitesses peuvent être négatives ou positives. En particulier si on utilise l'instruction lsr pour diviser par une puissance de 2 il faut la remplacer par l'instruction asr qui gérera correctement la division pour des valeurs négatives.
L’autre difficulté est de bien gérer l’utilisation des registres en n’oubliant pas de les sauvegarder.
J’ai galéré un moment avec les nombres float car pour vérifier les calculs j’utilisai l’instruction printf du C et celle ci utilise des registres utilisés dans mes routines sans les sauvegarder, ce qui engendrait de nouvelles erreurs.

Enfin l’animation est effectuée dans la routine gestionAttenteEvenements qui utilise l’appel système select pour attendre soit un évènement qui survient sur la connexion X11 soit un top du timer. La valeur d’attente est passée en paramètre sous la forme d’une structure contenant le délai en secondes et macro secondes. Puis nous appelons la fonction de gestion des évènements pour gérer la fermeture de la fenêtre et les clics de la souris pour créer les obstacles à la position du clic.
Il y a là aussi une curiosité : nous récupérons le FD de la connexion mais au lieu de stocker sa valeur dans la zone réservée, il faut mettre à jour le bit correspondant au FD. Par exemple si le FD est égal à 3, il faut mettre le bit en position 4 à 1 (les positions commençant à 0). Et passer la valeur 4 dans le premier paramètre d’appel à Select.

Après les tests et les corrections des erreurs, le programme fonctionne mais les poissons ont un comportement bizarre. Au bout d’un moment, ils se rassemblent presque tout dans le coin inférieur droit puis se déplacent à nouveau dans tout l’aquarium : la réserve de nourriture peut être !!!!. Certains poissons restent aussi collés aux parois pendant un certain temps puis repartent !!!
Je ne suis pas arrivé à trouver une erreur dans la programmation et j’en suis arrivé à penser que le comportement des  agents peut aboutir à des effets non prévus.
Attention : le spectacle du mouvement des poissons est hypnotique !!!

lundi 16 mars 2020

Chapitre 83: assembleur ARM64 : : Méta heuristiques d’optimisation problème du sac à dos.


Pour ce chapitre je vais de nouveau repartir du livre de Virginie MATHIVET « L’Intelligence Artificielle pour les développeurs »  en suivant le chapitre sur les algorithmes de méta heuristiques d’optimisation écrit pour le langage Java.

Les algorithmes développés dans ce chapitre sont : l’algorithme glouton, la descente de gradient, la recherche taboue, le recuit simulé et l’optimisation par essaims particulaires. Pour la théorie, je vous laisse aller voir le livre ci-dessus ou wikipedia ou d’autres sites sur internet. Dans le livre, l 'application de ces algorithmes s’effectue sur le célèbre problème du sac à dos, qui consiste à remplir un sac à dos de 20kg avec diverses boites de poids différents et d’optimiser la valeur totale du contenu.

Dans notre programme sacados64.s, nous allons cette fois ci, utiliser les listes chainées pour générer les diverses solutions.
J’ai essayé dans ce programme, d’améliorer l’utilisation des registres en respectant la norme préconisée. Mais il reste encore des points à revoir !!
Au début du programme nous décrivons 2 structures, une pour les boites et une pour la liste chainée de chaque solution. La structure boite contient le pointeur vers le nom de la boite, le poids et sa valeur. La liste chainée contient ses 3 éléments plus un pointeur sur le nœud suivant et un ratio égal à la valeur divisée par le poids. Pour éviter des calculs en virgule flottante ce ratio est calculé en multipliant en premier la valeur par 100. La liste chainée possède un nœud d’entête qui contiendra la somme de tous les poids des nœuds de la chaine, la valeur totale et le nombre de nœuds (ce dernier utilise la place du pointeur nom de la boite puisqu’il est inutile au niveau de l’entête).
La description des boites possibles est effectuée dans une table dont la modification est plus aisée. La création du problème consistera à transformer cette table en une liste chainée de toutes les boites disponibles, liste qui sera exploitée par tous les algorithmes.  Au niveau du code, nous retrouvons les routines vues au chapitre sur les listes chainées : creerNoeud, insererNoeud, supprimerNoeud, et triListe.
Cette dernière effectue un tri sur le ratio et en ordre inverse, la boite au plus fort ratio est triée en premier.
Nous rajoutons à ces fonctions, des fonctions de copie de liste, de copie de nœuds et les fonctions spécifiques à certains algorithmes.

Tout en respectant la logique des algorithmes tels que décrit dans le livre, J’ai simplifié certains calculs. Par exemple pour le recuit simulé il faut calculer une exponentielle pour déterminer la probabilité de maintenir une solution moins bonne. Après avoir fait des simulations avec Excel, et si je n’ai pas fait erreur le simple fait de diminuer la température de 1 à partir de 50 convenait parfaitement. Bien sûr si vous voulez utiliser cet algorithme pour d’autres types de problème, il faudra l’adapter pour que le calcul soit réaliste !!
Après de nombreux tests, le programme semble fonctionner correctement du moins avec les valeurs données dans le livre. Mais j’ai dû modifier le générateur de nombres aléatoires qui figure dans les routines asm64 car pour de petites valeurs il ne semble pas aléatoire du tout. J’ai donc utilisé une possibilité du système d 'exploitation qui fournit un appel system (urandom). Celui-ci fournit une suite de nombres aléatoires de la longueur que l’on désire. Mais avec l’inconvénient que les tirages sont différents à chaque lancement.
Voici un exemple de résultat :
SolutionEssaims
Valeur totale = 54 poids total = 19 nb de boites : 5
Liste des boites :
I ==> poids = 5 valeur = 12 ratio = 240
A ==> poids = 4 valeur = 15 ratio = 375
L ==> poids = 3 valeur = 7 ratio = 233
D ==> poids = 3 valeur = 10 ratio = 333
K ==> poids = 4 valeur = 10 ratio = 250

Remarque : si on supprime la dernière boite de la liste des boites disponibles  (L) l’algorithme glouton ne trouve pas la bonne solution !!

jeudi 12 mars 2020

Chapitre 82 : assembleur ARM64 : algorithme génétique appliqué au problème du voyageur de commerce


Pour ce chapitre je vais repartir du livre de Virginie MATHIVET « L’Intelligence Artificielle pour les développeurs »  en suivant le chapitre sur les algorithmes génétiques écrit pour le langage Java. Je pensais utiliser les listes chainées vues au chapitre précédent mais ici cela semble inutile car les données population et génome sont fixes. Il est donc plus performant d’utiliser des tables.

Comme dans le livre, nous allons utiliser ce type d’algorithmes pour résoudre le problème du voyageur de commerce pour 7 villes (PVC). Pour la théorie sur ce type d’algorithme, je vous renvoie au livre ci-dessus ou sur Wikipedia ou autres sites.
Dans le programme genPVS64.s nous commençons par décrire 2 structures de données nécessaires : individu et processus évolutionnaire puis les tables contenant les noms de ville, les pointeurs vers ces noms et une table contenant les distances entre toutes les villes. Ces tables peuvent être complétées ou modifiées sans difficulté . Vous remarquerez la façon de calculer le nombre de postes ce qui évite d’avoir une divergence entre données.
Seule la table population a sa place réservée dans la section BSS. La table génome associée à chaque individu sera créee sur le tas lorsque cela sera nécessaire.

Le code reprend les différentes routines pour traiter le problème. Les commentaires sont suffisamment nombreux pour la compréhension des instructions.
La difficulté d’écriture de longs programmes en assembleur provient de la bonne utilisation des registres. Normalement la convention est de séparer les registres en 3 parties : les  paramètres, les intermédiaires et les permanents. Seuls ces derniers doivent être sauvegardés. Mais cette norme est surtout valable si le programme fait appel à des librairies extérieures. Sur un programme personnel, sans contraintes d’optimisation, il faut surtout veiller à la bonne sauvegarde restauration des registres contenant des données générales. Et hélas j’ai pris de mauvaises habitudes en commençant par utiliser les registres x0,x1 x2 .. au lieu d’utiliser les registres x20, x21 etc.
Il faut aussi veiller à ce que chaque routine ne dépasse pas une taille d’écran afin qu'elle soit lisible en totalité sans avoir à utiliser l’ascenseur.
Enfin vous remarquerez que chaque nom de routine est suivie du mot clé INFO :. Cela me permet dans notepad++ d’utiliser un pluggin qui affiche toutes les routines et facilite leur accès.
Remarque : je n’ai pas encore poussé ce programme avec un nombre de villes plus important pour analyser les résultat.
Merci de me signaler les erreurs éventuelles.

Quelques jours après : j'ai testé de nouveau ce programme avec les 13 préfectures de la région Occitanie. J'ai seulement modifié le générateur de nombres aléatoires pour que ceux ci soient toujours différents après plusieurs lancements du programme.
Le résultat semble satisfaisant car il trouve ce chemin après modification des paramètres :

(+1257)Auch Toulouse Montauban Cahors Albi Rodez Mende Nimes Montpellier Perpignan Carcassonne Foix Tarbes

lundi 2 mars 2020

Chapitre 81 : assembleur ARM 64 : routines pour listes chainées


Après le précédent chapitre sur la gestion de tables, nous allons voir quelques routines pour gérer des listes chainées. Une liste chainée contient un certain nombre de nœuds contenant une ou plusieurs valeurs et un pointeur qui donne l’adresse du nœud suivant. Le pointeur du dernier nœud est à zéro. L’adresse de début de la chaine pointe sur une structure identique à un nœud. Si ce premier pointeur est nul, c’est que la chaine est vide.
Dans le programme, routListCH1.s, nous commençons par décrire la structure de chaque nœud. Pour faire simple, nous décrivons qu’une valeur et un pointeur next.
Nous déclarons deux têtes de liste par :
qDebutListe1:       .skip llist_fin
qDebutListe2:       .skip llist_fin
puis dans le programme principal, nous testons les différentes routines.
L’initialisation d’une liste consiste à mettre les valeurs et le pointeur à zéro. Cette routine n’est pas utilisée ici car les têtes sont décrites dans la bss section et donc initialisées à zéro par le système.
Ensuite nous trouvons une routine de création d’un nœud, qui se contente d’allouer la place de la taille d’un nœud données par la structure et de stocker la valeur passée en paramètre et d’initialiser le pointeur vers le nœud suivant. La routine retourne l’adresse du nœud crée, adresse qui va nous servir dans la routine suivante d’insertion du nœud dans la chaine.
On passe à cette routine l’adresse du nœud à insérer et l’adresse du nœud après lequel il faut insérer ce nœud. Si c’est le premier nœud, on passe l’adresse de l’entête de liste. L insertion est très simple puisqu’il s’agit de mettre d’adresse du nœud dans le pointeur next du nœud précédent et de mettre l’adresse du nœud suivant dans le pointeur du nœud inséré !!!
Ensuite nous trouvons la routine d’affichage de toutes les valeurs de la liste. Nous passons à la routine l’adresse de l’entête de liste. Si le pointeur next est à zéro, nous affichons le message liste vide sinon nous passons au premier nœud pour afficher sa valeur et nous chargeons le nouveau pointeur. Nous bouclons sur ces instructions jusqu’à trouver un pointeur à zéro.
La recherche d’une valeur dans la liste consiste simplement à balayer la liste jusqu’à trouver la valeur correspondante. Mais si la valeur n’est pas trouvée, toute la liste est balayée ce qui peut être couteux en temps d’exécution : c’est le principal défaut des listes chainées par rapport aux tables.
Il est possible d’améliorer la recherche en triant la liste puis en arrêtant la recherche quans la clé recherchée est inférieure à une valeur de la liste.
La routine de tri est un peu particulière car elle détruit la liste initiale pour reconstruire une nouvelle liste avec une entête différente. Il faut donc après cette routine, utiliser l’adresse retournée dans le registre x0 pour toute action sur cette liste triée.
Nous trouvons ensuite une routine d’insertion dans une liste triée qui insère un nœud à la bonne place, une routine de copie de liste et une routine de suppression d’un nœud en fonction de sa valeur.
Nous verrons dans les chapitres suivants, une utilisation de ces routines.

Il existe aussi des listes doublement chainées qui permettent le balayage dans les 2 sens à l’aide d’un pointeur supplémentaire.
Le programme DblListCH.s  donne quelques routines de base en modifiant les structures de définition de la liste et des nœuds.
Nous créons une structure de liste qui contient 2 pointeurs, un qui pointe sur le premier nœud de la liste (ou est nul si la liste est vide) et l’autre qui pointe sur le dernier nœud de la liste (ou qui est nul si la liste est vide).
Et nous ajoutons à la structure du nœud un pointeur prev qui pointera sur le nœud précédent (ou qui est nul s’il s’agit du premier nœud).
Ensuite nous trouvons 3 routines : une qui insère un nœud au début de liste, une qui insère en fin de liste et la dernière qui insère une valeur après une autre. Ces 3 routines nécessitent la manipulation des pointeurs pour assurer un bon fonctionnement de la liste.
Puis nous avons une routine de recherche de valeur dans la liste et 2 routines d’affichage de la liste complète ,une dans un sens et l’autre dans l’autre sens.
Comme exercice, je vous laisse le soin d’écrire la suppression d’un nœud et l’insertion avant une certaine valeur.