jeudi 9 mai 2019

le programme et ses instructions


Les instructions issues du langage d’assemblage et directement exécutables par le processeur sont stockées dans la section .text.  Pour notre assembleur arm, il s’agit d’instructions de type RISC d’une longueur de 32 bits (4 octets) quelle que soit la nature de l’instruction. Cela entraine quelques limitations.
Le détail de la composition de chaque instruction présente peu d’intérêt pour la programmation : code condition, code opération, bit maj état, codes des registres utilisés etc, qui utilisent une partie des 32 bits. Il ne reste donc que quelques bits pour le reste de l’instruction ce qui explique que pour stocker une valeur immédiate il ne reste plus que 12 bits (dont 8 bits pour la valeur et 4 bits pour indiquer un multiple).
Lors du lancement du programme, le système d’exploitation charge les différentes sections en mémoire de l’ordinateur suivant les directives établies par le linker et donc charge les instructions dans la section .text puis initialise un registre particulier avec la première instruction à exécuter.  Le registre est le 15ième registre et porte le nom de pc (program counter) et il contient toujours l’adresse de la prochaine instruction à exécuter (pas tout à fait en réalité car le processeur peut anticiper l’exécution : voir pour plus de précision….).

L’adresse de la première instruction est indiquée par la directive entry du linker. Il s’agit le plus souvent de l’adresse de la procédure principale du programme. Cette procédure doit donc être accessible par le système d’exploitation et doit être déclarée avec la pseudo instruction .globale

Les adresses du code qui doivent être utilisées pour des sauts doivent être déclarées par des labels comme  tache1 :  toto:  ou addition:  (avec les 2 points en fin).
Pour effectuer un saut à un label ou étiquette nous utilisons l’instruction b (branch en anglais) par exemple b tache1. Pour effectuer des sauts conditionnels après un test, il suffit d’ajouter les codes conditions comme déjà vus dans le chapitre un registre.
Par exemple :
Cmp r1,#0
Beq   egal          @ si r1= 0 saut au label egal
Bne pasegal     @ sinon saut au label pasegal
Et bien sûr le label peur être placé avant ce qui permet d’effectuer des boucles :
Mov r0,#0   @ init du compteur de boucle
Debut :               @ label de début de boucle
Add r0,#1      @ incrémentation du compteur
Cmp r0,#5       @ comparaison à la valeur 5
Ble debut        @ si plus petit ou égal ->boucle
….puis suite du programme
L’assembleur as autorise aussi des labels numériques locaux à une sous routines. On peut accéder au label précèdent en mettant un b (pour before) ou au label suivant par un f (pour forward). Exemple autre boucle :
Mov r0,#0
1 :  @ label début de boucle
Cmp r0,#5
Bgt 1f   @ saut au label 1 suivant si r0 supérieur à 5
Add r0,#1
B 1b       @ saut au label 1 précèdent
1 :   … suite du programme
Mais je déconseille l’utilisation du même N° car cela me parait peu lisible.

Appel d’une sous procédure :
Il s’effectue avec l’instruction bl label  (branch and link en anglais). A la rencontre de cette instruction, le processeur stocke l’adresse de l’instruction suivante dans le registre N°14 (lr pour Link return) et met l’adresse du label appelé dans le compteur de programme, ce qui permet le saut à l’instruction de la sous routine.
Le retour de la sous procédure s’effectue par l’instruction bx lr qui va mettre l’adresse contenu dans le registre lr dans le compteur de programme ce qui permet le retour à l’instruction qui suivait l’appel.
Ce mécanisme simple ne fait pas appel à la pile (comme dans le call d’autres assembleurs) et il faudra donc veiller à ce que l’adresse de retour contenue dans le registre lr ne soit pas perdue. Et c’est le cas quand une routine va elle-même appeler une sous-routine car le processeur va stocker dans lr l’adresse de retour de la sous-routine et écraser l’adresse de retour de la routine !! C’est pourquoi , vous verrez dans de nombreuses routines que la première instruction est push {lr] et l’avant dernière pop {lr) puis bx lr pour être sûr que l’adresse de retour n’a pas été perdue.
Pour passer des paramètres à la routine, nous pouvons utiliser des registres ou stocker des valeurs sur la pile. Comme souvent, les programmes assembleurs peuvent faire appel à des routines de la librairie du langageC , il est fréquent de respecter la norme de ce langage :
Les 4 premiers paramètres sont passés dans les registres r0 à r3, et si nécessaire les autres sont passés sur la pile. La valeur de retour est passée dans le registre r0 et éventuellement les autres valeurs dans les autres registres r1 à r3. Si la routine utilise les autres registres (r4-r12) en interne, elle doit les sauvegarder et restaurer leur valeur en fin.
Attention : les registres r4 à r11 sont bien sauvegardés par les routines du C mais le registre r12 pas toujours (car il peut servir pour des appels longs)
Voyons de plus près le mécanisme de passage des paramètres par la pile. Supposons que l’adresse de la pile contenue dans le registre sp soit 1000
Dans le programme appelant nous stockons les valeurs  r4 et r5 sur la pile par
Push {r4}   @ donc r4 est stockée à l’adresse 1000 puis la pile est décrémentée de 4 octets soit 996
Push {r5} @ donc r5 est stockée à l’adresse 996 et la pile est décrémentée de 4 octets soit 992.
Dans la sous-routine, nous commençons par sauvegarder lr ce qui décrémente  l’adresse de la pile de 4 octets soit 988 puis nous récupérons le deuxième paramètre  par ldr r5,[sp,#4] et le premier par ldr r4,[sp,#8]
Mais il est aussi possible de stocker l’adresse de la pile sp dans le registre fp (frame pointer) et d’utiliser cette adresse quelle que soit l’évolution de la pile dans la routine. Exemple :
Push {fp,lr}
add fp,sp,#8
Et donc le deuxième paramètre est récupéré par ldr r5,[fP] et le premier par ldr r4,[fp,#4]
Attention, si vous utilisez l’instruction push {r0,r1}, r1 est stocké sur la pile avant r0 !!
Il reste une dernière intervention à effectuer dans la routine (ou dans le programme appelant), c’est de remettre la pile à son état initial, et pour éviter de modifier des registres, nous ajoutons simplement le nombre d’octets utilisés par le ou les push. Ici il y a 2 push donc nous ajoutons l’instruction add sp,#8. Si vous ne le faites pas, votre programme ne fonctionnera plus en cas d’appel imbriqué de routines.

L’instruction bx peut aussi servir à l’appel d’une routine dont l’adresse a été stockée dans un registre.

Et maintenant je peux vous la vérité sur l'instruction de chargement de l'adresse d'une variable. Je vous avais dit que la seule instruction possible de lecture de la mémoire était l'instruction ldr rn,[rm] mais que l'on pouvait charger une adresse (toto) par l'instruction ldr rn,iAdrtoto avec iAdrtoto déclarée en fin de routine par l'instruction iAdrtoto:  .int toto . Mais iAdrtoto n'est pas de la forme [rm] donc hiatus !!!!
En fait, ldr rn,iAdrtoto est une pseudo instruction que le compilateur va remplacer par l'instruction ldr rn,[pc,#ecart]  pc étant le registre 15 (program counter) et #ecart, la différence entre l'adresse de l'instruction et l'adresse du label iAdrtoto, différence que le compilateur calcule très bien !!!

Aucun commentaire:

Enregistrer un commentaire