Consignes
Pour ce TP, vous devrez rendre une archive zip contenant les fichiers assembleur question_??.s (pour les questions pertinentes) ainsi qu'un fichier texte README.
Ce fichier README n'est pas un compte-rendu "classique". Vous devez produire un document synthétique du style "fiche de révision" contenant un résumé des notions et concepts que vous aurez explorés pendant le TP.
L'objectif est que vous produisiez une synthèse contenant les choses qui peuvent vous servir, par exemple pour l'examen !
Il n'est pas nécessaire d'apprendre toutes les instructions utilisées par cœur, mais les techniques utilisées pour les 2 premiers tiers du TP sont au programme de l'examen.
Tous vos fichiers contiendront obligatoirement une entête avec :
-
votre nom, votre filière et votre groupe,
-
le numéro de votre poste (si vous faites le TP sur un des ordinateurs de l'université).
N'envoyez pas les fichiers exécutables ou les fichiers *.o produits par la compilation. Vous pouvez facilement les supprimer avec la commande
$ make clean
IMPORTANT: les bases de la programmation en assembleur x86 sont au programme de l'examen. Il n'est pas nécessaire d'apprendre les noms des instructions, mais vous devez savoir faire l'équivalent des questions 3, 4, 6, 7 et 9. Les concepts associés à la pile et aux appels de fonctions sont également au programme.
Liens utiles
-
email des enseignants (Chambéry) damien.decout@univ-smb.fr, david.telisson@univ-smb.fr, gerald.cavallini@univ-smb.fr, jean-yves.ramel@univ-smb.fr, pierre.hyvernat@univ-smb.fr
-
email des enseignants (Annecy) Ammar.Mian@univ-smb.fr, andrew.mary-huet-de-barochez@univ-smb.fr, guillaume.ginolhac@univ-smb.fr, sebastien.monnet@univ-smb.fr
-
le fichier Makefile utilisé pour compiler et lancer le débogueur
Objectifs du TP
L'objectif de ce TP est de se familiariser avec les concepts de base du langage d'assemblage Intel x86.
1. Préliminaires
Si vous travaillez sur votre ordinateur, vous pourrez avoir des besoins de programmes qui ne sont pas toujours installés par défaut. Vous pouvez les installez avec
$ sudo apt install binutils gdb strace
Un fichier complet en assembleur ressemble à
.intel_syntax noprefix # <--- pour activer la syntaxe intel, plus simple que la syntaxe par défaut (AT&T)
.global _start # <--- déclaration du début du code à exécuter
##############
.section .text # <--- le segment "text" contient les instructions
# d'autres segments sont data, rodata et bss
# ...
_start: # <--- point d'entrée du programme ("fonction principale")
# instructions
# ...
# ...
# fin du programme # <--- fin du programme
fin: # NE PAS MODIFIER LES LIGNES SUIVANTES
mov rax, 60
mov rdi, 0
syscall
L'exécution d'un programme écrit en assembleur est plus complexe que celle d'un programme Python.
-
Il faut transformer le langage d'assemblage en langage machine, par exemple avec la commande as (comme assemble) :
$ as -g question_00.s -o question_00.o(N'oubliez pas de remplacer question_00.s par le nom du fichier que vous souhaiter utiliser !)
-
La commande précédente produit question_00.o qui est un fichier binaire contenant des instructions machines. Le fichier n'est pas "complet" car il peut encore faire référence à des commande présentes dans d'autres fichiers / bibliothèques partagées. Pour le transformer en fichier exécutable complet, on utilise la commande ld :
$ ld -g question_00.o -o question_00 -
On peut ensuite lancer le fichier exécutable avec
$ ./question_00Comme faire des affichages (
printen Python) en assembleur est assez complexe, nous ne pourrons en général pas observer ce qui se passe.Pour expérimenter, nous allons exécuter le programme dans un débogueur pour pouvoir exécuter les instructions une par une et observer l'état des registres à chaque étape. Pour éviter d'avoir à lancer ces commandes à chaque fois, vous pouvez télécharger le fichier Makefile et le mettre dans le même répertoire que votre fichier question_00.s. Vous pourrez ensuite utiliser
$ make question_00.gdbpour compiler votre fichier question_00.s et le lancer directement dans le débogeur.
Je ne garantie pas que ce TP fonctionne sur un Mac, qui utilise une architecture différente. Si vous voulez tester, il faudra probablement
-
vérifier que vous avez les programmes gdb et clang installés sur votre système,
-
décommenter les lignes AS = clang -g -c et LD = clang -g -nostartfiles -nostdlib dans le fichier Makefile,
-
remplacer
_startparmaindans tous vos programmes, -
remplacer
mov rax, 60à la fin de tous vos programmes parmov rax, 0x2000001, -
et probablement d'autres choses...
Téléchargez le fichier question_00.s et le fichier Makefile.
-
lancez la commande
$ make question_00.gdbVous devriez arriver sur un écran ressemblant à la capture suivante, peut-être avec des couleurs différentes.
-
Le programme a été lancé, mais l'exécution est stoppée juste avant la première instruction
mov rax, 3735805103. La partie haute contient les valeurs des registres, en hexadécimal et décimal ; la partie centrale contient le code assembleur du programme, avec la prochaine instruction surlignée ; et la partie basse sert a donner des commandes au débogeur.Pour avancer dans l'exécutions, vous pouvez utilisez les commandes suivantes dans le débogueur:
-
step, ou simplement s pour exécuter l'instruction suivante ;
-
continue, ou simplement c pour continuer l'exécution jusqu'à la fin ;
-
quit, ou simplement q pour quitter le débogueur.
D'autres commandes sont disponibles. Toutes les commandes utilisées dans le TP sont résumées à la fin du sujet.
Exécutez les 2 instructions suivantes (
movetadd) et cherchez la valeur du registrerax. -
-
Finissez l'exécution du programme avec la commande continue, et quittez le débogueur.
Que se passe t'il si vous supprimez les 3 dernières instructions
(mov, mov, syscall, après la ligne
fin:) du programme ?
Cherchez et expliquez le rôle de ces lignes.
2. Premiers programmes : registres et calculs simples
L'assembleur x86 a plusieurs registres généraux que l'on peut utiliser pour
les calculs: rax, rbx, rcx,
rdx, rsi et rdi. Ces registres font
chacun 8 octets, c'est à dire 64 bits.
Il est possible de faire une "affectation" de registre avec l'instruction
mov:
-
mov REG1, REG2correspond à ce qu'on écriraitREG1 = REG2en Python.
REG1 est un nom de registre, et REG2 est soit un
nom de registre, soit un nombre entier.
Les opérations arithmétiques usuelles sont
-
add REG1, REG2qui permet de faire une addition et affectation, que l'on écriraitREG1 = REG1 + REG2en Python ; -
sub REG1, REG2qui permet de faire une soustraction et affectation, que l'on écriraitREG1 = REG1 - REG2en Python.
Comme pour l'instruction mov, REG1 est un registre
(rax, rbx, etc.) et REG2 est soit un
registre, soit un nombre.
La multiplication est similaire, mais avec une difficulté supplémentaire : elle modifie 2 registres !
-
imul REG1, REG2fait une multiplication et affectation, que l'on écriraitREG1 = REG1 * REG2. Attention, cette instruction modifie également le registrerdx: ce registre contiendra les 64 bits de poids fort du résultat. (C'est utile car le produit de 2 nombres de 64 bits peut faire 128 bits.)
En partant du fichier question_03.s, écrivez quelques instructions assembleurs pour :
-
faire l'équivalent de
rcx = rax + rbx + 12345(vous ne devez pas modifierraxourbx) ; -
puis faire l'équivalent de
rax = rcx * (rbx + 12345*rax)(vous ne devez pas modifierrbxourcx) ; -
puis faire l'équivalent de
rbx = rcx * (rbx + 12345*rax)(vous ne devez pas modifierraxourcx).
Si vous avez besoin de registre auxiliaires, vous pouvez utiliser les
registres r8, r9, ..., r15. (Remarque :
ces registres n'existent pas en mode 32 bits.)
Pour vérifier, vous pouvez utiliser les valeurs suivantes : Les résultats devraient être :
-
pour
rcx = rax + rbx + 12345, avec-
mov rax, 54321 -
mov rbx, 6789
rcxdoit prendre la valeur73455, -
-
pour
rax = rcx * (rbx + 12345*rax), avec-
mov rax, 54321 -
mov rbx, 6789 -
mov rcx, 113355
raxdoit prendre la valeur76015810176570, -
-
rbx = rcx * (rbx + 12345*rax), avec-
mov rax, 54321 -
mov rbx, 6789 -
mov rcx, 113355
rbxdoit prendre la valeur76015810176570. -
3. Contrôle du flot : boucle et conditionnelles
Les langage d'assemblage n'ont pas de boucles for ou
while. Le programmeur doit gérer le flot d'exécution à la
main. Par défaut, le processeur exécute les instructions dans l'ordre. Pour
modifier ceci, il faut utiliser des instructions de saut.
-
jmp LABELpour aller directement à l'instruction annotée parLABEL
Pour faire en saut en fonction d'une comparaison, on utilise :
On peut aussi utiliser
-
une comparaison
cmp REG1, REG2, oùREG1etREG2sont des registres ou des valeurs, -
suivie d'un saut
-
je LABELpour aller à l'instruction annotée parLABELsi la comparaison précédente a renvoyé "égal" (ecommeequal), -
jne LABEL... si la comparaison précédente à renvoyé "différent" (necommenotequal), -
jl LABEL... si la comparaison précédente à renvoyé "plus petit" (lcommeless), -
jle LABEL... si la comparaison précédente à renvoyé "plus petit ou égal" (lecommeless orequal). -
jg LABEL... si la comparaison précédente à renvoyé "plus grand" (gcommegreater), -
jge LABEL... si la comparaison précédente à renvoyé "plus grand ou égal" (gecommegreater orequal).
Attention, il ne faut pas mettre d'autres instructions entre le
cmpet le saut ! -
LABEL désigne une annotation que l'on peut mettre devant
n'importe quelle instruction. Par exemple, _start: annote la
première instruction du fichier question_00.s. Voici un exemple de saut :
.intel_syntax noprefix
.global _start
##############
.section .text
_start:
# ...
# ...
sub rax, 1
cmp rax, 0
je fin # si rax devient 0, on quitte le programme
# ...
# ...
# fin du programme
fin: # NE PAS MODIFIER LES LIGNES SUIVANTES
mov rax, 60
mov rdi
syscall
Après une instruction add ou sub, on peut faire un
saut sans utiliser cmp :
-
jz LABELpour aller à l'instruction annotée parLABELsi l'instruction précédente a renvoyé 0 (zcommezero), -
jnz LABELpour aller à l'instruction annotée parLABELsi l'instruction précédente n'a pas renvoyé 0 (nzcommenotzero).
Pour connaitre le nombre de chiffres utilisés pour écrire un entier, il suffit de chercher la première puissance de 10 strictement plus grande au nombre en question. En Python :
def nb_chiffre(n: int) -> int:
r = 0
p = 1
while p <= n:
p = 10 * p
r += 1
return r
-
En partant du fichier squelette.s, écrivez l'équivalent en assembleur x86. Vous pouvez supposez que
n>0est contenu dans le registrerdiet vous devez calculez la valeur derdans le registrerax. -
La fonction Python
nb_chiffrerenvoie0pourn=0. Est-ce le cas pour votre code en assembleur ? Si non, corrigez votre code pour que la valeur finale deraxsoit bien égale à0sirdivaut initialement0. -
Donnez en commentaire la liste des registres auxiliaires que vous modifiez.
Une manière équivalente est de compter le nombre de division par 10 nécessaires avant de tomber sur 0. En Python :
def nb_chiffre(n: int) -> int:
r = 0
while n > 0:
n = n // 10
r += 1
return r
-
Écrivez l'équivalent en assembleur x86. En supposant que
n>0est contenu dans le registrerdi, calculez la valeur derdans le registrerax.Vous aurez besoin pour ceci de faire une division. Cette opération est plus complexe que les précédentes :
-
idiv REG, avec un unique argument, qui est obligatoirement un registre-
le registre
REGest le diviseur -
le nombre à diviser est toujours dans les 2 registres
raxetrdx:raxcontient les bits de poids faible etrdxles bits de poids fort. Lorsque le nombre est positif et inférieur à 263, il suffit de mettrerdxà 0. (On peut aussi utiliser l'instructioncqo, qui fonctionne pour les entiers négatifs.) -
le quotient sera mis dans le registre
rax -
le reste sera mis dans le registre
rdx.
-
-
-
La fonction Python
nb_chiffrerenvoie0pourn=0. Est-ce le cas pour votre code en assembleur ? Si non, corrigez votre code pour que la valeur finale deraxsoit bien égale à0sirdivaut initialement0. -
Donnez en commentaire la liste des registres auxiliaires que vous modifiez.
Écrivez l'équivalent du code suivant en assembleur x86.
def signe(x: int) -> int:
if x < 0:
s = -1
elif x > 0:
s = +1
else:
s = 0
return s
Vous supposerez que x est contenu dans le registre
rdi et calculerez la valeur de s dans le registre
rax.
4. La pile
Même avec les 8 registres supplémentaires r8, ...,
r15 disponibles en mode 64 bits, le nombre de registre devient
vite une limite dans les programme écrits en assembleur x86.
La pile est une zone mémoire allouée pour chaque programme. On peut y sauvegarder la valeur d'un registre et la récuperer plus tard.
-
push REGempile la valeur du registreREGdans la pile, -
pop REGdépile la dernière valeur et la copie dans le registreREG.
Votre code pour calculer le nombre de chiffres d'un nombre utilise au moins 3 registres :
-
rdiqui contient le nombre à considérer, -
raxqui contient le résultat, -
au moins un registre auxiliaire pour les calculs intermédiaires.
Faites une copie de votre fichier question_04.s en question_07.s et ajoutez les quelques instructions
pour sauvegarder et restaurer la valeur de ce(s) registre(s) auxiliaire(s)
autour du bloc de code correspondant à nb_chiffres.
Vérifiez que votre code fonctionne toujours et restaure bien les bonnes valeurs dans tous les registres.
Dans le débogueur, vous pouvez afficher les 5 dernières valeurs de la pile avec la commande "ss 5". Cela affichera l'adresse de chaque valeur, la valeur elle même en héxadécimal et en décimal.
-
Dans un programme assembleur, faites plusieurs push avec des valeurs connues. Est-ce que l'empilement des valeurs se fait en augmentant ou en diminuant les adresses mémoire ?
-
Est-ce que le nombre d'octets utilisés pour une valeur empilée dépend de la valeur ?
-
Un des registre spéciaux est utilisé pour stocker l'adresse de la dernière valeur de la pile. Quel est se registre ?
5. Fonctions
Pour appeler une "fonction" en assembleur, il suffit de faire un saut vers le
code correspondant avec jmp. Par exemple :
# ...
# calcul du nombre de chiffres en base 10 pour rdi
# le résultat est stocké dans rax
nb_chiffres:
# ...
# ...
# code principal
_start:
# ...
mov rdi, 12345
jmp nb_chiffres # pour faire les calculs avec la valeur 12345
# ...
Il y a 2 problèmes.
-
il faut penser à restaurer les valeurs des registres auxiliaires utilisés par les instructions intermédiaires.
-
Il faut, à la fin de la fonction, faire un saut vers l'instruction qui suivait l'appel à la fonction, càd l'instruction qui vient juste après
jmp ma_fonction.
Pour le premier problème, il suffit d'utiliser push et
pop (cf question précédente). Pour le second problème, on utilise
2 instructions de saut spéciales :
-
call LABELempile l'adresse de la prochaine instruction avant de faire un saut vers la destinationLABEL -
retdépile la dernière valeur et fait un saut vers l'adresse correspondante.
En partant de votre fichier contenant le code pour nb_chiffres,
ajoutez / modifiez les lignes pertinentes pour faire des appels de fonction
vers nb_chiffres pour les valeurs
-
12345(vous devez obtenirrax = 5) -
0x4321(vous devez obtenirrax = 5) -
100000(vous devez obtenirrax = 6) -
0(vous devez obtenirrax = 0).intel_syntax noprefix .global _start ############## .section .text nb_chiffres: # ... _start: mov rbx, 11111 # pour vérifier qu'on restaure bien mov rcx, 22222 # les valeurs des registres mov rdx, 33333 # ... mov rdi, 12345 # appel à nb_chiffres # vérification dans gdb que rax = 5 mov rdi, 0x4321 # appel à nb_chiffres # vérification dans gdb que rax = 5 mov rdi, 100000 # appel à nb_chiffres # vérification dans gdb que rax = 6 mov rdi, 0 # appel à nb_chiffres # vérification dans gdb que rax = 0 # vérification que les registres rbx, etc. ont bien # retrouvé leur valeur initiale # fin du programme mov rax, 60 mov rdi, 0 syscall
-
En exécutant un programme dans le débogueur, cherchez le registre spécial contenant l'adresse de l'instruction en cours. Comment s'appelle t'il ?
-
Vérifiez que l'adresse de l'instruction suivante est bien ajouté dans la pile par l'instruction
call LABEL. Décrivez ce que vous faites pour cette vérification. -
Que se passe t'il si vous empilez une valeur dans la pile avant d'utiliser l'instruction
ret? -
En regardant l'évolution du registre contenant l'adresse de l'instruction en cours, calculez la taille (en octets) des instruction x86
-
nop, -
add rax, rbx, -
add rax,1, -
add rax,123456789?
Décrivez comment vous obtenez vos résultats.
-
6. Pile et adresses : registre rsp
L'assembleur x86 utilise une douzaine de registre "généraux":
rax, rbx, rcx, rdx,
rdi, rsi, r8, r9,
r10, r11, r12, r13,
r14 et r15. (Les registres r8 jusqu'à
r15 n'existent pas en mode 32 bits.) Il n'est donc en général pas
possible d'utiliser un registre pour chaque variable d'un programme.
La solution est de stocker les variables dans la pile: un morceau de mémoire RAM où l'on peut stocker des valeurs.
-
push REGajoute la valeur du registreREGdans la pile, -
pop REGenlève la dernière valeur de la pile et la met dans le registreREG.
Ces 2 instructions modifient le registre spécial rsp
(sp comme stack pointer) :
-
pushva décrémenterrspde 8, car un registre 64 bits contient 8 octets, -
popva incrémenterrspde 8, car un registre 64 bits contient 8 octets.
Visuellement, voici ce qui se passe pour l'instruction push rax:
Comme rsp donne toujours la dernière adresse de la pile, on peut
récupérer
-
la dernière valeur empilée par un
pushen regardant à l'adressersp, -
l'avant dernière valeur empilée en regardant à l'adresse
rsp + 8, -
etc.
Les adresses sont notées entre crochets "[...]":
.intel_syntax noprefix
.global _start
##############
.section .text
_start:
push 123 # empile 123
push 456 # empile 456
# ...
# ...
mov rax, [rsp] # on récupère 123
mov rbx, [rsp + 8] # on récupère 456
imul rax, rbx
# fin du programme
mov rax, 60
mov rdi, 0
syscall
L'accès général à une adresse se fait avec
[REG1 + N*REG2 + OFFSET]
où
-
REG1est un registre qui contient une adresse -
N(facultatif) est un nombre d'octet égal à 1, 2, 4 ou 8, -
REG2(facultatif) est un registre, -
OFFSET(facultatif) est un nombre entier.
Il est parfois nécessaire de spécifier combien d'octet on souhaite lire à cet adresse :
-
byte ptr [REG1 + N*REG2 + OFFSET]pour un seul octet, -
word ptr [REG1 + N*REG2 + OFFSET]pour 2 octets, -
dword ptr [REG1 + N*REG2 + OFFSET]pour 4 octets, -
qword ptr [REG1 + N*REG2 + OFFSET]pour 8 octets.
Écrivez un programme en assembleur qui :
-
crée 100 valeurs sur la pile :
x0 = 0,x1 = 1, ...,x99 = 99 -
fait la somme de ces 100 valeurs dans le registre
rax.
À la fin, la valeur de rax doit être 4950.
Pour éviter d'avoir à avancer de plusieurs centaines d'étapes avant d'arriver au résultat, vous pouvez ajouter un "point d'arrêt" dans le débogeur:
-
breakpoint POS, ou simplement b POS pour que l'exécution s'arrête à la position POS (en général, un label du programme, ou un numéro de ligne),
-
continue, ou simplement c pour continuer l'exécution jusqu'au prochain point d'arrêt.
7. Pour aller plus loin
Appels système
L'affichage d'une chaine de caractère nécessite l'accès à un périphérique.
C'est le système d'exploitation qui doit s'en charger. En assembleur, il faut
explicitement faire une demande au système avec l'instruction
syscall.
Chaque fonctionnalité offerte par le noyau (on parle d'appel système)
a son propre numéro. Pour l'invoquer, on utilise l'instruction
syscall en mettant le numéro correspondant dans le registre
rax. Par exemple, pour le noyau Linux :
| appel système | numéro | effet |
|---|---|---|
read
|
0
|
lit rdx octets depuis le fichier ou assimilé rdi vers l'adresse rsi
|
write
|
1
|
écrit rdx octets depuis l'adresse rsi vers le fichier ou assimilé rdi
|
open
|
2
|
ouvre un fichier ou assimilé ... |
close
|
3
|
ferme un fichier ou assimilé ... |
| ... | etc. | ... |
Pour afficher une chaine de caractères on utilise la sortie standard,
qui est assimilée à un fichier et qui a automatiquement le numéro
1. Il faut donc
-
mettre l'entier
1dans le registrerax(pour spécifier write), -
mettre l'entier
1dansrdi(pour spécifier stdout), -
mettre des octets (ASCII) dans la mémoire et stocker l'adresse du premier octet dans
rsi, -
mettre le nombre d'octets que l'on souhaite afficher dans
rdx, -
utiliser l'instruction
syscall.
L'étape 3 est la plus complexe. On peut
-
ecrire la chaine explicitement dans une partie réservée de la mémoire : le segment
data:.intel_syntax noprefix .global _start ############## .section .data chaine: .ascii "une chaine de caractères" ############## .section .text _start: # instructions # ... -
recopier l'adresse de la chaine du segment
datadans un registre avec l'instructionlea(comme load effective address)# ... lea rax, [chaine] # on met l'adresse de 'chaine' dans rax # ... -
et faire l'appel système comme décrit plus haut.
-
Écrivez votre propre version du célèbre "hello world" en assembleur x86 64 bits. N'oubliez pas de terminer votre affichage par un saut de ligne.
-
Pour le tester, utiliser à la fois make question_12-hello.gdb, puis faite une exécution "normale" avec
$ ./question_12-hellopour voir l'affichage directement dans le terminal.
La commande strace permet d'afficher (sur stderr) tous les appels système utilisé par un processus. L'option -n permet d'afficher leur numéro :
$ strace -n COMMANDE
...
... exécution de COMMANDE, en affichant les appels systèmes
... sur stderr lorqu'ils sont exécutés
...
-
Quels sont les appels systèmes utilisés par le programme question_12-hello ?
-
Écrivez une version du programme "Hello world" en Python. Quels sont les appels système utilisés par ce programme ? Que constatez-vous ?
Compilez et exécutez le code contenu dans le fichier question_14-mystere.s.
-
Que se passe t'il ?
-
Expliquez le comportement du programme en détaillant ce que font les instructions importantes.
Un "gros" programme
Pour finir, nous allons écrire un "vrai" programme en assembleur x86. Ce programme agira comme un filtre qui lit des données sur stdin, passe tous les caractères ASCII alphabétiques en majuscule, et écrit le résultat sur stdout :
$ ./question_15 < question_00.s
.INTEL_SYNTAX NOPREFIX
.GLOBAL _START
##############
.SECTION .TEXT
_START:
# INSTRUCTIONS
MOV RAX, 3735805103
ADD RAX, 123456
NOP
# FIN DU PROGRAMME
MOV RAX, 60
MOV RDI, 0
SYSCALL
Nous avons presque tous les outils pour écrire ce programme.
-
L'appel système read permet de lire des octets dans un fichier ou assimilé :
-
son numéro est
0, il faut donc mettre0dans le registreraxavant d'appeler l'instructionsyscall; -
l'entrée standard est automatiquement ouverte, et son numéro est
0, il faut donc mettre0dans le registrerdiavant d'appeler l'instructionsyscall; -
comme pour write, il faut donner une adresse dans
rsi. C'est à cette adresse que les octets lus seronts copiés. -
comme pour write, il faut donner un nombre d'octets dans
rdx. Attention, la zone mémoire correspondant àrsidoit pouvoir contenir au moins ce nombre d'octets !
-
-
Un registre contient 8 octets. Pour travailler sur les caractères d'une chaine, il faut utiliser un seul octet. Il est possible de n'utiliser qu'un seul octet des registres traditionnels
rax,rbx,rcxetrdx. Pour ceci, on utilisealau lieu derax,blau lieu derbx,clau lieu dercxetdlau lieu derdx. Cela permet en particulier de ne recopier qu'un seul octet lorsqu'on écritmov al, [REG] # on récupère l'octet à l'adresse REG -
Les lettres ASCII minuscules vont de
97(correspondant à la lettre a) à122(correspondant à la lettre z). La lettre A a le numéro65. Pour passer une lettre minuscule en majuscule, il suffit donc de lui soustraire32(32 = 97-65).Vous pouvez écrire un caractère ASCII entre guillemets simples "'" pour dénoter son nombre en ASCII:
cmp REG, 97est équivalent àcmp REG, 'a'.
À la fin de l'appel système, le nombre d'octets lus et copiés à l'adresse
rsi est stocké dans rax. Ce nombre peut être
inférieur à rdx si le fichier (ou dans notre cas, l'entrée
standard) contenait moins d'octets.
Écrivez un filtre dans un fichier question_15.s qui lit toute l'entrées standard et la réécrit sur la sortie standard en passant les lettres ASCII en majuscules.
Attention, il s'agit d'un programme conséquent. (Ma version fait 50 lignes.) Ajoutez dans votre fichier des commentaires pour expliquer votre démarche et vos essais. Ceci est particulièrement important si vous utilisez l'IA pour vous aidez à écrire votre code.
L'architecture usuelle d'un tel programme ressemble en général à :
-
on initialise un tableau ("buffer") de 4096 octets (par exemple),
-
on rempli ce buffer avec read
-
si read renvoie "
0octet lu", on termine le programme -
sinon, on travaille avec les octets du buffer avant de recommencer à l'étape 2.
-
Résumé des instructions x86
| instruction | sémantique | arguments | notes |
|---|---|---|---|
nop
|
rien (nop: "no operation")
|
||
mov REG1, REG2
|
REG1 = REG2
|
REG1: registre ou adresse, REG2: registre, adresse ou nombre
|
|
add REG1, REG2
|
REG1 = REG1 + REG2
|
REG1: registre ou adresse, REG2: registre, adresse ou nombre
|
|
sub REG1, REG2
|
REG1 = REG1 + REG2
|
REG1: registre ou adresse, REG2: registre, adresse ou nombre
|
|
imul REG1, REG2
|
REG1 = REG1 * REG2
|
REG1: registre ou adresse, REG2: registre, adresse ou nombre
|
rdx est modifié par l'instruction
|
idiv REG
|
rax = rax // REG rdx = rax % REG
|
REG: registre ou adresse
|
rdx doit être mis à 0 avant l'instruction
|
jmp LABEL
|
saut à l'instruction après LABEL
|
LABEL est un label
|
|
jz LABEL
|
saut si l'instruction précédente (add ou sub) a donné 0
|
LABEL est un label
|
|
jnz LABEL
|
saut si l'instruction précédente (add ou sub) n'a pas donné 0
|
LABEL est un label
|
|
cmp REG1, REG2
|
compare les 2 valeurs entières |
REG1 et REG2: registres, adresses ou nombres
|
utilisé avec les sauts conditionnels |
je, jne,jl, jle, jg, jge
|
saut conditionnel après une instruction cmp
|
LABEL est un label
|
e pour equal, ne pour not equal,l pour less, le pour less or equal,g pour greater, etc.
|
push REG
|
ajoute la valeur d'un registre dans la pile | ||
pop REG
|
enlève la dernière valeur de la pile et la stocke dans un registre | ||
call LABEL
|
appel de fonction |
LABEL est un label
|
doit être associé à des instructions return pour retourner après l'instruction call ...
|
ret
|
retourne à l'instruction suivant le dernier call ...
|
Résumé des commandes GDB
| commande gdb | abbreviation | sémantique | notes |
|---|---|---|---|
| step | s | passe à l'instruction suivante | |
| quit | q | quitte gdb | |
| continue | c | continue l'exécution jusqu'à la fin ou le prochain point d'arrêt | |
| break POS | b POS | ajoute un point d'arrêt | POS peut être un label ou un numéro de ligne |
| next | n | passe à l'instruction suivante |
ne rentre pas dans les fonctions (instruction call ...
|
| showstack N | ss N | affiche les N dernières valeurs de la pile | fonction ad-hoc, n'existe pas dans gdb par défaut |

