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

Aucun commentaire:

Enregistrer un commentaire