aboutsummaryrefslogtreecommitdiff
path: root/semestre 3/architecture des ordinateurs/2- Programmation en ASM Mips.md
blob: 67d68238c1b307d80290d1432e821d16c9febfe2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
---
tags:
  - sorbonne
  - informatique
  - architecture-des-ordinateurs
semestre: 3
---
## Registres
Un registre de $n$ bits est un composant capable de mémoriser un mot binaire de $n$ bits
|> changement de valeur possible uniquement lors de front montant/descendant du signal de l’horloge
|> émission de la valeur contenue dans le registre en continu
|> les registres dépendent du processeur
-> contient toutes les informations utilisées par le processeur

Tous les registres du Mips font 32 bits et en possèdent 32
|> les registres sont nommés par leur numéro

PC (Program Counter) = adresse de l’instruction en cours d’exécution (ou la suivante)
|> modifié après l’exécution de chaque instruction

IR (Instruction Register) = instruction en cours de traitement

HI/LO (High/Low) = les registres contenant le résultat d’opérations de multiplication ou de division

Il y a d'autres registres qu'on n'utilisera pas en cours

> [!info] L'architecture moderne du Mips est le RISC-V

Processus d'exécution :
1. Lire une instruction en mémoire (dans IR)
2. Décoder l'instruction
3. Exécuter l'instruction
4. Calculer l'adresse de l'instruction suivante : mettre à jour le PC

Utilisation des registres :
- `$0` contient la valeur 0 -> est le générateur du 0
- `$1` registre réservé à l'assembleur (programme qui génère le binaire)
- `$2 - $3` contiennent les résultats des appels de fonction
- `$2` peut aussi contenir le numéro d'appel système
- `$4 - $7` (est aussi appelé de `a0` à `a3`) utilisés pour le passage d'arguments lors des appels de fonctions ou appels systèmes
- `$8 - $15` contiennent les valeurs non persistantes (libre)
- `$16 - $25` contiennent les valeurs persistantes (libre)
- `$26 - $27` contiennent les valeurs OS
-> voir les diapos pour les autres

Les valeurs persistantes gardent les valeurs avant les appels

> [!NOTE] Notation des registres
> En Mips, on peut les notés avec `R`, `r` ou `$`, ainsi `R1 = r1 = $1`

> [!danger] On **doit** respecter les règles d'utilisation
## Jeu d'instruction
La vue externe d'un CPU peut être définie par l'ensemble des instructions qu'il est capable de traiter

Jeu d'instruction d'un CPU (aussi appelé ISA) est la donnée :
- de l'ensemble des instructions qu'il peut effectuer
- le codage de ces instructions en binaire

Une instruction, c'est une commande définissant le traitement à effectuer et quelle sera la prochaine instruction à exécuter
-> le traitement séquentiel est implicite

On peut spécifier quelle autre ligne à utiliser après l'instruction en cours
-> c'est un « saut »

Le code d'opération définit quelle opération utiliser
|> elle porte sur les opérandes
|> opérandes immédiates sont codées dans l'instruction
|> les autres opérandes sont dans des registres indiqués

`add $4, $2, $5` signifie `$4 <- $2 + $5`
`ori $4, $2, 0xABCF` signifie `$4 <- $2 | (0x0000 ABCF)`
`addi $4, $2, 0xABCF`signifie `$4 <- $2 + (0xFFFF ABCF)` car, par défaut, les entiers sont considérés comme relatifs
`mult $3, $4` signifie `(HI/LO) <- $3 × $4`
`div $3, $4` signifie `(HI/LO) <- $3 ÷ $4` (`HI` contient le quotient et `LO` le reste)

On peut définir un label pour savoir où sauter

4 classes d'instructions :
- arithmétique et logique -> addition, and...
- transfert mémoire -> lire la mémoire...
- rupture de séquence -> faire un saut...
- appels systèmes -> lire un caractère, écrire un entier sur l'écran

Voir le memento pour la liste des instructions

L'instruction `ori` permet de placer une certaine valeur dans un registre
|> `ori $2, $0, 0x1234` place `0x1234` dans `$2`

Les instructions en Mips possèdent 3 formats :
- R -> quand on utilise 3 registres
- I -> quand on fait des calculs avec des immédiats
- J -> quand on fait des sauts

