École d'assemblage : langage assembleur pour processeurs centraux d'architecture ARM. École d'assemblage : langage assembleur pour processeurs centraux d'architecture ARM Règles de base pour l'écriture de programmes en assembleur

Salut tout le monde!
De par ma profession, je suis programmeur Java. Les derniers mois de travail m'ont obligé à me familiariser avec le développement pour Android NDK et, par conséquent, à écrire des applications natives en C. Ici, j'ai été confronté au problème de l'optimisation des bibliothèques Linux. Beaucoup se sont révélés totalement non optimisés pour ARM et ont fortement chargé le processeur. Auparavant, je n'avais pratiquement jamais programmé en langage assembleur, donc au début c'était difficile de commencer à apprendre ce langage, mais j'ai quand même décidé d'essayer. Cet article a été écrit, pour ainsi dire, par un débutant pour des débutants. Je vais essayer de décrire les bases que j'ai déjà apprises, j'espère que cela intéressera quelqu'un. De plus, je serai heureux de recevoir des critiques constructives de la part de professionnels.

Introduction
Alors, commençons par comprendre ce qu'est ARM. Wikipédia donne cette définition :

L'architecture ARM (Advanced RISC Machine, Acorn RISC Machine, advanced RISC machine) est une famille de cœurs de microprocesseurs 32 bits et 64 bits sous licence développés par ARM Limited. L'entreprise développe exclusivement des noyaux et des outils pour ceux-ci (compilateurs, outils de débogage, etc.), gagnant de l'argent en concédant l'architecture sous licence à des fabricants tiers.

Si quelqu’un ne le sait pas, la plupart des appareils mobiles et des tablettes sont désormais développés sur cette architecture de processeur. Le principal avantage de cette famille est sa faible consommation d'énergie, grâce à laquelle elle est souvent utilisée dans divers systèmes embarqués. L'architecture a évolué au fil du temps, et à partir d'ARMv7, 3 profils ont été définis : « A » (application) - applications, « R » (temps réel) - temps réel, « M » (microcontrôleur) - microcontrôleur. Vous pouvez lire l’histoire du développement de cette technologie et d’autres données intéressantes sur Wikipédia ou en la recherchant sur Internet. ARM prend en charge différents modes de fonctionnement (Thumb et ARM, de plus, Thumb-2 est récemment apparu, qui est un mélange d'ARM et de Thumb). Dans cet article, nous examinerons le mode ARM lui-même, dans lequel un jeu d'instructions de 32 bits est exécuté.

Chaque processeur ARM est créé à partir des blocs suivants :

  • 37 registres (dont seulement 17 sont visibles lors du développement)
  • Unité arithmétique et logique (ALU) - effectue des tâches arithmétiques et logiques
  • Barrel Shifter - un appareil conçu pour déplacer des blocs de données d'un certain nombre de bits
  • Le CP15 est un système spécial qui contrôle les coprocesseurs ARM
  • Décodeur d'instructions - traite de la conversion des instructions en une séquence de micro-opérations
