Partie 25 - Annexes
Annexes
Dans cette page, vous trouverez plusieurs informations regroupées ensemble dont on a pu parler lors de ce cours :
- des astuces Ida
- des astuces gdb
- les principales instructions x86
Si vous cherchez une info ou commande bien précise, n’hésitez pas à utiliser
Ctrl+F😉.
Astuces IDA
Astuce IDA : Vous pouvez utiliser le raccourcis
Npour renommer une fonction, un label ou une variable en ayant préalablement cliqué dessus avant de la renommer.
Astuce IDA : Pour modifier le type d’une fonction ou d’une variable, il suffit de cliquer dessus et d’appuyer sur
Y.
Astuce IDA : Le raccourcis permettant d’assigner à des constantes des énumérations est
M.
Astuce IDA : Il est possible de mettre un commentaire sur la même ligne que l’instruction sélectionnée dans la fenêtre de décompilation avec le raccourcis
/.Dans la fenêtre du code désassemblé, cela est possible avec
:ou;.
Astuce IDA : Vous pouvez utiliser le raccourcis
Inserpour saisir un commentaire avant l’instruction sélectionnée.
Astuce IDA : En utilisant la touche
Entrée, vous pouvez ajouter des sauts de lignes, pratique lorsque l’on souhaite espacer le code.
Astuce IDA : Les variables nommées
v1,v2etc. correspondent à des variables locales d’une fonction tandis que les variablesa1,a2etc. correspondent aux arguments de la fonction.
Astuce IDA : Vous pouvez utiliser le raccourcis
Gpour aller à une adresse en particulier.
Astuce IDA : Vous pouvez utiliser le raccourcis
espacepour basculer du mode “graphe” vers le mode “texte” et inversement.
Astuce IDA : En mode “graphe”, vous pouvez modifier la couleur des blocs de base en cliquant sur l’icône la plus à gauche en haut du bloc.
Astuce IDA : Parfois, au lieu d’afficher une chaîne de caractères, IDA affiche un offset en mémoire plutôt que la
stringdirectement. Pour y remédier, aller dansEdit➡️Plugins➡️Hex-Rays Decompiler➡️Options➡️Analysis options 1et décocherPrint only constant string literals.
Astuce IDA : Il est souvent intéressant d’avoir les deux onglets désassembleur / décompilateur sur la même vue. Vous pouvez faire cela en déplaçant l’un des deux onglets. Vous pouvez ensuite synchroniser les deux vues en faisant un clic droit dans la fenêtre de décompilation et en cliquant sur
Synchronize with > IDA View.De cette manière, lorsque vous cliquerez sur un ligne ou que vous changerez de fonction, IDA affichera la ligne adéquate dans la fenêtre de désassemblage.
Astuce IDA : Pour désactiver (ou réactiver) le cast des variables, c’est le raccourcis
Alt Gr + \. Cela permet d’avoir du code plus lisible.Mais attention, parfois les casts donnent des informations importantes, notamment lorsque l’on souhaite reprogrammer un algorithme en C, Python ou autre, il est nécessaire de faire attention à la taille des variables.
Astuce IDA : Une fois que vous avez trouvé l’adresse de base de votre programme, il suffit, dans IDA, d’aller dans
Edit➡️Segments➡️Rebase programpuis saisir l’adresse de base trouvée dans gdb aveclibset cliquer surOk.
Astuces gdb
Certaines de ces commandes sont propres à
pwndbg.
Liste des formats :
- o : octal
- x : hexadécimal
- u : décimal non signé
- t : binaire
- f : nombre à virgule (ou flottant)
- a : adresse
- c : char
- s : chaîne de caractères
Tailles définies dans gdb :
| Abréviation | Signification | Taille (en octets) |
|---|---|---|
b | byte | 1 |
h | half word | 2 |
w | word | 4 |
g | giant word | 8 |
Astuce gdb : Si un programme accepte des arguments via
argv, il est possible de les spécifier lors de la commanderun.Exemple :
run arg1 arg2
Astuce gdb : La commande
hb *0xaddr(hardware breakpoint) permet d’insérer un point d’arrêt matériel à l’adresse0xaddr.
Astuce gdb : Vous pouvez utiliser
i b(pourinfo breakpoints) afin de lister les points d’arrêts du programme.Cela est très utile pour s’y retrouver. Chaque point d’arrêt ayant un numéro unique, il sera affiché dans cette commande.
Astuce gdb : Pour supprimer un point d’arrêt vous pouvez utiliser
d N(pourdelete N) afin de supprimer le breakpoint numéroN.
Astuce gdb : Vous pouvez lister les zones mémoire mappées avec la commande
libs.
Astuce gdb : L’instruction
startipermet de charger le programme en mémoire et de s’arrêter à la première instruction de ce dernier, sans l’exécuter.
Astuce gdb : Vous pouvez quitter gdb avec les commandes
quitouexit. De manière plus rapide, vous pouvez utiliserCtrl+D.
Astuce gdb : Pour exécuter l’instruction courante et s’arrêter à la prochaine, il est possible d’utiliser
siouni(pourstep instructionetnext isntruction).La différence entre les deux est que lors de l’appel d’une fonction,
niexécute la fonction jusqu’au retour alors quesientre dans la fonction et s’arrête à la première instruction.
Astuce gdb : Le fait de saisir à chaque fois
sipour avancer d’une instruction peut être fastidieux 😤. Vous pouvezspammerutiliser la toucheEntréedans le terminal gdb afin de ré-exécuter la dernière commande que vous avez lancée précédemment.
Astuce gdb : Vous pouvez utiliser la commande
c(oucontinue) pour poursuivre l’exécution du processus jusqu’à arriver à un point d’arrêt.
Astuce gdb : Vous pouvez utiliser le raccourcis
fin(oufinish) pour finir l’exécution d’une fonction jusqu’à atteindre l’adresse de retour et s’y arrêter.
Astuce gdb : La commande
p(ouSi la valeur à afficher est une adresse (ou pointeur), elle ne sera pas déréférencée.
Astuce gdb : Pour afficher un registre, il suffit de le préfixer avec le signe
$. Exemple :print $reg.
Astuce gdb : Vous pouvez utiliser le raccourcis
x( pourexplore) afin d’examiner le contenu d’une zone mémoire.
Astuce gdb : Vous pouvez spécifier un nombre d’éléments à afficher avant les formats afin d’afficher plus ou moins de données en mémoire.
Le nombre d’éléments à afficher ainsi que la taille ne sont utilisables qu’avec
x. Cela ne fonctionnera pas avec
Astuce gdb : Avec
x, vous pouvez également donner en argument une expression avec des opérations (addition, soustraction, multiplication …).Cela peut être pratique pour afficher une donnée dans un tableau dont on connait l’index et l’adresse de base. Par exemple, pour afficher la 5ème case d’un tableau d’éléments de 64 bits :
x 0x401000+8*5(en supposant que le tableau soit stocké à partir de l’adresse0x401000).
Astuce gdb : La commande
searchde pwndbg permet de rechercher des motifs en mémoire.
Astuce gdb : La commande
setpermet d’écrire dans des registres, variables et la mémoire.
Astuce gdb : Vous pouvez utiliser
rel(pourreload) afin de rafraîchir la GUI de pwndbg et voir les changements effectifs.
Astuce gdb : Pour modifier une zone mémoire pointée par un registre, il est possible d’utiliser
set *$reg = value.Pour modifier directement les données pointées par une adresse :
set *0xaddr = value.
Astuce gdb : Si vous ne souhaitez modifier qu’un seul octet (au lieu de 4 par défaut) vous devez le spécifier. Exemple :
set {byte}0x401020 = 0xf5.
Instructions x86
mov reg_d, value
Opérandes
reg_d: registre de destinationvalue: valeur immédiate (ou concrète, constante).
Détails
Cette forme est la plus simple : elle affecte la valeur value au registre de destination reg_d.
C’est une manière de réaliser des affectations de valeurs concrètes (immédiates).
Exemple
Imaginons que eax vaille 0xaabbccdd puis que l’on exécute l’instruction mov eax, 0xdeadbeef. Alors la valeur de eax deviendra 0xdeadbeef.
Équivalent en C
1
2
3
4
5
// Initilisation du registre
int x = 0xaabbccdd; // eax
// Equivalent de : mov eax, 0xdeadbeef
x = 0xdeadbeef;
mov reg_d, reg_s
Opérandes
reg_d: registre de destinationreg_s: registre source
Détails
Le contenu du registre source reg_s est copié dans le registre de destination reg_d.
C’est une manière d’affecter le contenu d’une variable à une autre.
Exemple
1
2
3
4
mov eax, 0xaabbccdd
mov ebx, 0x11223344
mov ebx, eax ; ebx == 0xaabbccdd
Équivalent en C
1
2
3
4
5
6
// Initilisation des registres
int a = 0xaabbccdd; // eax
int b = 0x11223344; // ebx
// Equivalent de : mov ebx, eax
b = a; // b = 0xaabbccdd
mov reg_d, [reg_p]
Opérandes
reg_d: registre de destinationreg_p: registre pointant vers une zone mémoire
Détails
Cette forme est un peu plus complexe que les précédentes car elle fait appel à la notion de pointeur.
Ici reg_d est le registre de destination qui recevra une valeur, jusque-là rien de bien nouveau. Par contre, reg_p ne contient pas la valeur qui sera copiée mais un pointeur vers la valeur en question.
Ainsi, c’est la valeur pointée par reg_p qui est copiée dans reg_d.
C’est une manière de lire des données depuis la mémoire.
Exemple
Imaginons que je veuille exécuter ces instructions :
1
2
3
4
mov eax, 0x700000F0 ; 0x700000F0 -> 0x1a2b3c4d
mov ebx, 0xcafebabe
mov ebx, [eax]
On suppose également que l’adresse 0x700000F0 pointe vers l’entier de 4 octets 0x1a2b3c4d. Lorsque la dernière instruction mov ebx, [eax] sera exécutée, alors ebx vaudra 0x1a2b3c4d. Vous voyez la logique ?
Légères variantes
Il existe quelques variantes où un offset (positif ou négatif) est ajouté au registre reg_p, par exemple :
1
2
mov edx, [eax + 8]
mov ecx, [esi - 0x2000]
Équivalent en C
Cette forme est très similaire à l’utilisation de pointeurs en C :
1
2
3
4
5
6
7
8
9
// Initilisation des registres
int *a = 0x700000f0; // eax
int b = 0xcafebabe; // ebx
// Initilisation de la mémoire
*a = 0x1a2b3c4d;
// Equivalent de : mov ebx, [eax]
b = *a; // b = 0x1a2b3c4d
mov [reg_p], reg_s
Opérandes
reg_p: registre pointant vers une zone mémoirereg_s: registre source
Détails
Normalement, si vous avez bien saisi le principe de l’instruction mov reg_d, [reg_p] vous devriez deviner le fonctionnement de celle-ci.
En fait il s’agit de l’inverse de la précédente instruction. En effet, ici on copie la valeur du registre reg_s vers la zone mémoire pointée par reg_p.
C’est une manière d’écrire des données en mémoire.
Exemple
Reprenons le précédent exemple, nous avons cette fois-ci :
1
2
3
4
mov eax, 0x700000F0 ; 0x700000F0 -> 0x1a2b3c4d
mov ebx, 0xcafebabe
mov [eax], ebx ; 0x700000F0 -> 0xcafebabe
Légères variantes
Il existe quelques variantes où un offset (positif ou négatif) est ajouté au registre reg_p. Il est également possible de remplacer reg_s par une valeur immédiate. Par exemple :
1
2
mov [ebp + 8], edi
mov [esi - 0x200], 0xdeadbeef
Équivalent en C
1
2
3
4
5
6
7
8
9
// Initilisation des registres
int *a = 0x700000f0; // eax
int b = 0xcafebabe; // ebx
// Initilisation de la mémoire
*a = 0x1a2b3c4d; // 0x700000f0 -> 0x1a2b3c4d
// Equivalent de : mov [ebx], eax
*a = b; // 0x700000f0 -> 0xcafebabe
Résumé des différentes formes de mov
Je sais, ça fait beaucoup d’informations d’un coup, voici ainsi un résumé avec un exemple pour chacun des 4 formes possibles. Supposons que dans les 4 cas l’état initial est le suivant :
Alors le résultat est :
Les valeurs en 🔴 sont celles qui ont changé lors de l’exécutions de l’instruction tandis que celles en ⚫ sont les valeurs à l’origine du changement.
lea reg, [...]
Opérandes
reg: registre de destination[...]: valeur qui est souvent une adresse mémoire
Détails
Cette instruction a ainsi une seule forme où la première opérande est toujours un registre, la seconde opérande est une valeur qui est souvent une adresse vers une zone mémoire.
Ce que fait lea est tout simplement la copie de l’opérande de droite, sans la déréférencer, vers le registre de destination.
Voici quelques exemples :
1
2
3
lea eax, [0x400000] ; ici eax = 0x400000
lea edx, [ebp+8] ; ici edx = ebp +8
lea ecx, [ebx+eax] ; ici ecx = ebx+eax
Exemple
Comme
leane déréférence pas la seconde opérande, l’instructionlea eax, [0x400000]copie bien0x400000danseaxet non pas la valeur pointée par0x400000.
En fait, plus simplement, lea copie la valeur entre les crochets vers le registre de destination. En d’autres termes, lea reg, [...] est équivalente à mov reg, ....
J’en vois déjà certains froncer les sourcils 🤨.
Mais si cela est équivalent à faire un
mov, pourquoi se casser la tête avec une instruction en plus ?
En fait, contrairement à mov, l’instruction lea permet de faire de petites opérations au niveau de l’opérande de droite. Par exemple, si je souhaite affecter à ecx la somme de ebx et eax en utilisant mov, je suis obligé d’utiliser une instruction supplémentaire telle que add pour faire l’addition et ensuite stocker le résultat dans ecx avec mov.
Tandis qu’avec lea, je peux simplement faire : lea ecx, [ebx + eax]. Vous savez quoi ? On peut même faire lea ecx, [ebx + eax*2]😎.
Ainsi, lea permet de :
- Stocker le résultat de simples opérations en écrivant une seule instruction
- De manipuler des adresses en y ajoutant, ou non, un offset
S’il n’y avait qu’une seule chose à retenir de
lea: il s’agit d’unmovqui copie la “valeur entre crochets” vers la destination.
add reg_d, reg_s
Opérandes
reg_d: registre de destinationreg_s: registre source
Détails
“Add” en anglais signifie “ajouter”.
Cette instruction réalise ainsi deux actions :
- addition de la valeur du registre source avec celui de destination
- stockage du résultat (la somme) dans le registre de destination
C’est de cette manière que sont réalisées les additions.
Lorsque la somme des deux termes dépasse le plus grand entier que peut stocker le registre de destination, le résultat est tronqué pour qu’il puisse y être stocké
Exemple
Faisons la somme de 0xf0000034 et 0x20001200 :
1
2
3
4
mov eax, 0xf0000034
mov ebx, 0x20001200
add eax, ebx ; eax = 0x10001234 et non pas 0x110001234 car le résultat est tronqué aux 32 bits de poids faible
Équivalent en C
1
2
3
4
5
// Initilisation des registres
int a = 0xf0000034;
int b = 0x20001200;
a = a + b;
Autres formes
Il existe plusieurs autres formes :
add reg, valueadd [ptr], valueadd reg, [ptr]
Leur fonctionnement est toujours le même : somme des deux termes et stockage dans l’opérande de destination.
Toutes les instructions, sauf mention contraire (comme
lea), déréférencent les pointeurs vers des zones mémoire.Dans les précédentes formes, ce n’est donc pas le pointeur
ptrqui est utilisé dans la somme mais la valeur pointée parptrqui est[ptr](qui serait*ptren C).
and ope_d, ope_s
Opérandes
ope_d: opérande de destination. Peut être :- un registre
- un pointeur
ope_s: opérande source. Peut être- une valeur immédiate
- un registre
- un pointeur (vers une zone mémoire)
Détails
L’instruction and réalise un “et logique” entre les bits des deux opérandes. Le résultat est ensuite sauvegardé dans la première opérande (qui ne peut donc pas être une valeur immédiate).
Exemple
1
2
3
4
mov eax, 0xff00ff00
mov ebx, 0xabcdef12
and eax, ebx ; eax = 0xab00ef00
Équivalent en C
1
2
3
4
int a = 0xff00ff00;
int b = 0xabcdef12;
a = a & b;
Autres formes
Il existe d’autres formes en fonction du type d’opérandes mais le principe est toujours le même.
sub ope_d, ope_s
Opérandes
ope_d: opérande de destination. Peut être :- un registre
- un pointeur
ope_s: opérande source. Valeur soustraite. Peut être- une valeur immédiate
- un registre
- un pointeur
Détails
“Sub” provient de “substract” qui signifie soustraire.
Cette instruction réalise ainsi deux actions :
- soustraction de l’opérande source avec l’opérande de destination
ope_d - ope_s. - stockage du résultat (la différence) dans l’opérande de destination
C’est de cette manière que sont réalisées les soustractions.
Contrairement à
add, l’ordre des opérandes est important danssub. En effet, en inversant les opérandes, on inverse le signe du résultat.
Exemple
Faisons la différence de 0xf0000034 avec 0x10000034 :
1
2
3
4
mov eax, 0xf0000034
mov ebx, 0x10000034
sub eax, ebx ; eax = 0xe0000000
Équivalent en C
1
2
3
4
int a = 0xf0000034;
int b = 0x10000034;
a = a - b;
Autres formes
Il existe d’autres formes mais le principe est toujours le même.
cmp ope_d, ope_s
Opérandes
ope_d: opérande de destination. Peut être :- un registre
- un pointeur
ope_s: opérande source. Peut être :- une valeur immédiate
- un registre
- un pointeur
Détails
La comparaison avec cmp est effectuée d’une manière qui peut nous paraître bizarre. En effet, cmp effectue la soustraction suivante sub ope_d, ope_s mais sans stocker le résultat. Ainsi le contenu des opérandes restent inchangées.
Par contre, quelques flags parmi les EFLAGS vont être changés en fonction des valeurs des opérandes et du résultat. C’est à partir de ces EFLAGS que l’on saura si les opérandes sont égales ou s’il y en a une plus grande/petite que l’autre etc.
Il est important que vous ayez en tête la manière dont les entiers sont représentés en informatique, notamment les entiers signés avec le complément à deux.
Plus précisément, ce sont les flags ZF, SF, CF et OF qui nous intéressent principalement (et dans une moindre mesure PF). Nous les avions déjà vus brièvement précédemment, profitons-en pour nous rafraîchir la mémoire et rentrer plus dans les détails.
ZF(Zero Flag) :- 1 si les deux opérandes sont égales. La différence des deux termes vaut donc 0.
- 0 si les deux opérandes sont différentes.
SF(Sign Flag) :- 1 si le bit de poids fort du résultat est non nul. Dans le cas d’une opération signée cela implique qu’il est négatif. Dans le cas où elle est non signé, ce flag n’a pas d’importance.
- 0 si le bit de poids fort du résultat est nul
- Exemple : Prenons la soustraction signée suivante :
0x5 - 0x20 = -0x1b. Le résultat étant négatif, le complément à deux de0x1best0xe5qui s’écrit sur 8 bits en binaire0b11100101. Le bit de poids fort étant à1,SFl’est également. Etant donné qu’il s’agit d’une opération signéeSFnous permet de savoir que le résultat est négatif.
CF(Carry Flag) :- 1 si le résultat possède une retenue.
- 0 si le résultat ne possède pas de retenue
- Exemple : Par exemple, pour l’instruction
add al, blsur 8 bits oùalvaut0xFFetblvaut0x01, le résultat est0xFF + 0x01 = 0x100qui ne tient pas sur les 8 bit deal. Cela génère donc une retenue. Lors d’une soustractiona - b, une retenue est générée lorsquebest plus grand quea.
OF(Overflow Flag) :- 1 si un débordement a lieu avec des valeurs signées. Par exemple, cela peut avoir lieu lorsqu’il y a un résultat négatif d’opérandes positifs et inversement. Ce bit n’a pas d’importance lorsque l’on manipule des valeurs non signées.
- 0 s’il n’y a pas eu de débordement
- Exemple : Prenons l’addition signée suivante :
0x7F + 0x8 = 0x87. Ici, le bit de poids fort de0x87est à1: il s’agit donc d’un résultat négatif (-121). Pourtant, les deux termes sont strictement positifs. Il y a donc eu un débordement (overflow).
PF(Parity Flag) :1si le nombre de bits su résultat est pair0sinon
N’hésitez pas à utiliser asmdebugger pour faire quelques tests. Les 4
flagsétudiés sont affichés sur le site lors de l’exécution des instructions.En effet, si l’utilisation de ces flags vous paraît difficile, sachez que c’est normal car cela fait intervenir des notions que l’on utilise pas, en tant qu’humain, tous les jours comme le complément à deux pour représenter des nombres négatifs.
Lors d’une comparaison avec cmp, le processeur ne sait pas si les opérandes sont signées ou non. En fait, il s’en moque à ce stade. C’est pourquoi il va modifier, si besoin est, ces 4 flags bien que certains soient plutôt utilisés lors d’opérations signées (SF et OF) ou non signées (CF).
Exemples
Voici quelques exemples :
| Instruction | ZF | SF | CF | OF |
|---|---|---|---|---|
cmp 1, 5 | ✅ | ✅ | ||
cmp 5, 1 | ||||
cmp 5, 5 | ✅ | |||
cmp 4, 255 | ✅ | |||
cmp 127, 129 | ✅ | ✅ | ✅ |
Je vous conseille de représenter les entiers sous forme binaire et de faire attention à la représentation du complément à deux. En effet, 129 s’il n’est pas signé vaut 129 mais s’il est signé, il vaut -127.
Équivalent en C
Pour l’instruction cmp, il n’y a pas réellement d’équivalent en C. En fait, cmp n’est jamais (sauf exceptions) utilisées autrement qu’avec des sauts. Ainsi, représenter cmp tout seul dans du code C n’a pas de sens. Par contre, dans toutes les conditions du type if, else vous y trouverez un cmp (ou test) dans le code assembleur associé.
test ope_d, ope_s
Opérandes
ope_d: opérande de destination. Peut être :- un registre
- un pointeur
ope_s: opérande source. Peut être :- une valeur immédiate
- un registre
Détails
Cette instruction est également utilisée pour réaliser des comparaisons mais son fonctionnement sous-jacent est différent de cmp.
test va exécuter l’instruction and ope_d, ope_s sans stocker le résultat mais en mettant à jour des flags suivants : SF, ZF et PF. test est souvent utilisé pour savoir si un registre est nul ou non.
Exemple
L’instruction test eax, eax permet de voir si eax est nul ou non. En effet, lors de l’exécution de cette instruction, si ZF == 1, c’est que eax est nul. Sinon, cela signifie qu’il est non nul.
Équivalent en C
Même remarque que pour cmp : il n’y a pas réellement d’équivalent direct en C.
jmp dest
Opérandes
dest: destination du saut. Peut être :- une valeur immédiate (exemple : adresse relative ou absolue)
- un registre
- un pointeur
Détails
Unique instruction permettant de réaliser des sauts inconditionnels afin de “sauter” vers l’adresse de destination. Cela permet de pouvoir exécuter des instructions qui ne sont pas toujours situées linéairement dans le code.
La différence entre un saut et un appel de fonction
callest que l’on ne se préoccupe pas de sauvegarder l’adresse de retour afin de pouvoir y retourner plus tard.
Lorsque l’opérande dest est une valeur immédiate, il peut s’agir d’une adresse absolue ou relative :
- adresse absolue : l’adresse est “codée en dur” dans l’opcode de l’instruction. Cela permet de sauter plus loin dans le code mais l’instruction prend plus de place.
- Exemple :
e9 d8 12 00 00 jmp 0x12dd
- Exemple :
- adresse relative : seule la différence entre l’adresse courante de
eipet l’adresse de destination est insérée dans l’opcode. Cela permet d’avoir des opcodes plus courts mais de sauter moins loin.- Exemple :
eb 2a jmp short 0x12DC
- Exemple :
Concernant les adresses absolues, elles ne sont pas insérées tel quel dans l’opcode. En effet, il est nécessaire de prendre en compte la taille de l’instruction de saut (par exemple 5 octets) avant d’insérer l’adresse de destination. C’est pourquoi l’opcode de l’exemple contient
e9 d8 12et non pase9 dd 12.
Bien que le mnémonique jmp utilisé soit le même, il existe différentes forme où dest n’est pas toujours une adresse. Cela peut, en effet, être un pointeur ou registre.
Le souci, en tant que reverser, est qu’il ne sera pas toujours possible de savoir directement vers quelle adresse le processeur va sauter lorsqu’un registre (ou pointeur) va être utilisé. En analyse statique, il sera nécessaire de déterminer les différentes valeurs que peut prendre le registre afin de trouver les potentielles destinations.
Le fait d’utiliser un registre comme opérande est très commun dans la modélisation des switch en assembleur après compilation.
Exemple
1
2
3
4
jmp 0x401020
jmp rax
jmp [ebx]
Équivalent en C
Les sauts inconditionnels jmp sont l’équivalent de goto en C :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int main() {
int i = 0;
start_loop:
if (i < 5) {
printf("i = %d\n", i);
i++;
goto start_loop; // Sauter à l'étiquette start_loop
}
return 0;
}
jcc dest
Opérandes
dest: destination du saut. Peut être :- une valeur immédiate (exemple : adresse relative ou absolue)
Détails
jcc n’est pas un mnémonique en soi. Il s’agit d’un terme générique pour désigner le mnémonique de tous les sauts conditionnels. Les points communs de tous ces sauts sont les suivants :
- Ils utilisent certains flags parmi les EFLAGS afin de savoir s’il faut sauter
- Lorsque que le saut n’est pas exécutée, c’est l’instruction située immédiatement après le saut qui est réalisée
- Ils sont précédés d’une instruction
cmpoutest
Si vous retenez ça, vous avez retenu 60% du fonctionnement des sauts conditionnels. Le reste consiste seulement à se rappeler de ce que signifie chaque mnémonique et quels flags sont utilisés.
Voici les principaux sauts que vous pourrez rencontrer :
Selon le désassembleur utilisé, il peut y avoir quelques différences dans le mnémonique comme
jz(jump if zero) qui peut être désignéje(jump if equal) mais qui représentent exactement la même instruction.
| Mnémonique(s) | Description | Signe des opérations | Cas d’utilisation | Condition de saut |
|---|---|---|---|---|
jo | Jump if overflow | Détection de débordement | OF == 1 | |
jno | Jump if not overflow | Détection de débordement | OF == 0 | |
js | Jump if sign | Tester le signe | SF == 1 | |
jns | Jump if not sign | Tester le signe | SF == 0 | |
jz / je | Jump if zero / equal | Tester l’(in)égalité | ZF == 1 | |
jnz / jne | Jump if not zero / not equal | Tester l’(in)égalité | ZF == 0 | |
jb / jnae / jc | Jump if below / not above or equal / carry | Non signé | Tester la supériorité / infériorité | CF == 1 |
jnb / jae / jnc | Jump if not below / above or equal / not carry | Non signé | Tester la supériorité / infériorité | CF == 0 |
jbe / jna | Jump if below or equal / not above | Non signé | Tester la supériorité / infériorité | CF == 1 \|\| ZF == 1 |
jnbe / ja | Jump if not below or equal / above | Non signé | Tester la supériorité / infériorité | CF == 0 && ZF == 0 |
jl / jnge | Jump if less / not greater or equal | Signé | Tester la supériorité / infériorité | SF != OF |
jnl / jge | Jump if not less / greater or equal | Signé | Tester la supériorité / infériorité | SF == OF |
jng / jle | Jump if not greater / less or equal | Signé | Tester la supériorité / infériorité | ZF == 1 \|\| SF != OF |
jg / jnle | Jump if greater / not less or equal | Signé | Tester la supériorité / infériorité | ZF == 0 && SF == OF |
Il est à noter qu’il n’existe pas une seule manière de représenter une condition du C vers l’assembleur. Prenons par exemple le code suivant :
1
2
3
4
5
6
7
8
9
10
unsigned int x = ...;
unsigned int y = ...;
if (x > y )
{
// Code A
}
else
{
// Code B
}
On peut très bien faire :
1
2
3
cmp x, y
ja addr_code_A
code_B
ou :
1
2
3
cmp x, y
jbe addr_code_B
code_A
Il faut donc être attentif lorsque l’on analyse du code assembleur pour savoir ce qui va être exécuté et sous quelles conditions.
Exemples
1
2
jz 0x555555550102
jns 0x405987
Équivalent en C
Selon le signe des variables comparées et le type de comparaison utilisé, certains sauts vont être utilisés plutôt que d’autres (les différents mnémoniques d’une même instruction ont été omis par souci de concision) :
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
int x = ...;
int y = ...;
if (x < 0) // js ou jns
{
//...
}
if (x == y) //jz ou jnz
{
//...
}
if(x < y) // jl ou jnl
{
//...
}
if(x >= y) // jnl ou jl
{
//...
}
if(x <= y) // jle ou jnle
{
//...
}
Autres formes
Il existe d’autres sauts mais que l’on rencontre moins souvent.
cdq
Opérandes
- Cette instruction n’a pas d’opérandes
Détails
cdq est l’abréviation de convert dword to qword. Vous l’avez compris, cela devrait donc permettre de convertir un dword (4 octets) en un qword (8 octets), mais comment ?
Tout d’abord, cette instruction ne s’applique que sur le registre eax (ou ses dérivées). C’est pourquoi elle ne dispose pas d’opérandes. De plus, cette instruction garde le signe de l’ancienne valeur lors de la conversion vers la nouvelle valeur.
En x86_64 on a des registres de 64 octets, ce qui n’est pas le cas en x86. Ainsi, pour doubler la taille des données contenues dans eax, c’est le registre edx (ou ses dérivées) qui va être utilisé de cette manière :
- si le nombre dans
eaxest négatif (bit de poids fort égal à1), alorsedxest rempli de1 - si le nombre dans
eaxest positif (bit de poids fort égal à0), alorsedxest rempli de0
Cette manière de générer une nouvelle valeur à partir d’une valeur signée est ce que l’on appelle l’extension de signe.
Ainsi on obtient une valeur de taille double en concaténant les deux registres sous la forme : edx:eax.
Cette instruction est très utilisée lors des divisions signées afin d’avoir un résultat cohérent et correct.
Exemples
1
2
3
4
5
mov eax, 0x70001234
cdq ; edx:eax = 0x00000000:0x70001234
mov eax, 0x80001234
cdq ; edx:eax = 0xffffffff:0x80001234
Équivalent en C
Il n’y pas a pas d’équivalent directe en C.
Autres formes
Il existe plusieurs dérivées mais dont le principe d’extension de signe est le même :
cwd(convert word to dword): la valeur convertie est contenue dansdx:axcqo(convert qword to double qword): la valeur convertie est contenue dansrdx:rax(disponible seulement en x86_64)
shr ope_d, n et sar ope_d, n
Opérandes
ope_d: opérande de destination. Peut être :- un registre
- un pointeur
n: opérande source. Peut être :- une valeur immédiate
- un registre (seulement le registre
cl)
Détails
L’instruction shr (ou shift right) permet de réaliser un décalage des bits de ope_d de n bits vers la droite.
Avec l’instruction
shret toutes les autres instruction deshift(décalage), il n’y a pas de rotation des bits sortants.Il existe d’autres instructions comme
ror/rolqui réalise un décalage rotatif des bits. C’est-à-dire que des bits qui sortent, par exemple, par la gauche, “rerentrent” par la droite.
Ainsi, le décalage de 0b01110011 d’un bit vers la droite est 0b00111001.
En fait, lorsqu’il y a un bit sortant, il n’est pas réellement perdu dans la nature : il est sauvegardé dans le flag
CFdes EFLAGS.
Il existe l’instruction sar (ou shift aritmetic right) est basée sur le même principe de décalage que shr. La seule différence est que sar prend en compte le signe du nombre qui sera décalé.
Ainsi, si le bit de poids fort de ope_d est 1, il sera réinitialisé à 1 après décalage. En fait sar agit en deux temps :
- exécuter
shr - si le précédent nombre était signé, mettre le bit de poids fort du résultat à
1
Voir les exemples ci-dessous pour comprendre de quoi il s’agit.
Ces instructions sont très utilisées pour réaliser des divisions par 2 d’un nombre (et dont le reste est dans le flag CF). En effet, le décalage d’un bit vers la droite revient à diviser par 2. Le décalage de n bits vers la droite revient à diviser par 2 puissance n.
Je ne vois pas en quoi décaler d’un bit vers la droite revient à diviser par deux ?
Pourtant c’est bien ce qui se passe lorsque l’on note un nombre en décimal et que l’on le décale d’une unité vers la droite, cela revient à diviser par 10.
Prenons par exemple 213950, en le décalant d’une unité vers la droite on obtient 21395, ce qui revient bien à diviser par 10.
Avec la notation en binaire, c’est la même chose : décaler d’un bit revient à diviser par deux.
Ainsi, sar et shr sont très utilisés pour réaliser des divisions de puissances de 2.
Exemple
1
2
3
4
5
6
7
mov eax, 0x80000001 (0b10.....001)
shr eax, 1 ; eax = 0x40000000 (0b01.....000)
; CF == 1
mov eax, 0x80000001
sar eax, 1 ; eax = 0xc0000000 (0b11.....000)
; CF == 1
Équivalent en C
1
2
3
4
5
int x = 0x80000001;
x = x >> 1; // x = 0xc0000000
int y = 0xdeadbeef;
y = y >> 13; // y = 0xfffef56d
Autres formes
De la même manière que shr/sar permettent de réaliser des décalages vers la droite, shl/sal permettent de réaliser des décalages vers la gauche avec le même principe.
A l’instar de la division par puissances de 2 de shr/sar, shl/sal permettent de réaliser des multiplications par puissances de 2 :
- ➡️
shr/sar: division par puissances de 2 - ⬅️
shl/sal: multiplication par puissances de 2
Vous pouvez également jeter un œil aux instructions rcl/rcr/rol/ror. Leur fonctionnement de décalage est le même. La principale différence est qu’il y a une rotation des bits sortants.