| Nom\n° de bit | 31 - 26 | 25 - 21 | 20 - 16 | 15 - 11 | 10 - 6 | 5 - 0 |
| ------------- | ------- | ------- | ------- | ------- | ------ | ----- |
| **R**         | OPCODE  | RS      | RT      | RD      | SH     | FUNC  |
| **I**         | OPCODE  | RS      | RT      | IMM     | IMM    | IMM   |
| **J**         | OPCODE  | JUMP    | JUMP    | JUMP    | JUMP   | JUMP  |
OPCODE est spécifié dans un codage normé
|> détecte le format utilisé en fonction de l'OPCODE

Le codage normé ne contient pas tous les OPCODE
|> s'il n'est pas dedans, l'OPCODE est le "special" et l'opération est dans la case FUNC
|> l'opération dans FUNC est aussi dans un codage normé

On regarde le memento pour savoir ce que signifie RS, RT et RD
|> j'ai l'impression que le registre contenant le résultat est toujours le dernier affiché, mais c'est à vérifier

SH permet d'utiliser le shift

Langage haut niveau :
- $\forall$ ISA
- notions de type
- peut créer des variables
- structure les traitements
- gestions d'erreurs

Assembleur :
- Allocation des données et gestion mémoire
- Suite d'instructions spécifiques
- Présence d'étiquettes pour désigner les adresses (données ou instructions)

Un programme de haut niveau peut être :
- natif, i.e. il est compilé pour être exécuté sur la machine cible
- interprété, i.e. un programme natif interprète le programme et l'exécuté

Nous, on ne regarde que les programmes natifs

Assemblage = assembleur -> binaire
Désassemblage = binaire -> assembleur

Un label (ou étiquette) s'écrit comme : `nom: add $4, $4, $3`
|> ici le label `nom` désigne la ligne `add $4, $4, $3`
-> elles ne sont pas conservées par lors de l'assemblage

En Mips, toujours deux sections différentes :
1. les données du programme
2. la section de code

Directive `.data` permet de dire que la suite sera des données
Directive `.text` indique que la suite sera des instructions

On met toujours `.data`, y compris si c'est vide (dans le cadre de cette UE)

Pour exécuter un programme, on a besoin de le charger
|> le mettre en mémoire
|> mettre dans PC la première adresse à exécuter

En Mips, les syscall se font à l'aide de `syscall`
|> il cherche toujours le numéro de l'appel dans `$2`
-> se finit donc toujours par
```asm
ori $2, $0, 10 # place 10 dans $2
syscall # syscall dans $2, i.e. syscall 10, i.e. fin du programme
```

On utilise le simulateur Mars pour écrire / exécuter des programmes
## Lancer un programme
Pour exécuter un programme, on a besoin de le lancer
|> besoin de le charger en mémoire -> souvent, le *loader* s'en occupe
|> chargé en RAM depuis l'adresse de base contenant le début du programme
|> modifie PC pour contenir l'adresse du début (adresse en RAM)
## System call
Un appel système est une demande de service fourni par le système
|> est un garde fou
|> `syscall` appelle ces services
|> chaque service possède son numéro en Mips
|> on met le numéro du service dans `$2`
|> pour passer des argument à `syscall`, on doit les mettre dans les registres `$4` à `$7`

Pour afficher 125 :
```asm
ori $4, $0, 125
ori $2, $0, 1
syscall
```

`syscall` est un déroutement -> est gérer par l'OS
## Données en mémoire
Variables globales et partie `.data`

On peut voir la mémoire en mode "mot" (32 bits) ou en "octet" (8 bits)
|> représente la même mémoire
|> "mot" est 4 fois plus grand qu'un octet

Mips ne peut lire que 4 octets (ou un mot, ou 32 bits) à la fois
|> adresse mémoire d'un mot est l'adresse la plus petite des 4 octets
|> octet est l'unité adressable -> pas possible d'être plus petit que ça

On utilise toujours les notations en Kio

Mémoire non réinscriptible = ROM, lente d'accès, contient le code de démarrage ou l'ensemble du code dans certains systèmes embarqués
Mémoire vive = RAM, volatile et rapide
Mémoire flash (USB, SD Card) = non volatile, peut écrire (mais pas beaucoup)

RAM garde les données en mémoire et ne les supprime pas jusqu'à ce qu'on écrit dessus
|> lecture est non destructive (pas vraie partout !)

Écriture RAM -> *store*
Lecture RAM -> *load*

