Assembleur ARM

Introduction

L'assembleur est un langage d'ultra bas niveau juste au dessus du langage machine.

Les instructions font appels aux appels systèmes et aux registres.

La syntaxe pour l'assembleur ARM est la suivante :

    .equ nom, valeur    @Déclaration de constantes
                        @appelés : #constante
    .arm		@Type d’assemblage

@ ------------------ Partie où on traite les données------------------------------ @
    .data		@ déclarations …
    
nom: type valeur        @ déclaration d’un pointeur
nom= valeur             @ déclaration d’une variable
@ -------------------- Partie où on écrit les instructions --------------------------- @
    .text
    .global _start      @étiquette liée au début du code

_start :                @ début du code
    @instructions…
.end                    @fin

Compilation

#Phase d’assemblage
as -gstabs -o file.o file.s
#Phase d’édition de liens
ld -O0 file.o
#Lancement
./a.out

Notez que as génère un fichier .o, donc un fichier objet comme en c avec gcc -c. Vous pouvez donc écrire du code en c, avec des fonctions écrites en assembleur, ça marche !

Types

Il existe 3 types :

  • .ascii convertir une chaîne selon code ascii
  • .asciz convertir une chaîne selon code ascii avec ‘’ à la fin
  • .word 4 octets (int)

Une variable peut être soit une valeur soit une adresse

tab: .word 1,5,7,46,89        @ déclaration d’un pointeur
taille: . 		      @ taille du tableau ; . contient le nombre de valeur écrite juste avant donc 5 ici
Registres

Toutes les opérations se font en manipulant des registres. Vous stocker des variables dans des registres et appelez des appels système ou transmettez des variables à des fonctions

  • les registres r0-r6 (compris) et r8-r10 (compris) sont utilisés pour stocker des valeurs.
  • le registre r7 contient la valeur de l'appel système
  • le registre r12 contient ip (Intra Procedural Call)
  • le registre r13 contient sp (Stack Pointer)
  • le registre r14 contient lr (Link Register)
  • le registre r15 contient pc (Program Counter)

Appel système : write

On ne place pas les valeurs dans des registres aléatoire.

L'appel système int write(1, "test", 5); par exemple prends un premier argument 1, donc on met 1 dans r0 ; en second argument test, donc on met test dans r1 etc...

Il faut ensuite mettre dans le registre r7, la valeur correspond à l'appel système write (chercher avec man) par exemple 4 (sous ubuntu).

r0 contient 1
r1 contient "test"
r2 contient 5

r7 contient 4

On procède à l'appel de write avec l'appel swi

.equ SYS_EXIT, 1
.equ SYS_WRITE, 4
	.arm
	.data
	
texte: .asciz "test"
texte_len: .word 5

	.text
	.global _start
_start :
    mov r0, #1
    ldr r1, =texte
    ldr r2, =texte_len
    mov r7, #SYS_WRITE
    
    swi #0	@software interaction, execution de l'appel système 4 avec (1,"test",5);
    
    @rappel, write retourne le nombre de caractères écris, soit ici 5 normalement
    @cette valeur est sauvegardé dans r0 après l'appel de swi
    @de façon générale, les registres sont remplis par les fonctions/appels systèmes avec des résultats
    
    @quitte le programme
    
    mov r0, #0
    mov r7,#SYS_EXIT	
    swi #0
.end

Les appels systèmes

Plus d’infos : less /usr/include/arm-linux-gnueabihf/asm/unistd.h

  1. sys_exit : void exit(int status);
  2. sys_fork : pid_t fork(void); (créer processus fils)
  3. sys_read : ssize_t read(int fd, void *buf, size_t count);
  4. sys_write : ssize_t write(int fd, const void *buf, size_t count);
  5. sys_open : int open(const char *pathname, int flags, mode_t mode);
  6. sys_close : int close(int fd);
Conditions

