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