Type de stockage en RAM :
- Big Endian (grand boutien) -> l'octet de poids fort est rangé à l'adresse la plus petite
- Little Endian (petit boutien) -> l'octet de poids faible est rangé à l'adresse la plus petite
-> avec les mêmes bits en RAM, on n'obtient pas le même mot !

Mips est en Little Endian

Après avoir écrit `0xAABBCCDD` à `0x4`, on obtient

| Adresse | `0x0` | `0x1` | `0x2` | `0x3` | `0x4`  | `0x5`  | `0x6`  | `0x7`  | `0x8` |
| ------- | ----- | ----- | ----- | ----- | ------ | ------ | ------ | ------ | ----- |
| Contenu | ?     | ?     | ?     | ?     | `0xDD` | `0xCC` | `0xBB` | `0xAA` | ?     |
On peut écrire en mot, demi-mot ou octet

Mémoire est structuré pour éviter d'avoir des problèmes des sécurités
|> programme ne peut pas utiliser la RAM de l'OS par exemple
-> elle est d'abord coupé en deux
|> une partie utilisateur et une autre système
|> est séparée par une adresse (tous ceux qui sont inférieures sont dans l'utilisateur en Mips)

Mémoire utilisateur, c'est :
1. segment de code (le code du programme)
2. segment de données (données et variables globales)
3. segment de pile (variables locales et contextes d'appels) -> stack
4. (le tas dans la pile, mais on n'en a pas ici)

**Besoin de lire le diapo 22/48** (et un peu avant)

Le transfert RAM -> CPU en Mips doit être sur la même ligne !

Les contraintes d'alignement sont respectées par défaut quand utilise les données globales
|> quand utilise `.space`, on a besoin d'utiliser `.align` pour aligner correctement les zones libres

Quand on utilise la mémoire, on a besoin de copier les valeurs dans les registres et d'après la mettre à jour

Signature des instructions d'accès mémoire : `Codop Rt, Imm16(Rs)`
|> `Rt` est la destination ou le registre source
|> `Imm16(Rs)` est la zone mémoire avec `Rs` étant le décalage en octet

On se place tjs du côté du processeur pour les opcodes
|> `l.` servent à *load*
|> `s.` servent à *store*
|> `.w` gèrent les mots (4 octets)
|> `.h` gèrent les half-words (2 octets)
|> `.b` gèrent les octets (1 octet)

```asm
lh $4, 4($3) # load le half-word contenu dans l'adresse 4 + $3 dans $4

lw $4, -2(3) # load le word contenu dans l'adresse -2 + 3 dans $4
```

Par défaut, tout est signé, si on veut être en non signé, on rajoute `u`, i.e. `lhu` pour récupérer un `uint16`

Il n'existe pas de pointeur
|> besoin de mettre à jour la mémoire
|> faut faire attention aux problèmes de synchronisation

Il y a toujours au moins un transfert mémoire vers CPU
|> est celui qui place le code dans IR
## Structures de contrôle
Pour ne plus faire une instruction séquentielle, on doit faire des sauts
|> deux types : inconditionnels et conditionnels
-> changent PC

Sauts inconditionnels -> les jumps (commencent par `j`)
|> `j label` saute au label
|> `jr Rt` saute à valeur contenu dans Rt

Sauts conditionnels -> les branchements (commencent par `b`)
|> saute si une condition est vraie
|> voir le memento

Afficher la valeur absolue d'un nombre
```asm
.data
n:	.word -1

.text
	lui $3, 0x10010000
	lw $4, 0($3)
	
# syntaxe du *if then* ici
	bgez $4, show
	sub $4, $0, $4
show:
	ori $2, $0, 1
	syscall
	
	ori $2, $0, 10
	syscall
```

Pour utiliser plusieurs conditions, on a besoin d'utiliser une instruction avant
|> `slt $10, $8, $9` met `$10` à `1` si `$8 < $9`, il le met à `0` sinon
-> on utilise un branchement après
|> si on veut une comparaison large $a\leqslant b$, on vérifie si $a > b$ est faux

Pour faire un `if else`, on fait :
```asm
	bcond ..., $0, else
	# instruction du if
	j next
else:
	# instruction du else
next:
	# suite
```

Pour faire un `while`, on fait :
```asm
boucle:
	bcond ..., $0, next
	# instruction du while
	j boucle
next:
	# suite
```

Le codage de `j label` est
|> `00010 Immédiat_sur_26_bits`
-> besoin de calculer la bonne adresse pour qu'elle soit sur 26 bits
|> on fait `PC = PC[31:28] | I * 4`
|> `PC[31:28]` permet de rester dans la partie code
|> `I` est l'immédiat avec en plus `[1:0]` qui sont nuls car multiple de 4
|> `* 4` provient du multiple de 4

Branchement sont en format I
|> immédiat est donc sur 16 bits
-> on a tjs besoin de le mettre sur 32 bits
|> on fait `PC = PC + 4 + (I * 4)`
|> le calcul du PC suivant est donc relatif à la ligne d'instruction en cours

**Dans tous les cas, on a besoin de savoir où se trouve l'étiquette pour la coder**
|> on a donc besoin de le faire en deux étapes (on les appelle "passe")
1. on s'occupe de toutes les instructions sans traiter les étiquettes
2. on traite les étiquettes après qu'on connait bien tout

**Revoir la construction des sauts**
## Tableaux, structures
**revoir le début des tableaux**

Un tableau de taille $n$ prend $n$ fois la taille du type stocké
|> un tableau d'entier prend $4n$ octets

L'adresse du $i$-ième élément du tableau est l'adresse du début $+ i\times n$
|> c'est déréférencer `&T[i]`

On déclare les tableaux comme
```asm
.text
tab:  .word 1, 2, 3 # équivalent de int tab[] = {1,2,3};
```

Attention à l'alignement quand on utilise les tableaux !

Les structures sont des ensembles de variables
|> on utilise les `.space` pour en initialiser des vides

**besoin de vérifier si c'est complet**
## Variables locales
Elles ne sont pas au même endroit que les variables globales
|> elles sont dans une zone variable qui contient les infos dynamiques
-> la pile !
|> création d'un contexte pour chaque fonction
|> contexte sont en LIFO, d'où la pile

**rattraper la gestion mémoire**
`0x8000 0000` -> égal ou plus grand pour le système
|> ce qui précède `0x8000 0000` est pour l'utilisateur
|> le fond de la partie utilisateur sont les contextes de fonction (la pile !)
-> le sommet de la pile est variable et est dans un registre particulier, le `$29` (stack pointer)

Adresse du fond de pile = `0x7FFF FFFC`

On définit la taille de la pile dans le prologue
|> se fait en première instruction
|> on modifie directement `$29` pour lui indiquer quelle taille on met dedans
|> la première variable est tjs celle qui est le plus haut (i.e. avec l'adresse la plus petite), est la convention gcc

Après, on diminue la taille de la pile dans l'épilogue
|> on dépile toutes les variables qu'on a utilisé

```c
int main(){
	int a = 1;
	int b = 2:
	int c;
	c = a + b;
	return 0;
}
```

```asm
.text
# prologue
addiu $29, $29, -12 # car on stock 3 mots
# initialisation des variables
ori $8, $0, 1
sw $8, 0($29)
ori $8, $0, 2
sw $8, 4($29)
# corps du main
lw $8, 0($29)
lw $9, 4($29)
addu $8, $8, $9
sw $9, 8($29)
# épilogue
addiu $29, $29, 12
# exit
ori $2, $0, 10
syscall
```
## Fonction
Pour arriver à une fonction, on utilise `jal label`
|> garde le lien de l'appel dans `$31`
|> saute à label

> [!warning] Besoin de sauvegarder `$31`
> Si `main` appelle `f` et `f` appelle `h`, alors on perd le `$31` pour `f` !
> |> fonction doit s'assurer que `$31` est identique entre l'entrée et la sortie
> 
> **On le sauvegarde tjs au fond du contexte**

Valeur de retour est dans `$2`
|> ou dans `$2` et `$3` si le mot fait 64 bits

Les arguments sont mis en haut de la pile
|> les 4 premiers sont dans les registres `$4`, `$5`, `$6`, `$7`
|> il y a quand même assez de place dans la pile pour garder les 4 premiers

Les registres `$16` à `$23` ne doivent pas être modifié par un appel de fonction
|> ou s'ils sont modifiés, leurs valeurs doit être restaurées

**Voir le mémento pour les conventions de stockage**

> [!warning] On garde toujours de la place dans la pile, y compris si on optimise en registre