Ce ne sont pas tous des composants d'ARM, mais plonger dans la jungle de la construction des processeurs dépasse le cadre de cet article.
Exécution du pipeline
Les processeurs ARM utilisent un pipeline en 3 étapes (à partir d'ARM8, un pipeline en 5 étapes a été implémenté). Examinons un pipeline simple utilisant le processeur ARM7TDMI comme exemple. L'exécution de chaque instruction se compose de trois étapes :

1. Étape d'échantillonnage (F)
À ce stade, les instructions circulent de la RAM vers le pipeline du processeur.
2. Étape de décodage (D)
Les instructions sont décodées et leur type est reconnu.
3. Phase d'exécution (E)
Les données entrent dans l'ALU et sont exécutées et la valeur résultante est écrite dans le registre spécifié.

Mais lors du développement, il faut tenir compte du fait qu'il existe des instructions qui utilisent plusieurs cycles d'exécution, par exemple charger(LDR) ou stocker. Dans ce cas, l'étape d'exécution (E) est divisée en étapes (E1, E2, E3...).

Exécution conditionnelle
L'une des fonctions les plus importantes de l'assembleur ARM est l'exécution conditionnelle. Chaque instruction peut être exécutée de manière conditionnelle et des suffixes sont utilisés à cet effet. Si un suffixe est ajouté au nom d'une instruction, les paramètres sont vérifiés avant de l'exécuter. Si les paramètres ne remplissent pas la condition, l'instruction n'est pas exécutée. Suffixes :
MI - nombre négatif
PL - positif ou zéro
AL - toujours exécuter les instructions
Il existe de nombreux autres suffixes d'exécution conditionnelle. Lisez le reste des suffixes et des exemples dans la documentation officielle : Documentation ARM
Il est maintenant temps de réfléchir...
Syntaxe de base de l'assembleur ARM
Pour ceux qui ont déjà travaillé avec l'assembleur, vous pouvez ignorer ce point. Pour tous les autres, je décrirai les bases du travail avec ce langage. Ainsi, chaque programme en langage assembleur se compose d’instructions. L'instruction est créée de cette façon :
(étiquette) (instruction|opérandes) (@ commentaire)
L'étiquette est un paramètre facultatif. L'instruction est un mnémonique direct d'instructions adressées au processeur. Les instructions de base et leur utilisation seront discutées ci-dessous. Opérandes - constantes, adresses de registre, adresses dans la RAM. Un commentaire est un paramètre facultatif qui n'affecte pas l'exécution du programme.
Enregistrer les noms
Les noms de registre suivants sont autorisés :
1.r0-r15

3.v1-v8 (registres variables, r4 à r11)

4.sb et SB (registre statique, r9)

5.sl et SL (r10)

6.fp et FP (r11)

7.ip et IP (r12)

8.sp et SP (r13)

9.lr et LR (r14)

10.pc et PC (compteur de programme, r15).

Variables et constantes
Dans l'assembleur ARM, comme dans n'importe quel (pratiquement) autre langage de programmation, des variables et des constantes peuvent être utilisées. Ils sont répartis dans les types suivants :
  • Numérique
  • casse-tête
  • Chaîne
Les variables numériques sont initialisées comme ceci :
un SETA 100 ; une variable numérique "a" est créée avec la valeur 100.
Variables de chaîne :
improb SETS "littéral" ; une variable improb est créée avec la valeur « littéral ». ATTENTION! La valeur de la variable ne peut pas dépasser 5 120 caractères.
Les variables booléennes utilisent respectivement les valeurs VRAI et FAUX.
Exemples d'instructions d'assembleur ARM
Dans ce tableau, j'ai rassemblé les instructions de base qui seront nécessaires pour un développement ultérieur (au stade le plus élémentaire :) :

Pour renforcer l'utilisation des instructions de base, écrivons quelques exemples simples, mais nous aurons d'abord besoin d'une chaîne d'outils arm. Je travaille sous Linux donc j'ai choisi : frank.harvard.edu/~coldwell/toolchain (arm-unknown-linux-gnu toolchain). Il peut être installé aussi facilement que n’importe quel autre programme sous Linux. Dans mon cas (Fedora russe), il me suffisait d'installer les packages RPM à partir du site Web.
Il est maintenant temps d'écrire un exemple simple. Le programme sera absolument inutile, mais l'essentiel est qu'il fonctionne :) Voici le code que je vous propose :
start: @ Ligne optionnelle indiquant le début du programme mov r0, #3 @ Charger le registre r0 avec la valeur 3 mov r1, #2 @ Faire de même avec le registre r1, seulement maintenant avec la valeur 2 ajouter r2, r1, r0 @ Additionnez les valeurs de r0 et r1, la réponse est écrite dans r2 mul r3, r1, r0 @ Multipliez la valeur du registre r1 par la valeur du registre r0, la réponse est écrite dans r3 stop : b stop @ Ligne de fin de programme
On compile le programme pour obtenir le fichier .bin :
/usr/arm/bin/arm-unknown-linux-gnu-as -o arm.o arm.s /usr/arm/bin/arm-unknown-linux-gnu-ld -Ttext=0x0 -o ​​​​arm. elf arm .o /usr/arm/bin/arm-unknown-linux-gnu-objcopy -O binaire arm.elf arm.bin
(le code est dans le fichier arm.s, et la chaîne d'outils dans mon cas est dans le répertoire /usr/arm/bin/)
Si tout s'est bien passé, vous aurez 3 fichiers : arm.s (le code réel), arm.o, arm.elf, arm.bin (le programme exécutable réel). Afin de vérifier le fonctionnement du programme, il n'est pas nécessaire d'avoir votre propre appareil arm. Il suffit d'installer QEMU. Pour référence:

QEMU est un programme gratuit et open source permettant d'émuler le matériel de diverses plates-formes.

Inclut l'émulation des processeurs Intel x86 et des périphériques d'E/S. Peut émuler les processeurs 80386, 80486, Pentium, Pentium Pro, AMD64 et autres processeurs compatibles x86 ; PowerPC, ARM, MIPS, SPARC, SPARC64, m68k - seulement partiellement.

Fonctionne sur Syllable, FreeBSD, FreeDOS, Linux, Windows 9x, Windows 2000, Mac OS X, QNX, Android, etc.

Donc, pour émuler arm, vous aurez besoin de qemu-system-arm. Ce paquet est dans yum, donc pour ceux qui ont Fedora, vous n'avez pas à vous embêter et exécutez simplement la commande :
miam, installez qemu-system-arm

Ensuite, nous devons lancer l'émulateur ARM pour qu'il exécute notre programme arm.bin. Pour ce faire, nous allons créer un fichier flash.bin, qui sera la mémoire flash pour QEMU. C'est très simple de faire ceci :
dd if=/dev/zero of=flash.bin bs=4096 count=4096 dd if=arm.bin of=flash.bin bs=4096 conv=notrunc
Maintenant, nous chargeons QEMU avec la mémoire flash résultante :
qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
Le résultat ressemblera à ceci :

$ qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
Moniteur QEMU 0.15.1 - tapez "help" pour plus d'informations
(qému)

Notre programme arm.bin a dû modifier les valeurs de quatre registres, donc, pour vérifier le bon fonctionnement, regardons ces mêmes registres. Cela se fait avec une commande très simple : info enregistre
À la sortie, vous verrez les 15 registres ARM, et quatre d'entre eux auront des valeurs modifiées. Vérifiez :) Les valeurs des registres correspondent à celles auxquelles on peut s'attendre après l'exécution du programme :
(qemu) registres d'informations R00=00000003 R01=00000002 R02=00000005 R03=00000006 R04=00000000 R05=00000000 R06=00000000 R07=00000000 R08=00000000 R09=00 000000 R10=00000000 R11=00000000 R12=00000000 R13=00000000 R14= 00000000 R15=00000010 PSR=400001d3 -Z-- A svc32

P.S. Dans cet article, j'ai essayé de décrire les bases de la programmation en assembleur ARM. J'espère que tu as aimé! Cela suffira pour approfondir davantage la jungle de ce langage et y écrire des programmes. Si tout se passe bien, j'écrirai davantage sur ce que j'ai découvert moi-même. S'il y a des erreurs, ne me donnez pas de coup de pied, car je suis nouveau dans l'assembleur.

Cette section décrit les jeux d'instructions du processeur ARM7TDMI.

4.1 Brève description du format

Cette section fournit une brève description des jeux d'instructions ARM et Thumb.

La clé des tableaux de jeux d’instructions est présentée dans le tableau 1.1.

Le processeur ARM7TDMI est basé sur l'architecture ARMv4T. Une description plus complète des deux jeux d'instructions est fournie dans le manuel de référence de l'architecture ARM.

Tableau 1.1. Clé des tableaux

Les formats du jeu d’instructions ARM sont illustrés à la figure 1.5.

Pour des informations plus détaillées sur les formats de jeu d’instructions ARM, consultez le manuel de référence architectural ARM.

Graphique 1.5. Formats des jeux d'instructions ARM

Certains codes d'instructions ne sont pas définis, mais ils n'entraînent pas de recherche d'instructions non définies, comme une instruction de multiplication avec le bit 6 défini sur 1. De telles instructions ne doivent pas être utilisées car leur effet pourrait être modifié à l'avenir. Le résultat de l'exécution de ces codes d'instructions dans le processeur ARM7TDMI est imprévisible.

4.2 Brève description des instructions ARM

Le jeu d’instructions ARM est présenté dans le tableau 1.2.

Tableau 1.2. Brève introduction aux instructions ARM

Opérations Syntaxe d'assemblage
Expédition Expédition MOV (cond)(S) Rd,
Renvoi NON MVN (cond)(S) Rd,
Transmission du SPSR au registre MRS (cond.) Rd, SPSR
Envoi du CPSR pour s'inscrire MRS (cond) Rd, CPSR
Transfert du registre SPSR MSR (cond) SPSR (champ), Rm
Transmission CPSR MSR (cond) CPSR (champ), Rm
Passer des constantes aux indicateurs SPSR MSR (cond) SPSR_f, #32bit_Imm
Passer des constantes aux indicateurs CPSR MSR (cond) CPSR_f, #32bit_Imm
Arithmétique Ajout AJOUTER (cond)(S) Rd, Rn,
Ajout avec transport ADC (cond)(S) Rd, Rn,
Soustraction SUB (cond)(S) Rd, Rn,
Soustraction avec report SBC (cond)(S) Rd, Rn,
Soustraction soustraction inverse RSB (cond)(S) Rd, Rn,
Soustraction soustraction inverse avec report RSC (cond)(S) Rd, Rn,
Multiplication MUL (cond)(S) Rd, Rm, Rs
Multiplication-accumulation MLA (cond)(S) Rd, Rm, Rs, Rn
Multiplier des nombres longs non signés UMULL
Multiplication - accumulation non signée de valeurs longues UMLAL (cond)(S) RdLo, RdHi, Rm, Rs
Multiplier les longs signés SMULL (cond)(S) RdLo, RdHi, Rm, Rs
Multiplication - accumulation signée de valeurs longues SMLAL (cond)(S) RdLo, RdHi, Rm, Rs
Comparaison Chemin CMP (suite),
Comparaison négative Chemin CMN (suite),
casse-tête Examen TST (cond) Rn,
Contrôle d'équivalence TEQ (cond) Rn,
Enregistrer. ET ET (cond)(S) Rd, Rn,
Hors. OU EOR (cond)(S) Rd, Rn,
ORR ORR (cond)(S) Rd, Rn,
Bit de réinitialisation BIC (cond)(S) Rd, Rn, >
Transition Transition (cond)étiquette
Suite à un lien (cond)étiquette
Transition et changement de jeu d'instructions (cond)Rn
En lisant mots LDR (suite) Rd,
LDR(cond)T Rd,
octets LDR (cond) B Rd,
LDR (cond) BT Rd,
octet signé LDR(cond)SB Rd,
demi-mots LDR(cond)H Rd,
des demi-mots avec un signe LDR(cond)SH Rd,
opérations avec plusieurs blocs de données -
  • avec pré-incrémentation
  • LDM (cond)IB Rd(, !} {^}
  • avec incrément ultérieur
  • LDM (cond)IA Rd(, !} {^}
  • avec décrémentation préalable
  • LDM (cond)DB Rd(, !} {^}
  • suivi d'une décrémentation
  • LDM (cond)DA Rd(, !} {^}
  • opération de pile
  • LDM (suite) Chemin(, !}
  • fonctionnement de la pile et récupération CPSR
  • LDM (suite) Chemin(, !} ^
    opération de pile avec des registres d'utilisateurs LDM (suite) Chemin(, !} ^
    Enregistrer mots STR (suite) Rd,
    mots avec l'avantage du mode utilisateur STR (cond) T Rd,
    octets STR (cond) B Rd,
    octet avec priorité du mode utilisateur STR (cond) BT Rd,
    demi-mots STR(cond)H Rd,
    opérations sur plusieurs blocs de données -
  • avec pré-incrémentation
  • STM (cond)IB Rd(, !} {^}
  • avec incrément ultérieur
  • STM (cond)IA Rd(, !} {^}
  • avec décrémentation préalable
  • STM (cond)DB Rd(, !} {^}
    o suivi d'une décrémentation STM (cond)DA Rd(, !} {^}
  • opération de pile
  • STM (suite) Chemin(, !}
  • opération de pile avec des registres d'utilisateurs
  • STM (suite) Chemin(, !} ^
    Échange mots SWP (cond.) Rd, Rm,
    octet SWP (cond)B Rd, Rm,
    Coprocesseur Opération sur les données CDP(cond)p , , CRd, CRn, CRm,
    Transfert vers le registre ARM depuis le coprocesseur MRC(cond)p , , Rd, CRn, CRm,
    Transfert vers le coprocesseur depuis le registre ARM MCR(cond)p , , Rd, CRn, CRm,
    En lisant PMA(cond)p ,CRd,
    Enregistrer STC(cond)p ,CRd,
    Interruption logicielle SWI 24 bits_Imm

    Vous pouvez vous familiariser en détail avec le système de commande en mode ARM.

    Modes d'adressage

    Les modes d'adressage sont des procédures utilisées par diverses instructions pour générer des valeurs utilisées par les instructions. Le processeur ARM7TDMI prend en charge 5 modes d'adressage :

    • Mode 1 - Opérandes de décalage pour les instructions de traitement des données.
    • Mode 2 - Lire et écrire un mot ou un octet non signé.
    • Mode 3-lire et écrire un demi-mot ou charger un octet de signe.
    • Mode 4 - Lecture et écriture multiples.
    • Mode 5 - Lecture et écriture du coprocesseur.

    Les modes d'adressage indiquant leurs types et codes mnémoniques sont présentés dans le tableau 1.3.

    Tableau 1.3. Modes d'adressage

    Mode d'adressage Type ou mode d'adressage Code mnémonique ou type de pile
    Mode 2 Constante de décalage
    Registre de décalage
    Registre de décalage d'échelle
    Décalage pré-indexé -
    Constante !
    Registre !
    Registre de balance !
    !
    !
    !
    !
    -
    Constante , #+/-12bit_Offset
    Registre , +/-Rm
    Registre de balance
    Mode 2, privilégié Constante de décalage
    Registre de décalage
    Registre de décalage d'échelle
    Décalage suivi d'une indexation -
    Constante , #+/-12bit_Offset
    Registre , +/-Rm
    Registre de balance , +/-Rm, LSL #5bit_shift_imm
    , +/-Rm, LSR #5bit_shift_imm
    , +/-Rm, ASR #5bit_shift_imm
    , +/-Rm, ROR #5bit_shift_imm
    Mode 3, > Constante de décalage
    !
    Indexation ultérieure , #+/-8bit_Offset
    Registre
    Pré-indexation !
    Indexation ultérieure , +/-Rm
    Mode 4, lecture IA, incrément ultérieur FD, descente complète
    ED, vide décroissant
    DA, décrémentation ultérieure FA, ascendant complet
    Pré-décrémentation de la base de données EA, vide ascendant
    Mode 4, enregistrement IA, incrément ultérieur FD, descente complète
    IB, pré-incrément ED, vide décroissant
    DA, décrémentation ultérieure FA, ascendant complet
    Pré-décrémentation de la base de données EA, vide ascendant
    Mode 5, transfert de données coprocesseur Constante de décalage
    Pré-indexation !
    Indexation ultérieure , #+/-(8bit_Offset*4)

    Opérande 2

    Un opérande est la partie d'une instruction qui fait référence à des données ou à un périphérique. Les opérandes 2 sont présentés dans le tableau 1.4.

    Tableau 1.4. Opérande 2

    Les champs sont présentés dans le tableau 1.5.

    Tableau 1.5. Des champs

    Champs de condition

    Les champs de condition sont présentés dans le tableau 1.6.

    Tableau 1.6. Champs de condition

    Type de champ Suffixe Description Condition
    État (suite) égaliseur Équivaut à Z=1
    NE Inégal Z=0
    C.S. Non signé supérieur ou égal à C=1
    CC Non signé moins C=0
    MI Négatif N=1
    PL. Positif ou zéro N=0
    CONTRE Débordement V=1
    V.C. Pas de débordement V=0
    SALUT Non signé plus C=1, Z=0
    L.S. Non signé inférieur ou égal à C=0, Z=1
    G.E. Plus ou égal N=V (N=V=1 ou N=V=0)
    LT Moins NV (N=1 et V=0) ou (N=0 et V=1)
    GT Plus Z=0, N=V (N=V=1 ou N=V=0)
    L.E. Inférieur ou égal Z=0 ou NV (N=1 et V=0) ou (N=0 et V=1)
    AL Toujours vrai les drapeaux sont ignorés

    4.3 Brève description du jeu d'instructions Thumb

    Les formats des jeux d’instructions Thumb sont illustrés à la figure 1.6. Pour plus d’informations sur les formats de jeu d’instructions ARM, consultez le manuel de référence architectural ARM.


    Graphique 1.6. Formats des jeux d'instructions du pouce

    Le jeu d’instructions Thumb est présenté dans le tableau 1.7.

    Tableau 1.7. Brève description du jeu d'instructions Thumb

    Opération Syntaxe d'assemblage
    Transfert (copie) constantes Chemin MOV, #8bit_Imm
    senior à junior MOV Rd, HS
    junior à senior MOV Hd, RS
    senior à senior MOV Hd, HS
    Arithmétique ajout AJOUTER Rd, Rs, #3bit_Imm
    ajouter un mineur à un mineur AJOUTER Rd, Rs, Rn
    ajouter l'aîné au plus jeune AJOUTER Rd, Hs
    ajouter junior à senior AJOUTER HD, Rs
    ajouter l'aîné à l'aîné AJOUTER Hd, Hs
    addition avec une constante AJOUTER Rd, #8bit_Imm
    ajouter de la valeur à SP AJOUTER SP, #7bit_Imm AJOUTER SP, #-7bit_Imm
    ajout avec transport Chemin ADC, RS
    soustraction SUB Rd, Rs, Rn SUB Rd, Rs, #3bit_Imm
    soustraction d'une constante Chemin SUB, #8bit_Imm
    soustraction avec report Chemin SBC, Rs. 1 000.
    inversion de signe NEG Rd, RS
    multiplication Chemin MUL, Rs. 1 000.
    comparer plus jeune avec plus jeune Chemin CMP, Rs. 1 000.
    comparer junior et senior CMP Rd,Hs
    comparer les plus âgés et les plus jeunes CMP HD, Rs
    comparer les personnes âgées et âgées CMP Hd, Hs
    comparer négatif Chemin CMN, Rs. 1 000.
    comparer avec une constante Chemin CMP, #8bit_Imm
    casse-tête ET ET Rd, Rs
    Hors. OU EOR Rd, RS
    OU ORR Rd, RS
    Bit de réinitialisation Chemin BIC, Rs. 1 000.
    Renvoi NON Chemin MVN, RS.
    Test de bits Chemin TST, RS
    Changement/Rotation Décalage logique vers la gauche Chemin LSL, Rs, #5bit_shift_imm Chemin LSL, Rs
    Décalage logique vers la droite Chemin LSR, Rs, #5bit_shift_imm Chemin LSR, Rs
    Décalage arithmétique vers la droite Chemin ASR, Rs, #5bit_shift_imm Chemin ASR, Rs
    Tourner à droite ROR Rd, RS
    Transition sauts conditionnels -
    Étiquette BEQ
    Etiquette BNE
    Étiquette BCS
    Étiquette BCC
    Étiquette IMC
    Etiquette BPL
    Étiquette BVS
    Etiquette BVC
  • C=1, Z=0
  • Étiquette BHI
  • C=0, Z=1
  • Etiquette BLS
  • N=1, V=1 ou N=0, V=0
  • Etiquette BGE
  • N=1, V=0 ou N=0, V=1
  • Etiquette BLT
  • Z=0 et ((N ou V=1) ou (N ou V=0))
  • Étiquette BGT
  • Z=1 ou ((N=1 ou V=0) ou (N=0 et V=1))
  • Étiquette BLE
    Saut inconditionnel Étiquette B
    Clic sur un lien long Etiquette BL
    Changement d'état facultatif -
  • à l'adresse en ml. registre
  • BX Rs
  • à l'adresse à st. registre
  • BX HS
    En lisant avec constante de décalage -
  • mots
  • Chemin LDR,
  • demi-mots
  • Chemin LDRH,
  • octets
  • Chemin LDRB,
    avec registre décalé -
  • mots
  • Chemin LDR,
  • demi-mots
  • Chemin LDRH,
  • signer un demi-mot
  • Chemin LDRSH,
    Chemin LDRB,
  • octet signé
  • Chemin LDRSB,
    par rapport au compteur de programme PC Chemin LDR,
    par rapport au pointeur de pile SP Chemin LDR,
    Adresse -
  • par PC
  • AJOUTER Rd, PC, #10bit_Offset
  • en utilisant SP
  • AJOUTER Rd, SP, #10bit_Offset
    Lectures multiples LDMIA Rb!,
    Enregistrer avec constante de décalage -
  • mots
  • Chemin STR,
  • demi-mots
  • Chemin STRH,
  • octets
  • Chemin STRB,
    avec registre décalé -
  • mots
  • Chemin STR,
  • demi-mots
  • Chemin STRH,
  • octets
  • Chemin STRB,
    par rapport à SP Chemin STR,
    Entrée multiple STMIA Rb!,
    Pousser/sortir de la pile Pousser les registres sur la pile POUSSER
    Poussez LR et les registres sur la pile POUSSER
    Pop enregistre les registres de la pile POPULAIRE
    Registres pop et PC de la pile POPULAIRE
    Interruption logicielle - SWI 8 bits_Imm

    Les processeurs CISC effectuent des opérations assez complexes en une seule instruction, notamment des opérations arithmétiques et logiques sur le contenu des cellules mémoire. Les instructions du processeur CISC peuvent avoir des longueurs différentes.

    En revanche, RISC dispose d'un système d'instructions relativement simple avec une division claire par type d'opération :

    • travailler avec la mémoire (lecture de la mémoire dans les registres ou écriture des registres dans la mémoire),
    • traiter des données dans des registres (arithmétique, logique, décalage de données gauche/droite ou rotation de bits dans un registre),
    • commandes de transitions conditionnelles ou inconditionnelles vers d’autres adresses.

    En règle générale (mais pas toujours, et seulement si le code du programme entre dans la mémoire cache du contrôleur), une commande est exécutée dans un cycle de processeur. La longueur d'une instruction du processeur ARM est fixe - 4 octets (un mot informatique). En fait, un processeur ARM moderne peut passer à d'autres modes de fonctionnement, par exemple en mode THUMB, lorsque la longueur de l'instruction atteint 2 octets. Cela vous permet de rendre le code plus compact. Cependant, nous n'abordons pas ce mode dans cet article, car il n'est pas pris en charge dans le processeur Amber ARM v2a. Pour la même raison, nous ne considérerons pas les modes tels que Jazelle (optimisés pour l'exécution de code Java) et nous ne considérerons pas les commandes NEON - les commandes pour les opérations sur plusieurs données. Après tout, nous étudions le système d’instructions ARM pur.

    Registres du processeur ARM.

    Le processeur ARM dispose de plusieurs jeux de registres, dont le programmeur n'a actuellement accès qu'à 16. Il existe plusieurs modes de fonctionnement du processeur ; en fonction du mode de fonctionnement, la banque de registres appropriée est sélectionnée. Ces modes de fonctionnement :

    • mode application (USR, mode utilisateur),
    • mode superviseur ou mode système d'exploitation (SVC, mode superviseur),
    • mode de traitement des interruptions (IRQ, mode interruption) et
    • mode de traitement « interruption urgente » (FIRQ, mode interruption rapide).

    Autrement dit, lorsqu'une interruption se produit, le processeur lui-même se rend à l'adresse du programme de gestion des interruptions et « change » automatiquement de banque de registres.

    Les processeurs ARM des anciennes versions, en plus des modes de fonctionnement ci-dessus, disposent de modes supplémentaires :

    • Abandonner (utilisé pour gérer les exceptions d'accès à la mémoire),
    • Indéfini (utilisé pour implémenter un coprocesseur dans un logiciel) et
    • mode tâche privilégié du système d’exploitation Système.

    Le processeur Amber ARM v2a ne dispose pas de ces trois modes supplémentaires.

    Pour Amber ARM v2a, l’ensemble des registres peut être représenté comme suit :

    Les registres r0-r7 sont les mêmes pour tous les modes.
    Les registres r8-r12 sont communs uniquement pour les modes USR, SVC, IRQ.
    Le registre r13 est un pointeur de pile. Il est le sien dans tous les modes.
    Registre r14 - le registre de retour du sous-programme est également différent dans tous les modes.
    Le registre r15 est un pointeur vers des instructions exécutables. C’est commun à tous les modes.

    On voit que le mode FIRQ est le plus isolé, il possède le plus de ses propres registres. Ceci est fait pour que certaines interruptions très critiques puissent être traitées sans enregistrer les registres sur la pile, sans perdre de temps.

    Une attention particulière doit être portée au registre r15, également connu sous le nom de pc (Program Counter) - un pointeur vers des commandes exécutables. Vous pouvez effectuer diverses opérations arithmétiques et logiques sur son contenu, ainsi l'exécution du programme se déplacera vers d'autres adresses. Cependant, spécifiquement pour le processeur ARM v2a implémenté dans le système Amber, il existe quelques subtilités dans l'interprétation des bits de ce registre.

    Le fait est que dans ce processeur, dans le registre r15 (pc), en plus du pointeur réel vers les commandes exécutables, les informations suivantes sont contenues :

    Bits 31:28 - indicateurs pour le résultat d'une opération arithmétique ou logique
    Bits 27 - masque IRQ d'interruption, les interruptions sont désactivées lorsque le bit est défini.
    Bits 26 - Masque d'interruption FIRQ, les interruptions rapides sont désactivées lorsque le bit est activé.
    Bits 25:2 - le pointeur réel vers les instructions du programme n'occupe que 26 bits.
    Bits 1:0 - mode de fonctionnement actuel du processeur.
    3 - Superviseur
    2 - Interruption
    1 - Interruption rapide
    0 - Utilisateur

    Dans les anciens processeurs ARM, tous les indicateurs et bits de service sont situés dans des registres séparés Registre de l'état actuel du programme(cpsr) et le registre d'état du programme enregistré (spsr), pour l'accès auxquels il existe des commandes spéciales distinctes. Ceci est fait afin d'élargir l'espace d'adressage disponible pour les programmes.

    L'une des difficultés dans la maîtrise de l'assembleur ARM réside dans les noms alternatifs de certains registres. Ainsi, comme mentionné ci-dessus, le R15 est le même PC. Il y a aussi r13 - c'est le même sp (Stack Pointer), r14 est lr (Link Register) - le registre d'adresses de retour de la procédure. De plus, r12 est la même adresse IP (Intra-Procedure -call scratch register) utilisée par les compilateurs C d'une manière spéciale pour accéder aux paramètres de la pile. Une telle dénomination alternative est parfois déroutante lorsque vous regardez le code de programme de quelqu'un d'autre - ces deux désignations de registre s'y trouvent.

    Caractéristiques de l'exécution de code.

    Dans de nombreux types de processeurs (par exemple, x86), seule une transition vers une autre adresse de programme peut être effectuée par condition. Ce n'est pas le cas avec ARM. Chaque instruction du processeur ARM peut ou non être exécutée de manière conditionnelle. Cela vous permet de minimiser le nombre de transitions dans le programme et donc d'utiliser plus efficacement le pipeline du processeur.

    Après tout, qu’est-ce qu’un pipeline ? Une instruction du processeur est maintenant sélectionnée dans le code du programme, la précédente est déjà en cours de décodage et la précédente est déjà en cours d'exécution. C'est le cas du pipeline à 3 étages du processeur Amber A23, que nous utilisons dans notre projet pour la carte Mars Rover2Mars Rover2. La modification du processeur Amber A25 dispose d'un pipeline en 5 étapes, elle est encore plus efficace. Mais il y a un grand MAIS. Les commandes de saut forcent le processeur à vider le pipeline et à le remplir. Ainsi, une nouvelle commande est sélectionnée, mais il n'y a toujours rien à décoder et surtout rien à exécuter immédiatement. L'efficacité de l'exécution du code diminue avec les transitions fréquentes. Les processeurs modernes disposent de toutes sortes de mécanismes de prédiction de branchement qui optimisent d'une manière ou d'une autre le remplissage du pipeline, mais notre processeur ne l'a pas. Dans tous les cas, ARM a eu la sagesse de permettre à chaque commande d'être exécutée de manière conditionnelle.

    Sur un processeur ARM, dans tout type d'instruction, les quatre bits de la condition d'exécution de l'instruction sont codés dans les quatre bits les plus élevés du code instruction :

    Il y a un total de 4 indicateurs de condition dans le processeur :
    . Négatif - le résultat de l'opération s'est avéré négatif,
    . Zéro - le résultat est zéro,
    . Carry - un report s'est produit lors de l'exécution d'une opération avec des nombres non signés,
    . oVerflow - un débordement s'est produit lors de l'exécution d'une opération avec des nombres signés, le résultat ne rentre pas dans le registre)

    Ces 4 drapeaux forment de nombreuses combinaisons de conditions possibles :

    Code Suffixe Signification Drapeaux
    4"h0 équip Égal Ensemble Z
    4"h1 ne Inégal Z effacer
    4"h2 cs/hs Ensemble de transport / non signé supérieur ou identique Ensemble C
    4"h3 cc/lo Porter clair / non signé en bas C clair
    4"h4 mi Moins/négatif N ensemble
    4"h5 PL Plus / positif ou zéro N clair
    4"h6 contre Débordement Ensemble V
    4"h7 vc Pas de débordement V clair
    4"h8 Salut Non signé supérieur C défini et Z effacé
    4"h9 ls Non signé en bas ou identique C clair ou Z défini
    4"ha ge Signé supérieur ou égal N == V
    4" hauteur lt Signé moins de N !=V
    4" HC GT Signé plus grand que Z == 0,N == V
    4"HD le Signé inférieur ou égal Z == 1 ou N != V
    4"il Al Toujours (inconditionnel)
    4" haute fréquence - Condition invalide

    Cela conduit maintenant à une autre difficulté dans l'apprentissage des instructions du processeur ARM : les nombreux suffixes qui peuvent être ajoutés au code d'instruction. Par exemple, l'ajout à condition que l'indicateur Z soit défini est la commande addeq comme add + suffix eq . Passer au sous-programme si l'indicateur N=0 est blpl comme bl + suffixe pl .

    Drapeaux (Négatif, Zéro, Carry, débordement) la même chose n'est pas toujours définie lors d'opérations arithmétiques ou logiques, comme cela se produit, par exemple, dans un processeur x86, mais uniquement lorsque le programmeur le souhaite. Pour cela, il existe un autre suffixe aux mnémoniques de commande : « s » (dans le code de commande il est codé par le bit 20). Ainsi, la commande addition ne modifie pas les indicateurs, mais la commande add modifie les indicateurs. Ou bien il peut aussi y avoir une commande d'ajout conditionnel, mais qui change les drapeaux. Par exemple : ajouts. Il est clair que le nombre de combinaisons possibles de noms de commandes avec différents suffixes pour l'exécution conditionnelle et la définition des indicateurs rend le code assembleur d'un processeur ARM très particulier et difficile à lire. Cependant, avec le temps, on s’y habitue et on commence à comprendre ce texte.

    Opérations arithmétiques et logiques (Traitement des Données).

    Le processeur ARM peut effectuer diverses opérations arithmétiques et logiques.

    Le code d'opération réel à quatre bits (Opcode) est contenu dans les bits d'instruction du processeur.

    Toute opération est effectuée sur le contenu du registre et sur ce qu'on appelle shifter_operand. Le résultat de l'opération est inscrit au registre. Les quatre bits Rn et Rd sont des index des registres de la banque active du processeur.

    En fonction du bit I 25, shifter_operand est traité soit comme une constante numérique, soit comme un indice du deuxième registre de l'opérande, et même une opération de décalage sur la valeur du deuxième opérande.

    Des exemples simples de commandes assembleur ressembleraient à ceci :

    ajouter r0,r1,r2 @ placer la somme des valeurs des registres r1 et r2 dans le registre r0
    sub r5,r4,#7 @ placer la différence (r4-7) dans le registre r5

    Les opérations effectuées sont codées comme suit :

    4"h0 et ET Logique Rd:= Rn ET shifter_operand
    4"h1 eor OU exclusif logique Rd:= Rn XOR shifter_operand
    4"h2 sub Soustraction arithmétique Rd:= Rn - shifter_operand
    4"h3 rsb Soustraction inverse arithmétique Rd:= shifter_operand - Rn
    4"h4 add Addition arithmétique Rd:= Rn + shifter_operand
    4"h5 adc Addition arithmétique plus drapeau de report Rd:= Rn + opérande shifter + drapeau de report
    4"h6 sbc Soustraction arithmétique avec report Rd:= Rn - shifter_operand - NOT(Carry Flag)
    4"h7 rsc Soustraction inverse arithmétique avec report Rd:= shifter_operand - Rn - NOT(Carry Flag)
    4"h8 tst ET logique, mais sans stocker le résultat, seuls les drapeaux Rn AND shifter_operand S bit toujours activés sont modifiés
    4"h9 teq OU exclusif logique, mais sans stocker le résultat, seuls les flags Rn EOR shifter_operand sont modifiés
    Bit S toujours activé
    4"ha cmp Comparaison, ou plutôt soustraction arithmétique sans stocker le résultat, seuls les drapeaux Rn changent - shifter_operand Le bit S est toujours activé
    4"hb cmn Comparaison d'addition inverse, ou plutôt arithmétique sans stocker le résultat, seuls les drapeaux Rn + shifter_operand S bit toujours mis à 1
    4"hc orr OU Logique Rd:= Rn OU shifter_operand
    4"hd mov Copier la valeur Rd:= shifter_operand (pas de premier opérande)
    4"il bic Réinitialiser les bits Rd:= Rn AND NOT(shifter_operand)
    4"hf mvn Copier la valeur inverse Rd:= NOT shifter_operand (pas de premier opérande)

    Manette de vitesse à barillet.

    Le processeur ARM dispose d'un circuit spécial « barrel shifter » qui permet à l'un des opérandes d'être décalé ou tourné d'un nombre spécifié de bits avant toute opération arithmétique ou logique. C'est une fonctionnalité plutôt intéressante du processeur, qui permet de créer du code très efficace.

    Par exemple:

    @multiplier par 9, c'est multiplier un nombre par 8
    @ en décalant vers la gauche de 3 bits plus un autre chiffre
    ajouter r0, r1, r1, lsl #3 @ r0= r1+(r1<<3) = r1*9

    @ multiplier par 15, c'est multiplier par 16 moins le nombre
    rsb r0, r1, r1, lsl #4 @ r0= (r1<<4)-r1 = r1*15

    @ accès à une table de mots de 4 octets, où
    @r1 est l'adresse de base de la table
    @r2 est l'index de l'élément dans le tableau
    ldr r0,

    En plus du décalage logique vers la gauche lsl, il existe également un décalage logique vers la droite lsr et un décalage arithmétique vers la droite asr (un décalage préservant le signe, le bit le plus significatif est multiplié à gauche simultanément avec le décalage).

    Il y a aussi une rotation des bits de ror - les bits se déplacent vers la droite et ceux qui sont retirés se déplacent vers la gauche.
    Il y a un décalage d'un bit via le drapeau C - c'est la commande rrx. La valeur du registre est décalée d'un bit vers la droite. A gauche, le drapeau C est chargé dans le bit de poids fort du registre.

    Le décalage peut être effectué non pas par un nombre constant fixe, mais par la valeur du troisième registre d'opérande. Par exemple:

    ajoutez r0, r1, r1, lsr r3 @ c'est r0 = r1 + (r1>>r3);
    ajoutez r0, r0, r1, lsr r3 @ c'est r0 = r0 + (r1>>r3);

    Donc shifter_operand est ce que nous décrivons dans les commandes assembleur, par exemple comme "r1, lsr r3" ou "r2, lsl #5".

    Le plus intéressant est que le recours aux changements d’opérations ne coûte rien. Ces changements ne nécessitent (généralement) pas de cycles d'horloge supplémentaires, ce qui est très bon pour les performances du système.

    Utiliser des opérandes numériques.

    Les opérations arithmétiques ou logiques peuvent utiliser non seulement le contenu d'un registre, mais également une constante numérique comme deuxième opérande.

    Malheureusement, il existe ici une limitation importante. Étant donné que toutes les commandes ont une longueur fixe de 4 octets (32 bits), il ne sera pas possible d'y coder « n'importe quel » nombre. Dans le code d'opération, 4 bits sont déjà occupés par le code de condition d'exécution (Cond), 4 bits pour le code d'opération lui-même (Opcode), puis 4 bits - le registre récepteur Rd, et 4 autres bits - le registre du premier opérande Rn, plus divers indicateurs I 25 (désigne simplement une constante numérique dans le code d'opération) et S 20 (définir des indicateurs après l'opération). Au total, il ne reste que 12 bits pour une éventuelle constante, ce qu'on appelle shifter_operand - nous l'avons vu ci-dessus. Étant donné que 12 bits ne peuvent coder des nombres que dans une plage étroite, les développeurs du processeur ARM ont décidé de coder la constante comme suit. Les douze bits de shifter_operand sont divisés en deux parties : l'indicateur de rotation à quatre bits encode_imm et la valeur numérique réelle à huit bits imm_8.

    Sur un processeur ARM, une constante est définie comme un nombre de huit bits à l'intérieur d'un nombre de 32 bits, tourné vers la droite d'un nombre pair de bits. C'est-à-dire:

    imm_32 = imm_8 ROR (encode_imm *2)

    Cela s’est avéré assez délicat. Il s'avère que tous les nombres constants ne peuvent pas être utilisés dans les commandes assembleur.

    Tu peux écrire

    ajoutez r0, r2, #255 @ constante sous forme décimale
    ajoutez r0, r3, #0xFF @ constante en hexadécimal

    puisque 255 est dans la plage 8 bits. Ces commandes seront compilées comme ceci :

    0 : e28200ff ajouter r0, r2, #255 ; 0xff
    4 : e28300ff ajouter r0, r3, #255 ; 0xff

    Et tu peux même écrire

    ajoutez r0, r4, #512
    ajouter r0, r5, 0x650000

    Le code compilé ressemblera à ceci :

    0 : e2840c02 ajouter r0, r4, #512 ; 0x200
    4 : e2850865 ajouter r0, r5, #6619136 ; 0x650000

    Dans ce cas, le nombre 512 lui-même, bien entendu, ne rentre pas dans l'octet. Mais ensuite nous l'imaginons sous forme hexadécimale 32'h00000200 et voyons que c'est 2 étendu vers la droite de 24 bits (1 ror 24). Le coefficient de rotation est deux fois inférieur à 24, soit 12. Il s'avère donc shifter_operand = ( 4'hc , 8'h02 ) - ce sont les douze bits les moins significatifs de la commande. Il en va de même pour le numéro 0x650000. Pour lui, shifter_operand = ( 4’h8, 8’h65 ).

    Il est clair qu'on ne peut pas écrire

    ajouter r0, r1,#1234567

    ou tu ne peux pas écrire

    mouvement r0, #511

    puisqu'ici le nombre ne peut pas être représenté sous la forme imm_8 et encode_imm - le facteur de rotation. Le compilateur assembleur générera une erreur.

    Que faire lorsqu'une constante ne peut pas être directement codée dans shifter_operand ? Nous devrons faire toutes sortes de trucs.
    Par exemple, vous pouvez d'abord charger le nombre 512 dans un registre gratuit, puis en soustraire un :

    mouvement r0, #511
    sous r0,r0,#1

    La deuxième façon de charger un nombre spécifique dans un registre est de le lire à partir d'une variable spécialement réservée située en mémoire :

    ldr r7, ma_var
    .....
    ma_var : .word 0x123456

    La façon la plus simple de l'écrire est la suivante :

    ldrr2,=511

    Dans ce cas (notez le signe "="), si la constante peut être représentée par imm_8 et encode_imm , si elle peut tenir dans le bit 12 de shifter_operand , alors le compilateur d'assembly compilera automatiquement ldr dans une instruction mov. Mais si le nombre ne peut pas être représenté de cette façon, alors le compilateur lui-même réservera une cellule mémoire dans le programme pour cette constante, donnera lui-même un nom à cette cellule mémoire et compilera la commande dans ldr .

    Voici ce que j'ai écrit :

    ldr r7, ma_var
    ldrr8,=511
    ldrr8,=1024
    ldrr9,=0x3456
    ........
    Ma_var : .word 0x123456

    Après compilation, j'obtiens ceci :

    18 : e59f7030 ldr r7, ; 50
    1c : e59f8030 ldr r8, ; 54
    20 : e3a08b01 mouvement r8, #1024 ; 0x400
    24 : e59f902c ldr r9, ; 58
    .............
    00000050 :
    50 : 00123456 .mot 0x00123456
    54 : 000001ff .mot 0x000001ff
    58 : 00003456 .mot 0x00003456

    Notez que le compilateur utilise l'adressage mémoire relatif au registre PC (alias r15).

    Lire une cellule mémoire et écrire un registre en mémoire.

    Comme je l'ai écrit plus haut, le processeur ARM ne peut effectuer que des opérations arithmétiques ou logiques sur le contenu des registres. Les données des opérations doivent être lues dans la mémoire et le résultat des opérations doit être réécrit en mémoire. Il existe des commandes spéciales pour cela : ldr (probablement issue de la combinaison « LoaD Register ») pour la lecture et str (probablement « STore Register ») pour l'écriture.

    Il semblerait qu'il n'y ait que deux équipes, mais en réalité elles présentent de nombreuses variantes. Il suffit de regarder la façon dont les commandes ldr /str sont codées sur le processeur Amber ARM pour voir combien de bits d'indicateur auxiliaires sont L 20, W 21, B 22, U 23, P 24, I 25 - et elles déterminent le comportement spécifique de la commande:

    • Le bit L20 détermine l'écriture ou la lecture. 1 - ldr, lire, 0 - str, écrire.
    • Le bit B 22 détermine la lecture/écriture d'un mot de 32 bits ou d'un octet de 8 bits. 1 signifie opération d’octet. Lorsqu'un octet est lu dans un registre, les bits de poids fort du registre sont remis à zéro.
    • Le bit I 25 détermine l'utilisation du champ Décalage. Si I 25 ==0, alors le décalage est interprété comme un décalage numérique qui doit soit être ajouté à l'adresse de base à partir du registre, soit soustrait. Mais l'ajout ou la soustraction dépend du bit U 23.

    (Cond) - condition pour effectuer l'opération. Interprété de la même manière que pour les commandes logiques/arithmétiques - la lecture ou l'écriture peut être conditionnelle.

    Ainsi, dans le texte d'assemblage, vous pouvez écrire quelque chose comme ceci :

    ldr r1, @ dans le registre r1 lire le mot à l'adresse du registre r0
    ldrb r1, @ dans le registre r1 lit l'octet à l'adresse du registre r0
    ldreq r2, @ lecture de mots conditionnelle
    ldrgtb r2, @ lecture d'octet conditionnel
    ldr r3, @ lit le mot à l'adresse 8 par rapport à l'adresse du registre r4
    ldr r4, @ lit le mot à l'adresse -16 par rapport à l'adresse du registre r5

    Après avoir compilé ce texte, vous pouvez voir les codes réels de ces commandes :

    0 : e5901000 ldr r1,
    4 : e5d01000 ldrb r1,
    8 : 05912000 ldreq r2,
    c: c5d12000 ldrbgt r2,
    10 : e5943008 ldr r3,
    14 : e5154010 ldr r4,

    Dans l'exemple ci-dessus, j'utilise uniquement ldr , mais str est utilisé à peu près de la même manière.

    Il existe des modes d'accès à la mémoire en écriture pré-index et post-index. Dans ces modes, le pointeur d'accès à la mémoire est mis à jour avant ou après l'exécution de l'instruction. Si vous êtes familier avec le langage de programmation C, vous connaissez les constructions d'accès par pointeur telles que ( *psource++;) ou ( a=*++psource;). Le processeur ARM implémente ce mode d'accès à la mémoire. Lorsqu'une commande de lecture est exécutée, deux registres sont mis à jour en même temps : le registre récepteur reçoit la valeur lue dans la mémoire et la valeur dans le registre pointeur vers la cellule mémoire est avancée ou reculée.

    Écrire ces commandes est, à mon avis, quelque peu illogique. Il faut beaucoup de temps pour s'y habituer.

    ldr r3, ! @psrc++ ; r3 = *psrc;
    ldr r3, , #4 @ r3 = *psrc; psrc++;

    La première commande ldr incrémente d'abord le pointeur, puis lit. La deuxième commande lit d'abord, puis incrémente le pointeur. La valeur du pointeur psrc est dans le registre r0.

    Tous les exemples discutés ci-dessus concernaient le cas où le bit I 25 du code de commande était réinitialisé. Mais il peut toujours être installé ! Alors la valeur du champ Offset ne contiendra pas une constante numérique, mais le troisième registre participant à l'opération. De plus, la valeur du troisième registre peut toujours être pré-décalée !

    Voici des exemples de variantes de code possibles :

    0 : e7921003 ldr r1, @ adresse de lecture - somme des valeurs des registres r2 et r3
    4 : e7b21003 ldr r1, ! @ pareil, mais après lecture, r2 sera augmenté de la valeur de r3
    8 : e6932004 ldr r2, , r4 @ il y aura d'abord une lecture à l'adresse r3, puis r3 augmentera de r4
    c : e7943185 ldr r3, @ adresse de lecture r4+r5*8
    10 : e7b43285 ldr r3, ! @ lire l'adresse r4+r5*32, après lecture, r4 sera mis à la valeur de cette adresse
    14 : e69431a5 ldr r3, , r5, lsr #3 @ adresse de lecture de r4, après l'exécution de la commande, r4 sera défini sur r4+r5/8

    Ce sont les variantes des commandes de lecture/écriture dans le processeur ARM v2a.

    Dans les anciens modèles de processeurs ARM, cette variété de commandes est encore plus grande.
    Cela est dû au fait que le processeur permet, par exemple, de lire non seulement des mots (nombres de 32 bits) et des octets, mais aussi des demi-mots (16 bits, 2 octets). Ensuite, le suffixe « h », issu du mot demi-mot, est ajouté aux commandes ldr/str. Les commandes ressembleront à ldrh ou strh . Il existe également des commandes permettant de charger des demi-mots ldrsh ou des octets ldrsb interprétés comme des nombres signés. Dans ces cas, le bit le plus significatif du mot ou de l'octet chargé est multiplié par les bits les plus significatifs du mot entier dans le registre du récepteur. Par exemple, le chargement du demi-mot 0xff25 avec la commande ldrsh dans le registre de destination donne 0xffffff25 .

    Lectures et écritures multiples.

    Les commandes ldr /str ne sont pas les seules à accéder à la mémoire. Le processeur ARM dispose également de commandes qui vous permettent d'effectuer des blocs-transferts - vous pouvez charger le contenu de plusieurs mots consécutifs depuis la mémoire et plusieurs registres à la fois. Vous pouvez également écrire séquentiellement les valeurs de plusieurs registres en mémoire.

    Les mnémoniques des commandes de transfert de blocs commencent à la racine ldm (LoaD Multiple) ou stm (Store Multiple). Mais ensuite, comme d'habitude chez ARM, l'histoire des suffixes commence.

    En général, la commande ressemble à ceci :

    op(cond)(mode) Rd(, {Register list} !}

    Le suffixe (Cond) est compréhensible, c'est une condition d'exécution de la commande. Le suffixe (mode) est le mode de transmission, nous en reparlerons plus tard. Rd est un registre qui détermine l'adresse de base en mémoire pour la lecture ou l'écriture. Un point d'exclamation après le registre Rd indique qu'il sera modifié après une opération de lecture/écriture. La liste des registres chargés depuis la mémoire ou paginés en mémoire est (Liste des registres).

    La liste des registres est spécifiée entre accolades séparées par des virgules ou sous forme de plage. Par exemple:

    stm r0,(r3,r1, r5-r8)

    La mémoire sera écrite dans le désordre. La liste indique simplement quels registres seront écrits en mémoire et c'est tout. Le code de commande contient 16 bits réservés à la liste des registres, exactement le nombre de registres dans la banque de processeurs. Chaque bit de ce champ indique quel registre participera à l'opération.

    Parlons maintenant du mode lecture/écriture. Il y a ici matière à confusion. Le fait est que différents noms de mode peuvent être utilisés pour la même action.

    Si nous faisons une petite parenthèse lyrique, alors nous devons parler de... la pile. Une pile est un moyen d'accéder aux données de type LIFO - Last In First Out (wiki) - dernier entré, premier sorti. La pile est largement utilisée en programmation lors de l'appel de procédures et de la sauvegarde de l'état des registres à l'entrée des fonctions et de leur restauration à la sortie, ainsi que lors du passage de paramètres aux procédures appelées.

    Il existe, qui l'aurait cru, quatre types de pile mémoire.

    Le premier type est Full Descendant. C'est à ce moment-là que le pointeur de pile pointe vers un élément de pile occupé et que la pile grandit vers des adresses décroissantes. Lorsque vous devez mettre un mot sur la pile, le pointeur de pile est d'abord diminué (Décrémenter avant), puis le mot est écrit à l'adresse du pointeur de pile. Lorsque vous devez supprimer un mot informatique de la pile, le mot est lu en utilisant la valeur actuelle du pointeur de pile, puis le pointeur monte (Incrément après).

    Le deuxième type est Ascendant Complet. La pile ne croît pas vers le bas, mais vers le haut, vers des adresses plus grandes. Le pointeur pointe également vers l'élément occupé. Lorsque vous devez mettre un mot sur la pile, le pointeur de pile est d'abord incrémenté, puis le mot est écrit sur le pointeur (Incrémenter avant). Lorsque vous devez supprimer de la pile, vous lisez d'abord le pointeur de pile, car il pointe vers un élément occupé, puis le pointeur de pile est diminué (Décrémenter après).

    Le troisième type est Descendant Vide. La pile croît vers le bas, comme dans le cas de Full Descending, mais la différence est que le pointeur de la pile pointe vers une cellule inoccupée. Ainsi, lorsqu'il faut mettre un mot sur la pile, une entrée est faite immédiatement, puis le pointeur de la pile est diminué (Décrémenter Après). Lors du retrait de la pile, le pointeur est d'abord incrémenté, puis lu (Incrémenter avant).

    Le quatrième type est Vide Ascendant. J'espère que tout est clair - la pile grandit. Le pointeur de pile pointe vers un élément vide. Mettre sur la pile signifie écrire un mot à l'adresse du pointeur de pile et incrémenter le pointeur de pile (Incrémenter après). Pop from stack - décrémentez le pointeur de pile et lisez le mot (Décrémenter avant).

    Ainsi, lorsque vous effectuez des opérations sur la pile, vous devez augmenter ou diminuer le pointeur - (Incrémenter/Décrémenter) avant ou après (Avant/Après) la lecture/écriture en mémoire, selon le type de pile. Les processeurs Intel, par exemple, disposent de commandes spéciales pour travailler avec la pile, telles que PUSH (mettre un mot sur la pile) ou POP (extraire un mot de la pile). Il n'y a pas d'instructions spéciales dans le processeur ARM, mais les instructions ldm et stm sont utilisées.

    Si vous implémentez la pile à l'aide des instructions du processeur ARM, vous obtenez l'image suivante :

    Pourquoi fallait-il donner des noms différents à la même équipe ? Je ne comprends pas du tout... Ici, bien sûr, il faut noter que le standard de stack pour ARM est toujours Full Descending.

    Le pointeur de pile dans un processeur ARM est le registre sp ou r13. C'est généralement l'accord. Bien sûr, l'écriture de stm ou la lecture de ldm peuvent également être effectuées avec d'autres registres de base. Cependant, vous devez vous rappeler en quoi le registre sp diffère des autres registres - il peut être différent selon les modes de fonctionnement du processeur (USR, SVC, IRQ, FIRQ), car ils ont leurs propres banques de registres.

    Et encore une remarque. Écrivez une ligne comme celle-ci dans le code assembleur ARM pousser (r0-r3), Bien sûr vous pouvez. Seulement en réalité ce sera la même équipe stmfd sp!,(r0-r3).

    Enfin, je donnerai un exemple de code assembleur et son texte démonté compilé. Nous avons:


    stmfd sp!,(r0-r3)
    stmdb sp!,(r0-r3)
    pousser (r0-r3)

    @ces trois instructions sont les mêmes et font la même chose
    pop(r0-r3)
    ldmia sp!,(r0-r3)
    ldmfd r13!,(r0-r3)

    Stmfdr4,(r0-r3,r5,r8)
    stmea r4!,(r0-r3,r7,r9,lr,pc)
    ldm r5,(r0,pc)

    Après compilation on obtient :

    0 : e92d000f pousser (r0, r1, r2, r3)
    4 : e92d000f pousser (r0, r1, r2, r3)
    8 : e92d000f pousser (r0, r1, r2, r3)
    c : e8bd000f pop (r0, r1, r2, r3)
    10 : e8bd000f pop (r0, r1, r2, r3)
    14 : e8bd000f pop (r0, r1, r2, r3)
    18 : e904012f stmdb r4, (r0, r1, r2, r3, r5, r8)
    1c : e8a4c28f stmia r4 !, (r0, r1, r2, r3, r7, r9, lr, pc)
    20 : e8958001 ldm r5, (r0, pc)

    Transitions dans les programmes.

    La programmation n'est pas possible sans transitions. Dans tout programme, il existe une exécution cyclique du code et des appels de procédures et de fonctions, ainsi qu'une exécution conditionnelle de sections de code.

    Le processeur Amber ARM v2a n'a que deux commandes : b (du mot Branch - branche, transition) et bl (Branch with Link - transition tout en conservant l'adresse de retour).

    La syntaxe de la commande est très simple :

    étiquette b(cond)
    bl(cond)étiquette

    Il est clair que toutes les transitions peuvent être conditionnelles, c'est-à-dire que le programme peut contenir des mots étranges comme ceux-ci, formés à partir des racines « b » et « bl » et des suffixes de condition (Cond) :

    beq, bne, bcs, bhs, bcc, blo, imc, bpl, bvs, bvc, bhi, bls, bge, bgt, ble, bal, b

    bleq, blne, blcs, blhs, blcc, bllo, blmi, blpl, blvs, blvc, blhi, blls, blge, blgt, blle, blal, bl

    La variété est incroyable, n'est-ce pas ?

    La commande jump contient un décalage de 24 bits. L'adresse de saut est calculée comme la somme de la valeur actuelle du pointeur PC et du numéro de décalage décalé de 2 bits vers la gauche, interprété comme un nombre signé :

    Nouveau PC = PC + Décalage*4

    Ainsi, la plage de transitions est de 32 Mo en avant ou en arrière.

    Regardons ce qu'est une transition tout en préservant l'adresse de retour bl. Cette commande est utilisée pour appeler des sous-programmes. Une caractéristique intéressante de cette commande est que l'adresse de retour de la procédure lors de l'appel de la procédure n'est pas stockée sur la pile, comme sur les processeurs Intel, mais dans le registre r14 habituel. Ensuite, pour revenir de la procédure, vous n'avez pas besoin d'une commande ret spéciale, comme avec les mêmes processeurs Intel, mais vous pouvez simplement recopier la valeur de r14 sur le PC. Il est maintenant clair pourquoi le registre r14 a un nom alternatif lr (Link Register).

    Regardons la procédure d'outbyte du projet hello-world pour le SoC Amber.

    000004a0<_outbyte>:
    4a0 : e59f1454 ldr r1, ; 8FC< адрес регистра данных UART >
    4a4 : e59f3454 ldr r3, ; 900< адрес регистра статуса UART >
    4a8 : e5932000 ldr r2, ; lire l'état actuel
    4ac : e2022020 et r2, r2, #32
    4b0 : e3520000 cmp r2, #0 ; vérifiez que l'UART n'est pas occupé
    4b4 : 05c10000 strbeq r0, ; écrire un caractère sur l'UART uniquement s'il n'est pas occupé
    4b8 : 01b0f00e movseq pc, gd ; retour conditionnel de la procédure si l'UART n'était pas occupé
    4bc : 1afffff9 bne 4a8<_outbyte+0x8>; boucle pour vérifier l'état de l'UART

    Je pense que d'après les commentaires de ce fragment, le fonctionnement de cette procédure est clair.

    Une autre remarque importante sur les transitions. Le registre r15 (pc) peut être utilisé dans des opérations arithmétiques ou logiques ordinaires comme registre récepteur. Ainsi, une commande comme add pc,pc,#8 est toute une instruction pour passer à une autre adresse.

    Une remarque supplémentaire doit être faite concernant les transitions. Les processeurs ARM plus anciens disposent également d'instructions de branchement supplémentaires bx, blx et blj. Ce sont des commandes permettant d'accéder à des fragments de code avec un système de commande différent. Bx /blx permet de passer au code THUMB 16 bits des processeurs ARM. Blj est un appel aux procédures du système d'instructions Jazelle (support du langage Java dans les processeurs ARM). Notre Amber ARM v2a n'a pas ces commandes.

    Actuellement, des langages de haut niveau sont utilisés pour programmer même des microcontrôleurs assez simples, généralement des sous-ensembles du langage C ou C++.

    Cependant, lors de l'étude de l'architecture des processeurs et de ses fonctionnalités, il est conseillé d'utiliser des langages Assembly, car seule cette approche peut assurer l'identification des fonctionnalités de l'architecture étudiée. Pour cette raison, la présentation ultérieure est effectuée en utilisant le langage Assembly.

    Avant de commencer à considérer les commandes ARM7, il est nécessaire de noter les caractéristiques suivantes :

      Prise en charge de deux jeux d'instructions : ARM avec des instructions 32 bits et THUMB avec des instructions 16 bits. Ensuite, nous considérons le jeu d'instructions 32 bits, le mot ARM désignera les instructions appartenant à ce format, et le mot ARM7 désignera le CPU lui-même.

      Prise en charge de deux formats d'adresse 32 bits : processeur big-endian et processeur small-endian. Dans le premier cas, le bit le plus significatif (Most Significant Bit - MSB) est situé dans le bit le moins significatif du mot, et dans le second cas - dans le bit le plus significatif. Cela garantit la compatibilité avec d'autres familles de processeurs 32 bits lors de l'utilisation de langages de haut niveau. Cependant, dans un certain nombre de familles de processeurs dotés d'un cœur ARM, seuls les octets petit-boutiste sont utilisés (c'est-à-dire que MSB est le bit le plus significatif de l'adresse), ce qui facilite grandement le travail avec le processeur. Étant donné que le compilateur utilisé pour ARM7 fonctionne avec du code dans les deux formats, vous devez vous assurer que le format de mot est correctement défini, sinon le code résultant sera retourné.

      Possibilité d'effectuer différents types de décalage d'un des opérandes « en passe » avant de l'utiliser dans l'ALU

      Prise en charge de l'exécution conditionnelle de n'importe quelle commande

      Possibilité d'interdire la modification des indicateurs de résultat d'opération.

        1. Exécution conditionnelle des commandes

    L'une des caractéristiques importantes du jeu d'instructions ARM est qu'il prend en charge l'exécution conditionnelle de n'importe quelle instruction. Dans les microcontrôleurs traditionnels, les seules commandes conditionnelles sont les commandes de saut conditionnel, et peut-être un certain nombre d'autres, telles que les commandes permettant de tester ou de modifier l'état de bits individuels. Dans le jeu d'instructions ARM, les 4 bits les plus significatifs du code d'instruction sont toujours comparés aux indicateurs de condition dans le registre CPSR. Si leurs valeurs ne correspondent pas, la commande à l'étape de décryptage est remplacée par une commande NOP (pas d'opération).

    Cela réduit considérablement le temps d'exécution des sections de programme avec des transitions « courtes ». Ainsi, par exemple, lors de la résolution d'équations quadratiques avec des coefficients réels et des racines arbitraires avec un discriminant négatif, avant de calculer la racine carrée, il est nécessaire de changer le signe du discriminant et d'attribuer le résultat à la partie imaginaire de la réponse.

    La solution traditionnelle à ce problème consiste à saisir une commande de saut conditionnel. L'exécution de cette commande prend au moins 2 cycles d'horloge - décryptage et chargement de la nouvelle valeur d'adresse dans le compteur du programme et un nombre supplémentaire de cycles d'horloge pour charger le pipeline de commandes. Lors de l'utilisation d'une exécution de commande conditionnelle avec un discriminant positif, la commande de changement de signe est remplacée par une opération vide. Dans ce cas, le pipeline de commandes n'est pas effacé et la perte ne dépasse pas un cycle. Le seuil auquel le remplacement des commandes conditionnelles par une commande NOP est plus efficace que l'exécution de commandes de saut conditionnelles traditionnelles et le remplissage associé du pipeline est égal à sa profondeur, c'est-à-dire trois.

    Pour implémenter cette fonctionnalité, vous devez ajouter l'un des seize préfixes qui définissent les états testés des indicateurs de condition aux désignations mnémoniques de base des commandes assembleur (et du C également). Ces préfixes sont donnés dans le tableau. 3. En conséquence, il existe 16 options pour chaque commande. Par exemple, la commande suivante :

    MOVEQR1, #0x008

    signifie que le nombre 0x00800000 sera chargé dans le registre R1 uniquement si le résultat de la dernière commande de traitement des données était « égal » ou si un résultat 0 a été obtenu et que le drapeau (Z) du registre CPSR est positionné en conséquence.

    Tableau 3

    Préfixes de commande

    Signification

    Z installé

    Z réinitialiser

    Avec installé

    Supérieur ou égal à (non signé)

    C réinitialiser

    Ci-dessous (non signé)

    N installé

    Résultat négatif

    N réinitialiser

    Résultat positif ou 0

    V installé

    Débordement

    V réinitialiser

    Pas de débordement

    Avec installé,

    Z réinitialiser

    Ci-dessus (non signé)

    Avec réinitialisation,

    Z installé

    Inférieur ou égal à (non signé)

    Supérieur ou égal à (signé)

    N n'est pas égal à V

    Moins (signe)

    Z réinitialiser ET

    (N est égal à V)

    Plus (emblématique)

    Z réglé OU

    (N n'est pas égal à V)

    Inférieur ou égal à (signé)

    (ignoré)

    Exécution inconditionnelle

    Si vous utilisez la distribution Raspbian comme système d'exploitation de votre Raspberry Pi, vous aurez besoin de deux utilitaires, à savoir as (un assembleur qui convertit le code source du langage assembleur en code binaire) et ld (un éditeur de liens qui crée le fichier exécutable résultant). Les deux utilitaires sont inclus dans le progiciel binutils, ils peuvent donc déjà être présents sur votre système. Bien sûr, vous aurez également besoin d’un bon éditeur de texte ; Je recommande toujours d'utiliser Vim pour le développement de programmes, mais il a une barrière d'entrée élevée, donc Nano ou tout autre éditeur de texte GUI fonctionnera très bien.

    Prêt à commencer? Copiez le code suivant et enregistrez-le dans le fichier myfirst.s :

    Global _start _start : mov r7, #4 mov r0, #1 ldr r1, =string mov r2, #stringlen swi 0 mov r7, #1 swi 0 .data string : .ascii "Ciao!\n" stringlen = . -chaîne

    Ce programme imprime simplement la chaîne "Ciao!" à l'écran, et si vous avez lu des articles sur l'utilisation du langage assembleur pour travailler avec des processeurs x86, certaines des instructions utilisées vous sont peut-être familières. Mais il existe néanmoins de nombreuses différences entre les instructions des architectures x86 et ARM, qui peuvent également être mentionnées dans la syntaxe du code source, nous allons donc l'analyser en détail.

    Mais avant cela, il convient de mentionner que pour assembler le code donné et lier le fichier objet résultant dans un fichier exécutable, vous devez utiliser la commande suivante :

    Comme -o monpremier.o monpremier.s && ld -o monpremier monpremier.o

    Vous pouvez maintenant exécuter le programme créé à l'aide de la commande ./myfirst . Vous avez peut-être remarqué que le fichier exécutable est d'une taille très modeste, d'environ 900 octets - si vous utilisiez le langage de programmation C et la fonction puts(), la taille du fichier binaire serait environ cinq fois plus grande !

    Créer votre propre système d'exploitation pour Raspberry Pi

    Si vous avez lu les articles précédents de cette série sur la programmation en langage assembleur x86, vous vous souvenez probablement de la première fois que vous avez exécuté votre propre système d'exploitation, affichant un message à l'écran sans l'aide de Linux ou de tout autre système d'exploitation. Après cela, nous l'avons amélioré en ajoutant une interface de ligne de commande simple et un mécanisme permettant de charger et d'exécuter des programmes à partir du disque, laissant ainsi une base pour l'avenir. C'était un travail très intéressant, mais pas très difficile, principalement grâce à l'aide du micrologiciel du BIOS - il fournissait une interface simplifiée pour accéder à l'écran, au clavier et au lecteur de disquettes.

    Avec le Raspberry Pi, vous n'aurez plus à votre disposition des fonctionnalités utiles du BIOS, vous devrez donc développer vous-même les pilotes de périphériques, ce qui en soi est un travail difficile et sans intérêt par rapport au dessin sur l'écran et à la mise en œuvre du moteur pour exécuter vos propres programmes. . Dans le même temps, il existe plusieurs guides sur le réseau qui décrivent en détail les étapes initiales du processus de démarrage du Raspberry Pi, les caractéristiques du mécanisme d'accès aux broches GPIO, etc.

    L'un des meilleurs documents de ce type est un document intitulé Baking Pi (www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/index.html) de l'Université de Cambridge. Il s'agit essentiellement d'un ensemble de didacticiels décrivant les techniques permettant de travailler avec le langage assembleur pour allumer les LED, accéder aux pixels de l'écran, recevoir des entrées au clavier, etc. Vous en apprendrez beaucoup sur le matériel Raspberry Pi au fil de votre lecture, et les guides ont été rédigés pour les modèles originaux de ces ordinateurs monocarte, il n'y a donc aucune garantie qu'ils seront pertinents pour des modèles tels que les A+, B+ et Pi 2.

    Si vous préférez le langage de programmation C, vous devez vous référer au document de la ressource Valvers, situé sur http://tinyurl.com/qa2s9bg et contenant une description du processus de configuration d'un compilateur croisé et de construction d'un système d'exploitation simple. noyau, et dans la section Wiki de la ressource OSDev utile, située sur http://wiki.osdev.org/Raspberry_Pi_Bare_Bones pour plus d'informations sur la façon de créer et d'exécuter un noyau de système d'exploitation de base sur un Raspberry Pi.

    Comme mentionné ci-dessus, le plus gros problème dans ce cas est la nécessité de développer des pilotes pour divers périphériques matériels Raspberry Pi : contrôleur USB, emplacement pour carte SD, etc. Après tout, même le code des appareils mentionnés peut prendre des dizaines de milliers de lignes. Si vous souhaitez néanmoins développer votre propre système d'exploitation complet pour le Raspberry Pi, vous devez visiter les forums sur www.osdev.org et demander si quelqu'un a déjà développé des pilotes pour ces périphériques et, si possible, les adapter à votre noyau. système d'exploitation, économisant ainsi une grande partie de votre temps.

    Comment ça fonctionne

    Les deux premières lignes de code ne sont pas des instructions CPU, mais des directives assembleur et éditeur de liens. Chaque programme doit avoir un point d'entrée clairement défini appelé _start, et dans notre cas c'était au tout début du code. Ainsi, nous informons l'éditeur de liens que l'exécution du code doit commencer par la première instruction et qu'aucune action supplémentaire n'est requise.

    Avec l'instruction suivante, nous mettons le chiffre 4 dans le registre r7. (Si vous n'avez jamais travaillé avec le langage assembleur auparavant, sachez qu'un registre est un emplacement mémoire situé directement dans l'unité centrale de traitement. La plupart des unités centrales de traitement modernes implémentent un petit nombre de registres par rapport aux millions ou milliards d'emplacements mémoire, mais les registres sont indispensables car ils fonctionnent beaucoup plus rapidement.) Les puces d'architecture ARM fournissent aux développeurs un grand nombre de registres à usage général : un concepteur peut utiliser jusqu'à 16 registres nommés r0 à r15, et ces registres ne sont associés à aucune restriction historique, car dans le cas de l'architecture x86, où certains des registres peuvent être utilisés à certaines fins à certains moments.

    Ainsi, bien que l'instruction mov soit très similaire à l'instruction x86 du même nom, vous devez quand même faire attention au symbole dièse à côté du chiffre 4, indiquant que ce qui suit est une valeur entière et non une adresse mémoire. Dans ce cas, nous souhaitons utiliser l'appel système d'écriture du noyau Linux pour imprimer notre chaîne ; Pour utiliser les appels système, vous devez remplir les registres avec les valeurs nécessaires avant de demander au noyau de faire son travail. Le numéro d'appel système doit être placé dans le registre r7, le numéro 4 étant le numéro d'appel système d'écriture.

    Avec l'instruction mov suivante, nous plaçons le descripteur de fichier dans lequel la chaîne "Ciao!" doit être écrite, c'est-à-dire le descripteur de flux de sortie standard, dans le registre r0. Puisque le flux de sortie standard est utilisé dans ce cas, son descripteur standard, c'est-à-dire 1, est placé dans le registre. Ensuite, nous devons placer l'adresse de la chaîne que nous voulons afficher dans le registre r1 à l'aide de l'instruction ldr (une instruction "charger dans le registre" ; notez le signe égal indiquant que ce qui suit est une étiquette plutôt qu'une adresse). A la fin du code, notamment dans la section data, on déclare cette chaîne sous la forme d'une séquence de caractères ASCII. Pour utiliser avec succès l'appel système "write", nous devons également indiquer au noyau du système d'exploitation la longueur de la chaîne de sortie, nous mettons donc la valeur de stringlen dans le registre r2. (La valeur de stringlen est calculée en soustrayant l'adresse de fin de la chaîne de l'adresse de début.)

    À ce stade, nous avons rempli tous les registres avec les données nécessaires et sommes prêts à transférer le contrôle au noyau Linux. Pour ce faire, nous utilisons l'instruction swi, dont le nom signifie « interruption logicielle », qui saute dans l'espace du noyau du système d'exploitation (presque de la même manière que l'instruction int dans les articles sur l'architecture x86). Le noyau du système d'exploitation examine le contenu du registre r7, y trouve la valeur entière 4 et conclut : "Donc, le programme appelant veut imprimer une chaîne." Après cela, il examine le contenu des autres registres, imprime une chaîne et rend le contrôle à notre programme.

    Ainsi, nous voyons la ligne « Ciao ! » à l'écran, après quoi nous ne pouvons que terminer correctement l'exécution du programme. Nous résolvons ce problème en plaçant le numéro d’appel système de sortie dans le registre r7, puis en appelant le numéro d’instruction d’interruption logicielle zéro. Et c'est tout - le noyau du système d'exploitation termine l'exécution de notre programme et nous revenons au shell de commande.

    Vim (à gauche) est un excellent éditeur de texte pour écrire du code en langage assembleur - un fichier pour la coloration syntaxique de ce langage pour l'architecture ARM est disponible sur http://tinyurl.com/psdvjen.

    Conseil: Lorsque vous travaillez avec le langage assembleur, vous ne devez pas lésiner sur les commentaires. Nous n'avons pas utilisé un grand nombre de commentaires dans cet article afin de garantir que le code prenne le moins de place possible sur les pages du magazine (et aussi parce que nous avons décrit en détail le but de chacune des instructions). Mais lorsque vous développez des programmes complexes dont le code semble évident à première vue, vous devez toujours penser à ce à quoi il ressemblera après avoir partiellement oublié la syntaxe du langage assembleur ARM et revenir au développement après quelques mois. Vous pouvez oublier toutes les astuces et raccourcis utilisés dans le code, après quoi le code ressemblera à un charabia complet. Sur la base de tout ce qui précède, vous devez ajouter autant de commentaires que possible à votre code, même si certains d’entre eux semblent trop évidents pour le moment !

    Ingénierie inverse

    La conversion d'un fichier binaire en code en langage assembleur peut également être utile dans certains cas. Le résultat de cette opération n'est généralement pas un code de très haute qualité sans noms d'étiquettes et commentaires lisibles, qui peuvent néanmoins être utiles pour étudier les transformations effectuées par l'assembleur sur votre code. Pour démonter le binaire myfirst, exécutez simplement la commande suivante :

    Objdump -d monpremier

    Cette commande désassemblera la section de code exécutable du fichier binaire (mais pas la section de données, car elle contient du texte ASCII). Si vous regardez le code obtenu à la suite du démontage, vous remarquerez probablement que les instructions qu'il contient sont pratiquement les mêmes que celles du code d'origine. Les désassembleurs sont principalement utilisés lorsque vous devez étudier le comportement d'un programme disponible uniquement sous forme de code binaire, tel qu'un virus ou un simple programme fermé dont vous souhaitez émuler le comportement. En même temps, n'oubliez jamais les restrictions imposées par l'auteur du programme à l'étude ! Désassembler un fichier de programme binaire et simplement copier le code résultant dans le code de votre projet est, bien sûr, une mauvaise idée ; en même temps, vous pouvez tout à fait utiliser le code obtenu pour étudier le principe de fonctionnement du programme.

    Sous-programmes, boucles et instructions conditionnelles

    Maintenant que nous savons comment concevoir, assembler et relier des programmes simples, passons à quelque chose de plus complexe. Le programme suivant utilise des sous-programmes pour imprimer des chaînes (grâce à eux, nous pouvons réutiliser des fragments de code et nous éviter d'avoir à effectuer les mêmes opérations de remplissage des registres avec des données). Ce programme implémente une boucle d'événements principale qui permet la sortie d'une chaîne jusqu'à ce que l'utilisateur entre "q". Étudiez le code et essayez de comprendre (ou de deviner !) le but des instructions, mais ne désespérez pas si vous ne comprenez pas quelque chose, car un peu plus tard, nous l'examinerons également en détail. Notez que les symboles @ dans le langage assembleur ARM mettent en évidence les commentaires.

    Global _start _start : ldr r1, =string1 mov r2, #string1len bl print_string boucle : mov r7, #3 @ read mov r0, #0 @ stdin ldr r1, =char mov r2, #2 @ deux caractères swi 0 ldr r1, =char ldrb r2, cmp r2, #113 @ code ASCII pour "q" beq done ldr r1, =string2 mov r2, #string2len bl print_string b boucle terminée : mov r7, #1 swi 0 print_string : mov r7, #4 mov r0, #1 swi 0 bx lr .data string1 : .ascii "Entrez q pour quitter !\n" string1len = . - string1 string2 : .ascii "Ce n'était pas q...\n" string2len = . - string2 char : .word 0

    Notre programme commence par placer un pointeur sur le début de la chaîne et sa longueur dans les registres appropriés pour l'exécution ultérieure de l'appel système d'écriture, immédiatement après quoi il passe au sous-programme print_string situé en dessous du code. Pour effectuer cette transition, on utilise l'instruction bl, dont le nom signifie « branche et lien » (« branche avec préservation d'adresse »), et elle stocke elle-même l'adresse actuelle dans le code, ce qui permet d'y revenir ultérieurement en utilisant l'instruction bx. La routine print_string remplit simplement d'autres registres pour effectuer l'appel système d'écriture de la même manière que notre premier programme avant de sauter dans l'espace du noyau du système d'exploitation, puis de revenir à l'adresse de code stockée à l'aide de l'instruction bx.

    En revenant au code appelant, nous pouvons trouver une étiquette appelée loop - le nom de l'étiquette laisse déjà entendre que nous y reviendrons dans un moment. Mais nous utilisons d’abord un autre appel système appelé read (numéroté 3) pour lire le caractère saisi par l’utilisateur à l’aide du clavier. Nous mettons donc la valeur 3 dans le registre r7 et la valeur 0 (descripteur d'entrée standard) dans le registre r0 puisque nous devons lire les entrées de l'utilisateur et non les données d'un fichier.

    Ensuite, nous plaçons l'adresse où nous voulons stocker le caractère lu et placé par le noyau du système d'exploitation dans le registre r1 - dans notre cas, il s'agit de la zone mémoire char décrite à la fin de la section données. (En fait, nous avons besoin d'un mot machine, c'est-à-dire d'une zone mémoire pour stocker deux caractères, car il stockera également le code de la touche Entrée. Lorsque vous travaillez avec le langage assembleur, il est important de toujours se rappeler la possibilité de débordement de mémoire. domaines, car il n’existe pas de mécanismes de haut niveau prêts à vous venir en aide !).

    En revenant au code principal, on voit que la valeur 2 est placée dans le registre r2, correspondant aux deux caractères que l'on veut stocker, puis on saute dans l'espace noyau pour effectuer l'opération de lecture. L'utilisateur saisit un caractère et appuie sur la touche Entrée. Nous devons maintenant vérifier quel est le caractère : nous mettons l'adresse de la zone mémoire (char dans la section données) dans le registre r1, puis utilisons l'instruction ldrb pour charger l'octet de la zone mémoire pointée par la valeur dans ce registre.

    Les crochets dans ce cas indiquent que les données sont stockées dans la zone mémoire qui nous intéresse, et non dans le registre lui-même. Ainsi, le registre r2 contient désormais un seul caractère de la zone mémoire de caractères de la section de données, et c'est exactement le caractère saisi par l'utilisateur. Notre prochaine tâche sera de comparer le contenu du registre r2 avec le caractère "q", qui est le 113ème caractère de la table ASCII (se référer à la table de caractères située sur www.asciichart.com). Nous utilisons maintenant l'instruction cmp pour effectuer l'opération de comparaison, puis utilisons l'instruction beq, qui signifie "branche si égal", pour passer à l'étiquette terminée si la valeur dans le registre r2 est 113. Si ce n'est pas le cas , puis nous imprimons notre deuxième ligne, après quoi nous sautons au début de la boucle en utilisant l'instruction b.

    Enfin, après la marque Terminé, nous indiquons au noyau du système d'exploitation que nous voulons terminer le programme, tout comme dans le premier programme. Pour exécuter ce programme, il suffit de l'assembler et de le relier selon les instructions données pour le premier programme.

    Nous avons donc examiné une assez grande quantité d'informations sous la forme la plus condensée, mais il serait préférable que vous étudiiez le matériel par vous-même, en expérimentant le code ci-dessus. Il n'y a pas de meilleure façon de se familiariser avec un langage de programmation qu'en menant des expériences qui impliquent de modifier le code de quelqu'un d'autre et d'observer l'effet obtenu. Vous pouvez désormais développer des programmes simples en langage assembleur ARM qui lisent les données d'entrée et de sortie de l'utilisateur à l'aide de boucles, de comparaisons et de sous-programmes. Si vous n'avez jamais rencontré le langage assembleur avant aujourd'hui, j'espère que cet article vous a rendu le langage un peu plus compréhensible et a contribué à dissiper le stéréotype populaire selon lequel il s'agit d'un métier mystique réservé seulement à quelques développeurs talentueux.

    Bien entendu, les informations fournies dans l'article concernant l'utilisation du langage assembleur pour l'architecture ARM ne sont que la pointe de l'iceberg. L'utilisation de ce langage de programmation est toujours associée à un grand nombre de nuances et si vous souhaitez que nous en parlions dans l'un des articles suivants, faites-le nous savoir ! En attendant, nous vous recommandons de visiter une excellente ressource contenant de nombreux documents pour apprendre les techniques de création de programmes pour les systèmes Linux fonctionnant sur des ordinateurs équipés de processeurs centraux d'architecture ARM, située à l'adresse http://tinyurl.com/nsgzq89. Bonne programmation !

    Articles précédents de la série « Assembly School » :

    Avez-vous aimé l'article? Partager avec des amis: