Chargement/entreposage multiple
Il est parfois plus efficace de charger (ou d'entreposer) plusieurs valeurs simultanément. Pour cela, nous utilisons les instructions LDM (load multiple) et STM (store multiple). Ces instructions présentent des variantes qui ne diffèrent que par le mode d'accès à l'adresse initiale. C'est le code que nous utiliserons dans cette section. Nous allons détailler chaque instruction étape par étape :
- .data
-
- tampon_tableau:
- .word 0x00000000 @ tampon_tableau[0]
- .word 0x00000000 @ tampon_tableau[1]
- .word 0x00000000 @ tampon_tableau[2]. Cet élément a une adresse relative de tampon_tableau+8
- .word 0x00000000 @ tampon_tableau[3]
- .word 0x00000000 @ tampon_tableau[4]
-
- .text
- .global _start
-
- _start:
- adr r0, words+12 @ adresse des words[3] -> r0
- ldr r1, array_buff_bridge @ adresse de tampon_tableau[0] -> r1
- ldr r2, array_buff_bridge+4 @ adresse de tampon_tableau[2] -> r2
- ldm r0, {r4,r5} @ words[3] -> r4 = 0x03; words[4] -> r5 = 0x04
- stm r1, {r4,r5} @ r4 -> tampon_tableau[0] = 0x03; r5 -> tampon_tableau[1] = 0x04
- ldmia r0, {r4-r6} @ words[3] -> r4 = 0x03, words[4] -> r5 = 0x04; words[5] -> r6 = 0x05;
- stmia r1, {r4-r6} @ r4 -> tampon_tableau[0] = 0x03; r5 -> tampon_tableau[1] = 0x04; r6 -> tampon_tableau[2] = 0x05
- ldmib r0, {r4-r6} @ words[4] -> r4 = 0x04; words[5] -> r5 = 0x05; words[6] -> r6 = 0x06
- stmib r1, {r4-r6} @ r4 -> tampon_tableau[1] = 0x04; r5 -> tampon_tableau[2] = 0x05; r6 -> tampon_tableau[3] = 0x06
- ldmda r0, {r4-r6} @ words[3] -> r6 = 0x03; words[2] -> r5 = 0x02; words[1] -> r4 = 0x01
- ldmdb r0, {r4-r6} @ words[2] -> r6 = 0x02; words[1] -> r5 = 0x01; words[0] -> r4 = 0x00
- stmda r2, {r4-r6} @ r6 -> tampon_tableau[2] = 0x02; r5 -> tampon_tableau[1] = 0x01; r4 -> tampon_tableau[0] = 0x00
- stmdb r2, {r4-r5} @ r5 -> tampon_tableau[1] = 0x01; r4 -> tampon_tableau[0] = 0x00;
- bx lr
-
- words:
- .word 0x00000000 @ words[0]
- .word 0x00000001 @ words[1]
- .word 0x00000002 @ words[2]
- .word 0x00000003 @ words[3]
- .word 0x00000004 @ words[4]
- .word 0x00000005 @ words[5]
- .word 0x00000006 @ words[6]
-
- array_buff_bridge:
- .word tampon_tableau @ adresse de tampon_tableau, ou en d'autres termes - tampon_tableau[0]
- .word tampon_tableau+8 @ adresse de tampon_tableau[2]
Avant de commencer, gardez à l'esprit qu'un .word désigne un bloc de données (mémoire) de 32 bits = 4 octets. Ceci est important pour comprendre le décalage. Le programme se compose donc d'une section .data où nous allouons un tableau vide (tampon_tableau) de 5 éléments. Nous l'utiliserons comme emplacement mémoire inscriptible pour entreposer les données. La section .text contient notre code avec les instructions d'opérations mémoire et un bassin de données en lecture seule contenant deux étiquettes : une pour un tableau de 7 éléments, une autre pour « relier » les sections .text et .data afin d'accéder au tampon_tableau de la section .data.
- adr r0, words+12 @ adresse de words[3] -> r0
Nous utilisons l'instruction ADR (approche paresseuse) pour obtenir l'adresse du 4e élément (mots[3]) dans R0. Nous pointons vers le milieu du tableau de mots, car nous allons opérer en avant et en arrière à partir de là :
gef> break _start gef> run gef> nexti |
R0 contient désormais l'adresse du mot [3], étant ici 0x80B8. Notre tableau commence donc à l'adresse du mot [0] : 0x80AC (0x80B8 - 0xC).
gef> x/7w 0x00080AC 0x80ac <words>: 0x00000000 0x00000001 0x00000002 0x00000003 0x80bc <words+16>: 0x00000004 0x00000005 0x00000006 |
Nous préparons R1 et R2 avec les adresses des premier (tampon_tableau[0]) et troisième (tampon_tableau[2]) éléments du tableau tampon_tableau. Une fois les adresses obtenues, nous pouvons commencer à les exploiter.
- ldr r1, tampon_tableau_pont @ adresse de tampon_tableau[0] -> r1
- ldr r2, tampon_tableau_pont+4 @ adresse de tampon_tableau[2] -> r2
Après avoir exécuté les deux instructions ci-dessus, R1 et R2 contiennent les adresses de tampon_tableau[0] et tampon_tableau[2].
gef> info register r1 r2 r1 0x100d0 65744 r2 0x100d8 65752 |
L'instruction suivante utilise LDM pour charger deux valeurs de mots depuis la mémoire pointée par R0. Ainsi, comme R0 a pointé vers l'élément words[3] précédemment, la valeur words[3] est transférée vers R4 et la valeur words[4] vers R5.
- ldm r0, {r4,r5} @ words[3] -> r4 = 0x03; words[4] -> r5 = 0x04
Nous avons chargé plusieurs blocs (2 blocs de données) avec une seule commande, ayant défini R4 = 0x00000003 et R5 = 0x00000004.
gef> info registers r4 r5 r4 0x3 3 r5 0x4 4 |
Exécutons maintenant l'instruction STM pour stocker plusieurs valeurs en mémoire. L'instruction STM de notre code prend les valeurs (0x3 et 0x4) des registres R4 et R5 et les stocke dans un emplacement mémoire spécifié par R1. Nous avons précédemment configuré R1 pour qu'il pointe vers le premier élément tampon_tableau. Ainsi, après cette opération, tampon_tableau[0] = 0x00000003 et tampon_tableau[1] = 0x00000004. Sauf indication contraire, LDM et STM fonctionnent par pas d'un mot (32 bits = 4 octets).
- stm r1, {r4,r5} @ r4 -> tampon_tableau[0] = 0x03; r5 -> tampon_tableau[1] = 0x04
Les valeurs 0x3 et 0x4 doivent désormais être stockées aux adresses mémoire 0x100D0 et 0x100D4. L'instruction suivante inspecte deux mots mémoire à l'adresse 0x000100D0.
gef> x/2w 0x000100D0 0x100d0 <tampon_tableau>: 0x3 0x4 |
Comme mentionné précédemment, LDM et STM présentent des variantes. Le type de variante est défini par le suffixe de l'instruction. Les suffixes utilisés dans l'exemple sont : -IA (augmentation après), -IB (augmentation avant), -DA (diminution après), -DB (diminution avant). Ces variantes diffèrent par la manière dont elles accèdent à la mémoire spécifiée par le premier opérande (le registre stockant l'adresse source ou de destination). En pratique, LDM est identique à LDMIA, ce qui signifie que l'adresse de l'élément suivant à charger est incrémentée après chaque chargement. On obtient ainsi un chargement séquentiel (vers l'avant) des données à partir de l'adresse mémoire spécifiée par le premier opérande (registre stockant l'adresse source) :
- ldmia r0, {r4-r6} @ words[3] -> r4 = 0x03, words[4] -> r5 = 0x04; words[5] -> r6 = 0x05;
- stmia r1, {r4-r6} @ r4 -> tampon_tableau[0] = 0x03; r5 -> tampon_tableau[1] = 0x04; r6 -> tampon_tableau[2] = 0x05
Après avoir exécuté les deux instructions ci-dessus, les registres R4-R6 et les adresses mémoire 0x000100D0, 0x000100D4 et 0x000100D8 contiennent les valeurs 0x3, 0x4 et 0x5.
gef> info registers r4 r5 r6 r4 0x3 3 r5 0x4 4 r6 0x5 5 gef> x/3w 0x000100D0 0x100d0 <tampon_tableau>: 0x00000003 0x00000004 0x00000005 |
L'instruction LDMIB augmente d'abord l'adresse source de 4 octets (valeur d'un mot), puis effectue le premier chargement. De cette manière, le chargement des données est toujours séquentiel (vers l'avant), mais le premier élément est décalé de 4 octets par rapport à l'adresse source. C'est pourquoi, dans notre exemple, le premier élément chargé de la mémoire vers R4 par l'instruction LDMIB est 0x00000004 (mots[4]) et non 0x00000003 (mots[3]) comme indiqué par R0.
- ldmib r0, {r4-r6} @ words[4] -> r4 = 0x04; words[5] -> r5 = 0x05; words[6] -> r6 = 0x06
- stmib r1, {r4-r6} @ r4 -> tampon_tableau[1] = 0x04; r5 -> tampon_tableau[2] = 0x05; r6 -> tampon_tableau[3] = 0x06
Après avoir exécuté les deux instructions ci-dessus, les registres R4-R6 et les adresses mémoire 0x100D4, 0x100D8 et 0x100DC contiennent les valeurs 0x4, 0x5 et 0x6.
gef> x/3w 0x100D4 0x100d4 <tampon_tableau+4>: 0x00000004 0x00000005 0x00000006 gef> info register r4 r5 r6 r4 0x4 4 r5 0x5 5 r6 0x6 6 |
Lorsque nous utilisons l'instruction LDMDA, tout fonctionne à l'envers. R0 pointe vers les mots [3]. Au début du chargement, nous reculons et chargeons les mots [3], [2] et [1] dans R6, R5 et R4. Les registres sont également chargés à l'envers. Ainsi, une fois l'instruction terminée, R6 = 0x00000003, R5 = 0x00000002, R4 = 0x00000001. La logique ici est que nous reculons car nous décrémentons l'adresse source APRÈS chaque chargement. Le chargement du registre à l'envers se produit car à chaque chargement, nous décrémentons l'adresse mémoire et donc le numéro de registre afin de respecter la logique selon laquelle les adresses mémoire supérieures correspondent à des numéros de registre supérieurs. Prenons l'exemple de LDMIA (ou LDM) : nous avons d'abord chargé le registre inférieur car l'adresse source était inférieure, puis le registre supérieur car l'adresse source avait augmenté.
Chargement multiple, décrémentation après :
- ldmda r0, {r4-r6} @ words[3] -> r6 = 0x03; words[2] -> r5 = 0x02; words[1] -> r4 = 0x01
Registres R4, R5 et R6 après exécution :
gef> info register r4 r5 r6 r4 0x1 1 r5 0x2 2 r6 0x3 3 |
Charger plusieurs, décrémenter avant :
- ldmdb r0, {r4-r6} @ words[2] -> r6 = 0x02; words[1] -> r5 = 0x01; words[0] -> r4 = 0x00
Registres R4, R5 et R6 après exécution :
gef> info register r4 r5 r6 r4 0x0 0 r5 0x1 1 r6 0x2 2 |
Entreposer plusieurs valeurs, puis décrémenter après :
- stmda r2, {r4-r6} @ r6 -> tampon_tableau[2] = 0x02; r5 -> tampon_tableau[1] = 0x01; r4 -> tampon_tableau[0] = 0x00
Adresses mémoire de tampon_tableau[2], tampon_tableau[1] et tampon_tableau[0] après exécution :
gef> x/3w 0x100D0 0x100d0 <tampon_tableau>: 0x00000000 0x00000001 0x00000002 |
Entreposer plusieurs, décrémenter avant :
- stmdb r2, {r4-r5} @ r5 -> tampon_tableau[1] = 0x01; r4 -> tampon_tableau[0] = 0x00;
Adresses mémoire de tampon_tableau[1] et tampon_tableau[0] après exécution :
gef> x/2w 0x100D0 0x100d0 <tampon_tableau>: 0x00000000 0x00000001 |
PUSH et POP
Le processus comporte un emplacement mémoire appelé Pile. Le pointeur de pile (SP) est un registre qui, en temps normal, pointe toujours vers une adresse dans la région mémoire de la pile. Les applications utilisent souvent la pile pour le stockage temporaire de données. Comme mentionné précédemment, ARM utilise un modèle de chargement/stockage pour l'accès mémoire, ce qui signifie que les instructions LDR / STR ou leurs dérivées (LDM.. /STM..) sont utilisées pour les opérations mémoire. En x86, on utilise PUSH et POP pour charger et stocker depuis et vers la pile. En ARM, on peut également utiliser ces deux instructions :
Lorsque l'on pousse un élément sur la pile descendante complète, voici ce qui se produit7nbsp;:
- Tout d'abord, l'adresse dans SP est diminuée de 4.
- Ensuite, les informations sont entreposées à la nouvelle adresse pointée par SP.
Lorsque nous extrayons un élément de la pile, voici ce qui se produit :
- La valeur de l'adresse SP actuelle est chargée dans un registre spécifique.
- L'adresse SP est augmentée de 4.
Dans l'exemple suivant, nous utilisons à la fois PUSH/POP et LDMIA/STMDB :
Regardons le désassemblage de ce code :
gladir@labs:~$ as pushpop.s -o pushpop.o gladir@labs:~$ ld pushpop.o -o pushpop gladir@labs:~$ objdump -D pushpop pushpop: file format elf32-littlearm Disassembly of section .text: 00008054 <_start>: 8054: e3a00003 mov r0, #3 8058: e3a01004 mov r1, #4 805c: e92d0003 push {r0, r1} 8060: e8bd000c pop {r2, r3} 8064: e92d0003 push {r0, r1} 8068: e8bd0030 pop {r4, r5} 806c: e1200070 bkpt 0x0000 | |
Comme vous pouvez le constater, nos instructions LDMIA et STMDB ont été traduites en PUSH et POP. En effet, PUSH est synonyme de STMDB sp!, reglist et POP est synonyme de LDMIA sp! reglist.
Exécutons ce code dans GDB :
gef> break _start gef> run gef> nexti 2 [...] gef> x/w $sp 0xbefff7e0: 0x00000001 |
Après avoir exécuté les deux premières instructions, nous avons rapidement vérifié l'adresse mémoire et la valeur vers lesquelles pointe SP. L'instruction PUSH suivante doit diminuer SP de 8 et stocker les valeurs de R1 et R0 (dans cet ordre) sur la pile.
gef> nexti [...] ----- Stack ----- 0xbefff7d8|+0x00: 0x3 <- $sp 0xbefff7dc|+0x04: 0x4 0xbefff7e0|+0x08: 0x1 [...] gef> x/w $sp 0xbefff7d8: 0x00000003 |
Ensuite, ces deux valeurs (0x3 et 0x4) sont extraites de la pile et transférées dans les registres, de sorte que R2 = 0x3 et R3 = 0x4. SP est augmenté de 8 :
gef> nexti gef> info register r2 r3 r2 0x3 3 r3 0x4 4 gef> x/w $sp 0xbefff7e0: 0x00000001 |