Je n’ai toujours pas trouvé de solution au problème du
clavier relié au port USB du Raspberry. Donc en attendant, nous allons voir
comment exécuter un programme externe à partir de notre petit OS !! Pour
cela nous créons un programme pgm1.s qui n’effectuera que l’envoi d’un message
par l’Uart. Je laisse en plus quelques modules utilitaires pour vérification.
Le programme est compilé et linké pour fournir un exécutable de type ELF32
(puisque mon raspberry est en 32 bits). Je n’ai pas regardé dans ce premier
cas, toutes les contraintes à respecter et donc la seule est de linker les
modules avec l’option –r qui va créer les données nécessaires à la <relocalisation>
de l’exécutable. En effet, nous n’allons pas charger cet exécutable aux
adresses définies par le linker (par exemple 8000h) mais à nos propres adresses
et donc il est nécessaire de recalculer un certain nombre d’instructions pour
avoir une exécution correcte.
L’exécutable (pgm1.elf) ainsi crée sera stocké dans le
répertoire principal de la carte SD (avec le kernel.img) car lors d’un chapitre
précédent sur la carte SD, je n’ai pas écrit de fonction pour lire un fichier
dans des sous-répertoires de la carte SD (il faut bien que je vous laisse un
peu de travail !!).
Vous remarquerez aussi que ce petit programme utilise les
modules développes précédemment et donc va stocker en mémoire des instructions
qui y figurent déjà. Il serait préférable de développer un mécanisme pour
utiliser les fonctions déjà écrites et chargées dans le kernel.img. On verra
peut être cela un autre jour !!!
L’utilitaire readelf (disponible sous raspbian ou windows)
permet de visualiser les entités du programme. Voici par exemple le début des
données de relocalisation :
Nous pouvons y voir que la fonction uart_send_string qui se
trouve à l’emplacement 0x98c est utilisé au déplacement 0x140 du programme et
que cette table commence à l’adresse 0x2ab4 et quelle contient 91 symboles qui seront
à relocaliser.
Revenons au module kernel.s de notre OS. Nous y ajoutons une
commande exec pour lancer l’exécution de notre petit programme externe
(lancement par exec pgm1.elf) puis nous créerons un nouveau module execPgm.s
qui contiendra les fonctions nécessaires à cette exécution.
Dans ce module, nous commençons à lire toutes les données du
fichier contenant le programme à executer. Comme nous ne connaissons pas la
taille, nous chargeons les données sur le tas. Le tas est la zone mémoire
comprise entre la fin de la section bss et la pile et qui est complétement
libre. Pour cela nous gérons une adresse du tas que nous ferons évoluer à
chaque réservation de place nécessaire. Nous conservons l’adresse de début de
stockage dans le registre r10 car cette adresse va servir de référence pour
traiter toutes les donneés d’un programme de type ELF32.
Ensuite nous vérifions que les données lues correspondent à
un format ELF, relogeable et pour un processeur ARM en récupérant les données
dans l’entête du fichier grâce à la description d’une structure. Si le fichier
est valide, nous récupérons aussi la valeur du point d’entrée du programme et
les décalages des différentes tables (voir sur Internet la description d’un
programme ELF32) à savoir la table des entêtes sections, la table des noms de
section etc.
Puis nous allons analyser la table des entêtes sections pour
identifier chaque section du programme. Pour certaines nous nous contentions de
mettre à jour le décalage pour les retrouver et pour d’autres nous les
recopions sur le tas (.text, .data). En effet, ces sections doivent être
alignées sur des frontières particulières (multiple de 2 donnée par sectheaderElf_addralign).
Bien entendu nous stockons ces nouvelles adresses dans la structure générale du
programme.
Maintenant, il faut analyser la table des relocations pour
recalculer les adresses de symboles en fonction des nouvelles adresses des
sections .data et .text. Pour chaque
emplacement (décalage par rapport au début de la section) il faut retrouver le
déplacement de la position du symbole pour remplacer son adresse. Suivant le
type de relocation, il faut effectuer des opérations plus ou moins compliquées.
Dans la documentation ARM nous retrouvons pour chaque type l’opération à
effectuer. Par exemple pour le type R_ARM_ABS32 nous trouvons l’opération (S
+A) | T, ce qui signifie ajouter la nouvelle valeur à l’ancienne et nous ne
tenons pas compte de la valeur de T qui correspond à l’exécution d’instructions
en thumb ce qui n’est jamais le cas ici.
Pour le type R_ARM_CALL c’est une opération plus compliquée
et pour les autres types, je n’ai pas eu à les programmer car sur mes petits
programmes je n’ai pas trouvé de cas. Si dans un programme, il y a un cas non
traité, un message d’anomalie est affiché avec le code concerné et il faudra
donc rechercher l’opération à effectuer et la programmer.
Dans ce programme, il y a un petit problème, car le résultat
de la dernière opération est décalé de 4 octets et donc j’ai du les enlever
avant d’exécuter le programme externe. Je n’ai pas trouvé l’origine de ce
décalage : incompréhension ou erreur de ma part ?
Si tout est ok, on peut lancer le programme en effectuant
simplement un saut au point d’entrée qui sera calculé.
Il faudrait améliorer le programme pour sauvegarder l’adresse
de la pile dans une zone mémoire fixe au
cas où le programme appelé retourne une adresse de pile fausse !! Et aussi
sauver et restaurer les registres si nécessaire.
Après l’exécution, nous remettons l’adresse du tas au début
pour permettre l’exécution d’un autre programme sans perte d’espace mémoire.
Et tout le projet est ici.