Il existe 4 'flags' (drapeaux) NZCV qui contiennent des booléens valant true s'ils ont étés levés par une opération

  • N: résultat est négatif ?
  • Z: résultat vaut zéro ?
  • V: bit de sortie vaut 1 ? (dernière retenue d'une opération de bits, voir cours linux)
  • C: il y a eu débordement ? (voir cours linux)

Leur utilité principale est qu'il est possible de tester des conditions. En fonction du résultat d'une condition, vous pouvez ou non effectuer du code.

Tester une condition

@Comparaison : Rn - Operand2
cmp Rn, Operand2

@Comparaison : Rn + Operand2
cmn Rn, Operand2

@Fait un ET logique des bits
tst Rn, Operand2

@Fait un ou exclusif des bits
teq Rn, Operand2

Effectuer du code selon une condition

Il existe des opérateurs pouvant être ajoutés après le nom de chaque opération, par exemple si on veut faire une addition si égalité (eq) alors on met addeq.

mov r0, #6 @mets 6 dans r0
@on va comparer si 6 ? = ? 6
@ce qui mets à jour les flags
cmp r0, #6

@si r0 contient bien 6, alors on ajoute 6
addeq r0, #6
@sinon on ajoute 4 (ne = not equals)
addne r0, #4

Les opérateurs sont

NZCV {cond} NZCV Note
al Rien On fait toujours
0000 eq Z précédent test vaut 0
0001 ne !Z précédent test différent de 0
0100 mi N négatif
0101 pl !N positif
1010 ge N==V Supérieur ou égal
1011 lt N !=V Inférieur
1100 gt !Z AND (N==V) Supérieur
1101 le Z or (N != V) Inférieur ou égal
CS, CC C, !C Retenue, pas de retenue
VS, VC V, !V Débordement, pas de débordement

En temps normal, une opération ne modifie pas les valeurs NZCV mais il est possible de faire en sorte que ce soit le cas en ajoutant un s après l'éventuelle condition

mov r0, #2 @mets 2 dans r0, ne modifie pas NZCV
cmp r0, #6 @comparer 2 égal 6 ?, modifie NZCV

addnes r0, #4 @si r0 != 6, alors on ajoute 4. modifie NZCV

Effectuer un bloc de code selon une condition

b de branchement est l'équivalent d'un goto en C et permet de se déplacer à un étiquette.

@ while(r != 0) { ...; r--; }
boucle:
	cmp r0, #0
	beq exit @si la condition est vraie, alors on quitte
	
	@mise à jour de r0
	sub r0, r0, #1
	
	@code de la boucle
	b boucle

exit:
	@...
Opérateurs

Les opérations possibles avec les registres sont

Déplacement
 mov{cond}{s} Rd, Operand2 
Déplace une constante/ contenu dans Rd
 ldr{cond}{size} Rd, {address} 
Met dans Rd la valeur à address
 str{cond}{size} Rd, {address} 
Met à l'{address} la valeur contenue dans Rd
Calculs
 add{cond}{s} Rd,Rn,Operand2 
Rd = Rn + Operand2
 sub{cond}{s} Rd,Rn,Operand2 
Rd = Rn - Operand2
 rsb{cond}{s} Rd,Rn,Operand2 
Rd = Operand2 - Rn
.equ SYS_EXIT, 1

	.arm
	.data

tab:	 .word 1,2,3,4,5
sz:      .word 5

	.text
	.global _start
_start :
	@exemple : mov
	mov r0, r1 @met la valeur de r1 dans r0
	mov r0, #5 @met une constante immédiate dans r0
	mov r0, #SYS_EXIT @met une constante dans r0, avec SYS_EXIT une constante
	
	@exemple : ldr
	ldr r0, tab  @met une adresse dans r0
	ldr r0, =tab @met la valeur contenue dans à une adresse dans r0 soit 1
	ldr r1, [r0] @met dans r1, la valeur contenu à l'adresse contenue dans r0
	ldrb r1, [r0] @met dans r1, la valeur contenue du premier octet de l'adresse contenue dans r0
	ldrh r1, [r0] @met dans r1, la valeur contenue des 2 premiers octets de l'adresse contenue dans r0
	
	str r1, [r0] @met le contenue de r1 à l'adresse contenue dans r0
	
.end

Multiplications

 mul{s}{c} Rd, Rn, Rm 
multiplication de nombre donnant un nombre sur 32bits
 smull{s}{c} RdLo, RdHi, Rn, Rm 
multiplication d'entiers signés
 umull{s}{c} RdLo, RdHi, Rn, Rm 
multiplication d'entiers non signés, rdLo : 32 bits de poids faible ; rdHi : 32 bits de poids fort
Fonctions

Déclaration

	.arm
	.data
	.text
	
	@déclaration de la fonction
	@pas forcément dans un autre fichier
	
	@ceci est une fonction max, sa signature est
	@int max(int nombre1, int nombre2);
	.global max
max:
	@code de la fonction
	@...	
.end

Appel de la fonction

Pour appeler une fonction, tout se passe comme pour un appel système : vous mettez les arguments dans les registres et le résultat est placé dans des r0, ...

	@...
	mov r0, #5 @nombre1 = 5
	mov r1, #13 @nombre2 = 13
	
	bl max; @appel de max(5,13)
	
	@r0 contient le résultat soit 13
	
	@...

Program counter et Link register : comment coder une fonction ?

Pour coder une fonction, vous allez devoir utiliser la pile car il est possible que vous appeliez d'autres fonctions et qu'elles foutent le bazar dans vos registres ou encore que vous ayez un programme qui tourne à l'infini parce qu'il n'arrive pas à retrouver son chemin.

Link register ou lr correspond à un registre qui contient la prochaine instruction à exécuter.

Program counter ou pc correspond à un registre qui contient l'instruction exécuté.

@pc contient 4 par exemple, lr contient 5
4: mov r1, #13
@pc contient 5 par exemple, lr contient 6
5: bl max;
6: cmp r0, #13

@ et dans max

max:
	@pc contient 10 lr contient 6
	10: mov r5, r0
	@pc contient 11, lr contient 12
	@...
	15: bl sprintfd @appel de printf
	@pc contient 16, lr contient 17
	@...
	@le problème est que après la dernière instruction, il faut donner
	@ à pc la valeur 6, pour qu'il retourne dans le programme principal
	@cependant printf peut modifier tous les registres, donc on ne vas pas sauvegarder
	@lr dans un registre mais dans la pile
.end

Fonctionnement de la pile

push {lr} @empile des registres
pop {pc} @dépile des registres

@plusieurs
@notez que l'ordre est croissant !!! 0 puis 1 puis ... ;
@possible uniquement pour r0-r7 et lr (push) ; pc (pop)
push {r0, r1, r2}
push {r0-r2}
Constante immédiate

Les constantes immédiates, de la forme #nombre, sont particulières car elles sont codées sur 12 bits, donc toutes les valeurs ne sont pas représentables.

  • les 4 premier bits contiennent la rotation, il faut faire 2^rotation pour obtenir la vraie rotation à appliquer au 8 bits.
  • les 8 autres bits contiennent le nombre

On ne peux représenter que les nombres pour lesquels l'espace entre le premier et le dernier bit à 1 est inférieur ou égal à 8 et donc le nombre de décalages se note 2^rotation.

Retrouver un nombre depuis une constante immédiate

//ex rotation:0001, nombre:00000001
0001 00000001
//on met les 8 bits sur 32 bits
00000000 00000000 00000000 00000001
//on calcule la rotation
0001 en base 2 = 1 en base 10. Rotation = 2^1 = 2
//applique la rotation
10000000 00000000 00000000 00000000 //1
01000000 00000000 00000000 00000000 //2

Convertir un nombre en une constante immédiate

On ne peux représenter que les nombres pour lesquels l'espace entre le premier et le dernier bit à 1 est inférieur ou égal à 8 et donc le nombre de décalages se note 2^rotation.

//nombre au hasard : 748 326
00000000 00001011 01101011 00100110 //invalide, 19 entre 1er et dernier 1

//nombre au hasard : 32000
00000000 00000000 01111101 00000000 //test 1 : ok, écart = 7
//test 2 : décalage s'écrit de la forme 2^nombre ?
//on choisi nos 8 bits 'comme on veut' (on ne modifie pas l'ordre) et on les mets à droite
00000000 00000000 00000000 01111101 //pour remettre ce bloc à sa place, il faut décaler de 24 bits
//or on ne peut pas écrire 2^n = 24, test 2 : invalide.

//nombre : -58
10000000 00000000 00000000 00111010 //écart = 7, test 1 : ok
//on choisi nos 8 bits comme on veut, ici j'ai déplacé le 10 de tout à gauche, tout à droite
00000000 00000000 00000000 11101010 //on voit ici que décaler le 10 de 2 nous redonne le nombre de départ
//démo:
00000000 00000000 00000000 01110101 //1
10000000 00000000 00000000 001110100 //2
//et 2^1 = 2 ! donc la rotation vaut 1.
//
//constante : rotation = 0001 = 1 ; nombre = 11101010 = 234
//soit  0001 11101010
Barrel Shifter

Il est possible d’effectuer des opérations simples (décalages) sur Operand2 (seulement). On peut effectuer ces décalages dans une opération ou directement en appelant le décalage

@ ex
{opération}{cond}{S} Rd,Rn,Rb, {shift_type} {shift_amount}
@ou alors
{shift_type} Rd, Rb,{shift_amount}

Remarque : Rb n’est pas modifié.

{shift_types} Sémantique
LSL (Logical Shift Left) On avance de {shift_amount} vers la gauche, les bits en trop à gauche sont perdus, les bits à droite sont remplacés par des zéros (multiplication par 2{shift_amount})
LSR (Logical Shift Right) On avance de {shift_amount} vers la droite, les bits en trop à droite sont perdus, les bits à gauche sont remplacés par des zéros (division non signée par 2{shift_amount})
ASR (Arithmetic shift right) On repère {shift_amount} fois le bit de signe, puis on écrit Rb, les bits en trop à droite sont perdus. (division signée par 2{shift_amount})
ROR (rotate right) On met les {shift_amount} derniers bit de Rb, puis on écrit Rb, les bits en trop à droite sont perdus.
RRX (rotate right Extended) On fait une rotate right mais avec une étape supplémentaire : on déplace l’ancien bit de signe au début et l’ancien dernier à la place du bit de signe {shift_amount} fois.
@exemple : addition de r0 = r0 + r1 ; avec r1 décalé de 2 positions
add r0, r0, r1, lsl #2 

Par exemple, on peut utiliser le barrel shifter pour lire des valeurs d'un tableau sans le modifier !

Pre-indexed

On fait un calcul sur l'adresse avant d'accéder à la mémoire.

ldr r3, [r2, #-8]
@ | cond | 010 | 0 | 0 | 0 | 1 | 0010 | 0011 | 0000 0000 1000 |
@ condition (eq, ...) | 010 = code de ldr | pré-indexed ? | unsigned ? | write back ? |
@ 1 | r2 (registre shifté) | r3 (registre destination) | constante

@autre exemple
ldr r3, [r2, r1, lsl#16] @r3 = r2 + r1 * 2^16
@ | cond | 010 | 1 | 0 | 0 | 1 | 0010 | 0011 | 10 000 | 00 | 0001 |
@ condition (eq, ...) | 010 = code de ldr | pré-indexed ? | unsigned ? | write back ? |
@ 1 | r2 | r3 | shift amount : 16 | type de shift : 00 = lsl | r1

On peut écraser le registre shifté avec la valeur après rotation en ajoutant un '!' .

ldr r3, [r2, #-8]!

Post-indexed

On accède à la mémoire puis fait un calcul sur l'adresse et de sauvegarder la modification dans le registre shifté.

ldr r3, [r2], r1