jeudi 9 mai 2019

Accès aux routines du système d'exploitation (LInux)


Comme vu précédemment pour accéder au clavier ou au disque dur ou à l’écran, il faudra passer par des routines du système d’exploitation. Avec linux, vous trouverez plus d’une centaine de possibilité d’appel de routines (voir la documentation sous  http://syscalls.kernelgrok.com/).
Pour cela nous utilisons l’instruction svc (ou swi identique) en passant les paramètres nécessaires dans les registres r0 à r6 et le code opération dans le registre r7.
Par exemple pour terminer correctement un programme, nous allons mettre le code retour dans le registre r0, le code opération 1 dans le registre r7 et terminer par l’instruction svc comme ceci :
Mov r0,#0     @ code retour OK
Mov r7,#1    @ code pour l’EXIT programme
Svc #0      @ appel Linux
Remarque : après le code svc, nous pouvons mettre une valeur immédiate mais qui n’est pas exploitable !!

Pour plus d'exemple sur les appels système, voir mon autre blog :

Les accès aux périphériques


En assembleur ARM, il n’y a pas d’instructions d’entrées-sorties pour accéder à des périphériques comme le clavier, le disque dur ou le GPIO. Ceux-ci sont considérés comme des zones de mémoires particulières et sont donc accédées par les instructions ldr ou str et uniquement que sur des entiers (donc 32 bits). Mais là aussi, le système d’exploitation ne laissera pas un utilisateur standard à accéder à ces zones. Il faudra soit passer par des appels systèmes soit utiliser des mécanismes comme le mapping mémoire ou autoriser un programme par la commande sudo.

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 !!!

mardi 7 mai 2019

La mémoire et ses instructions


La mémoire est composée de barrettes ram externes au processeur qui peuvent stocker des données. La plus petite unité d’allocation de la mémoire n’est pas le bit comme pour les registres mais l’octet (ou byte). Suivant le type d’ordinateur vous disposez de 250, 512 mégaoctets ou 1giga octets de mémoire. Mais attention vous ne pouvez pas disposer de toute celle-ci car le système d’exploitation et tous les autres programmes qui s’exécutent dans votre ordinateur ont aussi besoin de mémoire.
Chaque octet de la mémoire peut être accédé par une adresse codée sur 32 bits, donc adresse qui peut aller de 0 à 2 puissance 31 -1. Pour faciliter la lecture des adresses, l’affichage de leur valeur se fait en hexadécimal.
La mémoire dont dispose votre programme est divisée en section. Une section est une zone d’octets contigus avec des caractéristiques particulières : lecture seule, lecture-écriture, commune avec d’autres programmes etc.
L’emplacement de chaque section dans la mémoire est défini soit par défaut soit par le linker.
Les sections les plus courantes sont les suivantes :
La zone des données que vous allez initialiser et que vous allez déclarer avec la pseudo instruction .data.
La zone de données que le système d’exploitation va initialiser au lancement de votre programme et que vous déclarez avec la pseudo instruction .bss
La zone de mémoire qui contiendra le code de votre programmes cad les instructions assembleur exécutables et que vous déclarez avec la pseudo instruction ..text.
Une zone de mémoire située en fin de la mémoire qui vous est autorisée et qui s’appelle la pile.
Enfin la zone de mémoire comprise entre les 3 premières zones et la pile et qui s’appelle le tas !!
Il est possible de créer d’autres sections avec des caractéristiques particulières avec la pseudo instruction .section (voir la documentation).

Section .data
C’est dans cette zone que vous allez déclarer toutes les données qui ont une valeur avant l’exécution du programme. Bien sûr ces données peuvent être modifiées au cours de l’exécution par votre programme. Comme il n’est pas possible de connaitre l’adresse exacte de chaque donnée, nous allons utiliser un label pour nommer chacune d’elle. C’est le compilateur qui se chargera de transformer chaque label en une adresse exploitable par le processeur. Pour certains labels qui font référence à des données externes au programmes, c’est le linker qui effectuera la relation.

 Déclaration d’un octet :
bOctet1 :    .byte   10
bCar1 :    .byte ‘A’    @ les ‘’ servent de délimiteur pour le compilateur
Déclaration d’un demi mot :
hToto :  .hword   0xFFFF
Déclaration d’un entier (un mot soit 32 bits)
iTruc :            .int  1000
wTroc :   .word   0xFFFFFFFF
Remarque importante : les entiers doivent être alignés en mémoire sur une frontière de mots cad que leur adresse doit être divisible par 4. Pour cela nous disposons de la pseudo instruction .align 4 qui sera à mettre avant chaque déclaration d’entiers (et donc qu’il vaut mieux grouper).
Déclaration d’une table d’entiers :
tTable1 :    .int   5
                  .int 10
                  .int  12   etc
Vous remarquerez que le début de chaque label reprend l’initiale du type de données, ce n’est pas obligatoire mais cela facilite la lecture d’un programme.
Les valeurs peuvent être indiquées en binaire 0b1100 en décimal 12 en octal 014 (attention c'est le zéro au début du nombre qui indique que c'est un nombre en octal) en hexadécimal 0xC, en virgule flottante simple précision 0 E12,0 ou double précision 0F12,0. Elles peuvent être aussi déclarées sous forme de constante :
.equ  NBAGENTS,   100
iNbAgents :   .int   NBAGENTS

Les chaines de caractères (string) peuvent être déclarées de 2 façons :
Sans 0 final :
sChaine1 :                .ascii  « Bonjour »     @ les « «  servent de délimiteur
Avec 0 final (comme dans le langage C) :
szChaine2 :              .asciz « Bonjour le Monde.\n »
Nous pouvons mettre des caractères spéciaux comme le retour ligne \n. (voir la liste des caractères spéciaux ascii).
Une astuce : pour connaitre la longueur du chaine fixe, il est possible d’utiliser la pseudo instruction suivante (le . représente l’adresse mémoire courante) :
szChaine3 : .asciz « Toto »
.equ LGCHAINE3,    . – szChaine3  @ résultat  = 5 car il y a un 0 final

Enfin il est possible de déclarer des structures qui permettent de faciliter l’utilisation de groupe de données.
Exemple :
    .struct  0           @ début de la structure
Données1:            @ label 1
    .struct  Données1 + 4 @ longueur du label 1 en octets
Données2:         @ label 2
    .struct  Données2 + 4    @ longueur du label 2 en octets
FinDonnées :

Accès aux données en mémoire :
Il n’y a qu’une seule instruction ldr pour cela mais qui se décline de différentes façons. Il n’est possible d’accéder à une donnée que si son adresse est stockée dans un registre. Donc il faut mettre son adresse dans un registre par la pseudo instruction : ldr r1,=iTruc puis charger la valeur par l’instruction ldr r0,[r1] pour un entier.
Mais attention cette pseudo instruction ne fonctionne que pour des petits programmes !! Pour être tranquille il vaut mieux utiliser une autre méthode en déclarant à la fin de la routine un pointeur vers l’adresse de la donnée comme ceci :
iAdriTruc :  .int   iTruc
Puis il suffit de charger l’adresse par
Ldr r1,iAdriTruc
Et charger la donnée par :
Ldr r0,[r1]           @ chargement de 4 octets à partir de l’adresse contenue dans r1

Remarque : il est possible d’utiliser le même registre : ldr r1,[r1] mais s’il faut recharger la donnée il faut remettre son adresse dans r1.
Pour lire un octet de la mémoire, nous ajoutons un b (byte) au code instruction et h (half word) pour un demi mot :
Ldrb r0,[r1] et ldrh r0,[r1]

A partir de l’adresse contenue dans r1, nous pouvons accéder à des données situés à des emplacements en avant ou en arrière comme ceci
Ldr r0,[r1,#8]    @ chargement des 4 octets situés à l’adresse contenue dans r1 + 8 octets.
Ou ldr r0,[r1,#-32]  @ chargement de 4 octets à l’adresse contenue dans r1 – 32 octets
Et bien sûr nous pouvons remplacer la valeur par une constante :
Ldr r0,[r1,#NBDEPL]
Mais il ne faut pas exagérer, le maximum possible n’est que de 4096 octets !!
Et nous pouvons aussi utiliser un registre :
Mov r2,#8
Ldr r0,[r1,r2]  @ chargement de 4 octets à l’adresse contenue dans r1 + la valeur contenue dans r2.
Et bien sûr, nous pouvons faire appel au barrel shifter :
Ldr r1,iAdrtTable1
Mov r2,#2
Ldr r0,[r1,r2,lsl #2]  @ chargement de 4 octets à l’adresse contenue dans r1 + (valeur de r2 * 4) donc dans r0 il y aura 12.
Instruction idéale pour charger un entier en fonction de son rang !!
Il est possible de faire progresser le registre r1 d’une quantité pour balayer une table  avec :
Ldr r0,[r1],#4  @ chargement de 4 octets à l’adresse contenue dans r1 puis ajout de 4 octets à cette adresse.
Il est possible d’incrémenter r1 avant de récupérer la donnée par :
Ldr r0,[r1,#4] !  @ Incrémentation de l’adresse contenue dans r1 de 4 octets puis lecture de la donnée  
Avec cette instruction il est possible de charger une valeur dans un registre sans limitation de taille : par exemple
Ldr r1,iConstante1
Et en déclarant en fin de routine : iConstante1 : . int 123456
Il ne peut s’agir que d’une constante puisque stockée dans la section .text elle ne peut être modifiée.

Il est aussi possible d’effectuer plusieurs récupérations simultanées  dans plusieurs registres avec l’instruction :
Ldm r1,{r0,r2}     @ les 4 premiers octets de l’adresse contenus dans r1 sont mis dans r0, les 4 suivants dans r2
Et nous pouvons compléter l’instruction ldm avec les 4 codes suivants : IA pour la post _incrémentation, IB pour la pré-incrémentation, DA pour la post – décrémentation et DB pour la pré décrémentation (et il faut ajouter le symbole !) par exemple :
ldmia r1!,{r2-r3}  @ chargement dans r2 des 4 octets de l’adresse r1 puis chargement des 4 octets suivants dans r3 puis incrémentation de r1 de 4 + 4 = 8 octets.

Stockage des données dans la mémoire :
Pour cela nous utilisons l’instructions str avec les mêmes conventions que la lecture ldr.
Pour stocker un entier :
Ldr r1,iAdrValeur1  @ chargement de l’adresse mémoire de valeur1 dans r1
Mov r0,#10           @ valeur 10 dans r0
Str r0,[r1]      @ chargement de la valeur 10 à l’adresse mémoire de valeur 1
Pour stocker un octet dans la mémoire, nous ajoutons un b (byte) au code instruction et h (half word) pour un demi mot :
Pour stocker les valeurs de plusieurs registres nous avons :
Stm  r1,{r0,r2,r3}  par exemple.

La section bss (Block Started by Symbol) :
Cette section contient les données qui seront mises à zéro par le système d’exploitation avant l’exécution de votre programme. Il est donc inutile d’initialiser des valeurs dans cette section. Vous y déclarerez toutes vos variables intermédiaires en réservant uniquement la place par la pseudo instruction .skip
Par exemple réservation d’un entier   iToto : .skip 4   @ réserve 4 octets
Ou sBuffer :  .skip 500    @ réserve 500 octets pour un buffer
Ou tTableEntier :  .skip  4 * NBENTIERS
Ces 3 zones seront remplies de zéros binaires avant l’exécution de votre programme.
L’accès et le stockage des données s’effectuent avec les mêmes instructions que pour la .data (voir ci-dessus).

La pile :
Il s’agit d’une région de la mémoire gérée de manière particulière à l’aide d’un registre spécial en fait le 13ième qui s’appelle r13 ou sp ou registre de pile. En général ce registre est décrémenté à chaque stockage de valeur d’ un registre soit 4 octets. C’est pourquoi la pile est située en fin de la mémoire allouée à votre programme. Vous n’avez pas (sauf cas très particulier) à  modifier l’adresse de la pile ni à déclarer de valeurs. Mais on ne peut stocker sur la pile que le contenu des registres (donc toujours 4 octets).
Pour stocker la valeur des registres sur la pile, l’instruction est :
Stmfd  sp !,{r1,r2,r3}
Et pour récupérer les données :

Ldmfd sp !,{r1,r2,r3}
Vous devez récupérer autant de données que vous avez stockées.
Pour simplifier ces instructions ( et pour s’aligner sur d’autres assembleurs), vous avez aussi les pseudo instructions :
Push {r1,r2}  et pop {r1,r2}
Ces instructions servent à sauvegarder la valeur des registres en début de sous-routines ou lorsque vous avez besoin de plus de 12 registres dans une routine. Elles servent aussi à passer des paramètres à une sous-routine (voir le chapitre instructions)
Une autre utilisation de la pile est de stocker des valeurs locales à une routine qui peut être appelée de manière récursive (voir par exemple le calcul d’une factorielle). Mais dans ce cas, il est bon de garder l’adresse du début de la pile dans le registre r11. C’est pourquoi vous le trouverez dans la documentation appelé fp pour Frame Pointer. Mais il me semble que ces utilisations sont moins fréquentes que dans l’assembleur X86 des processeurs Intel.

Le tas :
C’est une zone mémoire indifférenciée dont vous gérez la totalité à l’aide de pointeurs. Elle est utile lorsque vous ne connaissez pas à l’avance la taille des données dont vous avez besoin (par exemple un buffer de caractères contenant les données d’un fichier).