Description :
Ce mémo fournit une introduction détaillée à l’assembleur ASM
en se concentrant principalement sur les registres, les instructions de base, et l’alignement de la pile. Voici un retour sur chaque section du mémo :
-
Introduction à l’ASM : Ce paragraphe donne une définition générale de ce qu’est l’ASM et explique son rôle en tant que langage de programmation de bas niveau. Il met en avant le fait que l’ASM permet une interaction directe avec l’architecture matérielle de l’ordinateur.
-
Registres et Concepts de Base : Cette section présente les registres les plus couramment utilisés en ASM et explique leur rôle. Elle fournit également des exemples concrets de situations où chaque registre est utilisé. Cette partie est très informative et donne une compréhension approfondie des registres en ASM.
-
Registres en Version 64 Bits : Cette partie explique la différence entre les registres en version 32 bits et 64 bits, soulignant que les registres 64 bits sont utilisés pour une meilleure performance et la gestion de données plus volumineuses.
-
Registres d’Usage Général R8 à R15 (Version 64 Bits) : Elle introduit les registres d’usage général en version 64 bits, expliquant qu’ils offrent un espace de stockage plus important pour des opérations avancées. Cette information est utile pour ceux qui travaillent sur des architectures 64 bits.
-
Instructions de Base : Cette section résume les instructions de base couramment utilisées en ASM. Elle couvre les instructions de transfert de données, les instructions arithmétiques, les instructions de saut, et les instructions de pile. Cela fournit une vue d’ensemble des opérations possibles en ASM.
-
Gestion de la Mémoire - Introduction : Elle introduit la gestion de la mémoire en ASM et explique son importance, en mettant l’accent sur les octets comme unité de base.
-
Concepts de base : Cette section explique ce que sont les bits et les octets, offrant des informations de base sur la structure des données en mémoire.
-
Alignement de la pile : Elle explique l’importance de l’alignement de la pile en ASM et donne des exemples concrets pour montrer comment maintenir un alignement approprié. Cette partie est particulièrement cruciale pour éviter des erreurs inattendues liées à la pile.
-
Exemples d’alignement de la pile : Elle donne des exemples pratiques d’alignement de la pile pour les architectures x86 et x86-64, illustrant comment garantir un alignement correct lors de l’allocation d’espace sur la pile.
-
Problèmes de désalignement de la pile : Cette section met en garde contre les problèmes potentiels liés au désalignement de la pile et explique comment les éviter en utilisant des instructions de manière équilibrée.
-
Vérification de l’alignement de la pile : Elle explique comment vérifier l’alignement de la pile à l’aide d’instructions de test, ce qui est essentiel pour garantir que la pile est correctement alignée avant de poursuivre l’exécution du code.
En résumé, ce mémo est une ressource précieuse pour ceux qui souhaitent comprendre les bases de l’ASM, notamment les registres, les instructions, et l’alignement de la pile. Il fournit une explication détaillée et des exemples concrets pour aider à renforcer la compréhension de ces concepts fondamentaux en programmation assembleur.
Registres et Concepts de Base
En ASM, les registres sont des emplacements spéciaux dans le processeur utilisés pour stocker des données temporaires et des résultats de calcul. Voici quelques registres couramment utilisés en ASM :
-
EAX (Registre accumulateur) : Utilisé pour les calculs et le stockage de résultats.
-
EBX (Registre de base) : Utilisé pour stocker des données importantes ou des adresses mémoire.
-
ECX (Registre compteur) : Principalement utilisé comme compteur dans les boucles.
-
EDX (Registre de données étendu) : Utilisé pour stocker des résultats temporaires.
-
ESP (Stack Pointer) : Gestion de la pile.
-
EBP (Base Pointer) : Accès aux paramètres de fonction et aux variables locales.
-
ESI (Source Index) : Opérations sur des chaînes (source d’index).
-
EDI (Destination Index) : Opérations sur des chaînes (destination d’index).
-
EIP (Instruction Pointer) : Adresse de l’instruction en cours.
-
Registres généraux (EAX, EBX, ECX, EDX) : Imaginez ces registres comme des boîtes spéciales où vous pouvez mettre des nombres temporaires. Par exemple, vous avez une boîte appelée “EAX” où vous mettez temporairement le résultat d’une addition, comme 5 + 3 = 8. Donc, EAX contiendrait temporairement la valeur 8. Ces registres sont utilisés pour stocker des données en cours de calcul.
-
EAX (Registre accumulateur) :
- Exemple 1 : Calculer la somme de deux nombres, par exemple, 5 + 3.
- Exemple 2 : Stocker le résultat d’une multiplication, par exemple, 4 * 6.
- Exemple 3 : Garder en mémoire la valeur d’un compteur pendant une boucle.
-
EBX (Registre de base) :
- Exemple 1 : Stocker une adresse mémoire importante, comme l’emplacement d’un tableau.
- Exemple 2 : Utiliser comme registre général pour effectuer des opérations sur des données.
- Exemple 3 : Garder en mémoire une valeur constante utilisée à plusieurs endroits dans le programme.
-
ECX (Registre compteur) :
- Exemple 1 : Utiliser comme compteur dans une boucle, par exemple, pour répéter une opération 10 fois.
- Exemple 2 : Utiliser comme compteur pour parcourir les éléments d’un tableau.
- Exemple 3 : Compter le nombre d’occurrences d’un caractère dans une chaîne.
-
EDX (Registre de données étendu) :
- Exemple 1 : Stocker un résultat temporaire lors de calculs complexes.
- Exemple 2 : Utiliser comme registre pour stocker un résultat de division.
- Exemple 3 : Garder en mémoire un pointeur vers une structure de données.
EIP (Instruction Pointer) : Pensez à l’EIP
comme un marque-page dans un livre. Il indique la page actuelle que vous lisez. À chaque fois que vous tournez une page, vous déplacez le marque-page vers la page suivante. L’EIP
fonctionne de la même manière, il pointe vers la prochaine instruction à exécuter. Par exemple, si votre programme exécute une instruction pour additionner 5 + 3, l’EIP
indique que la prochaine instruction est celle qui affiche le résultat.
EBP (Base Pointer) et ESP (Stack Pointer) : Visualisez une pile de livres. ESP
est comme votre doigt pointant vers le sommet du livre que vous êtes en train de lire, c’est-à-dire la position actuelle de la pile. EBP
est comme un autre doigt que vous utilisez pour garder une trace d’une page particulière que vous voulez revenir plus tard. Ainsi, EBP
vous permet de vous rappeler où vous en étiez dans la lecture, par exemple, lors de l’appel d’une fonction.
EFLAGS (Flags) : Les EFLAGS
sont comme des indicateurs de statut qui vous informent sur ce qui se passe pendant l’exécution de votre programme. Chacun de ces indicateurs est un drapeau qui peut être “levé
” ou “baissé
” en fonction du résultat des opérations. Par exemple, il y a un drapeau appelé "est-nul" (ZF)
qui est levé si le résultat d’une opération est zéro
, comme 5 - 5 = 0. Vous pouvez vérifier ce drapeau pour prendre des décisions dans votre programme. Si le drapeau “est-nul” est levé, cela signifie que quelque chose d’important s’est produit, comme une égalité, et vous pouvez décider de réagir en conséquence. Les EFLAGS
sont essentiels pour permettre à votre programme de réagir intelligemment en fonction des résultats des opérations qu’il effectue.
Registres en Version 64 Bits
Les registres en version 32 bits
(EAX
, EBX
, ECX
, EDX
) sont les homologues des registres en version 64 bits
(RAX
, RBX
, RCX
, RDX
). La principale différence réside dans leur capacité à manipuler des valeurs plus grandes en 64 bits, offrant ainsi une plus grande capacité de stockage. Les registres 32 bits sont largement utilisés dans les systèmes d’exploitation et les applications 32 bits, tandis que les registres 64 bits sont utilisés dans les environnements 64 bits pour une meilleure performance et pour gérer des données plus volumineuses.
Registres d’Usage Général R8 à R15 (Version 64 Bits)
L’architecture x86-64
introduit de nouveaux registres d’usage général, à savoir R8
, R9
, R10
, R11
, R12
, R13
, R14
et R15
. Ces registres supplémentaires sont conçus pour des opérations avancées et pour optimiser le code. Ils fonctionnent de manière similaire aux registres EAX
, EBX
, ECX
et EDX
, mais offrent un espace de stockage plus important. Les registres R8 à R15
sont particulièrement utiles pour manipuler des données de grande taille, effectuer des calculs complexes et améliorer la performance des programmes.
Instructions de Base
En ASM, vous travaillez avec un ensemble d’instructions spécifiques. Voici quelques-unes des instructions ASM couramment utilisées :
- MOV (Move) : Déplace des données d’un endroit à un autre.
- ADD (Addition) : Effectue des opérations d’addition.
- SUB (Subtraction) : Effectue des opérations de soustraction.
- JMP (Jump) : Change le flux d’exécution en sautant à une autre adresse.
- CALL (Call) : Appelle des fonctions en empilant l’adresse de retour sur la pile.
- PUSH (Push) : Empile des valeurs sur la pile.
- POP (Pop) : Dépile des valeurs de la pile.
- CMP (Compare) : Compare deux valeurs.
- JZ (Jump if Zero) : Saut conditionnel si le résultat d’une opération est zéro.
- RET (Return) : Retour d’une fonction.
Instructions de transfert de données :
- MOV (Move) - Déplace des données d’un endroit à un autre, par exemple, entre registres ou entre registres et la mémoire.
Instructions arithmétiques :
-
ADD (Addition) - Effectue des opérations d’addition sur des valeurs, qu’elles soient dans des registres ou en mémoire.
-
SUB (Subtraction) - Effectue des opérations de soustraction sur des valeurs, qu’elles soient dans des registres ou en mémoire.
-
XOR (Exclusive OR) - Effectue une opération de “ou exclusif” entre deux valeurs, souvent utilisée pour inverser des bits ou effectuer des opérations de masquage.
-
AND (Bitwise AND) - Effectue une opération logique “et” entre deux valeurs, souvent utilisée pour définir ou effacer des bits spécifiques.
-
OR (Bitwise OR) - Effectue une opération logique “ou” entre deux valeurs.
-
NOT (Bitwise NOT) - Effectue une opération logique “non” sur une valeur, inversant tous les bits.
-
SHL/SHR (Shift Left/Right) - Décale les bits d’une valeur vers la gauche (SHL) ou la droite (SHR), ce qui est utile pour les opérations de décalage de bits.
Instructions de saut (Jump) :
- JMP (Jump) - Change le flux d’exécution en sautant à une autre adresse, permettant de créer des boucles ou des structures conditionnelles.
- JZ (Jump if Zero) - Saut conditionnel si le résultat d’une opération précédente est zéro.
- JLE (Jump if Less or Equal) - Sauter si le résultat est inférieur ou égal, en ignorant le zéro (ZF).
- JNE (Jump if Not Equal) - Sauter si le résultat n’est pas égal, en utilisant le drapeau zéro (ZF).
- JNO (Jump if No Overflow) - Sauter si aucune surcharge (overflow) ne s’est produite lors de la dernière opération arithmétique.
- JNS (Jump if No Sign) - Sauter si le drapeau de signe (SF) est à zéro, indiquant un nombre positif ou nul.
- JO (Jump if Overflow) - Sauter si une surcharge (overflow) s’est produite lors de la dernière opération arithmétique.
- JS (Jump if Sign) - Sauter si le drapeau de signe (SF) est à un, indiquant un nombre négatif.
- JMP (Jump Unconditional) - Sauter inconditionnellement à l’adresse spécifiée, utilisé pour créer des boucles ou des sauts inconditionnels.
- JCXZ (Jump If CX/ZF is Zero) - Sauter si le registre CX (ou ECX en x86) est zéro, souvent utilisé pour des boucles basées sur un compteur.
- JA (Jump if Above) - Saute si le résultat de la dernière opération est supérieur, en ignorant la retenue (CF) et le zéro (ZF).
- JAE (Jump if Above or Equal) - Sauter si le résultat est supérieur ou égal, en ignorant la retenue (CF) et le zéro (ZF).
- JB (Jump if Below) - Sauter si le résultat est inférieur, en prenant en compte la retenue (CF).
- JBE (Jump if Below or Equal) - Sauter si le résultat est** inférieur ou égal**, en prenant en compte la retenue (CF).
- JE (Jump if Equal) - Sauter si le résultat est égal, en utilisant le drapeau zéro (ZF).
- JG (Jump if Greater) - Sauter si le résultat est supérieur, en prenant en compte le zéro (ZF).
- JGE (Jump if Greater or Equal) - Sauter si le résultat est supérieur ou égal, en prenant en compte le zéro (ZF).
- JL (Jump if Less) - Sauter si le résultat est inférieur, en ignorant le zéro (ZF).
- LOOP (Loop) - Sauter jusqu’à ce que le registre de compteur (CX ou ECX) atteigne zéro, utilisé pour implémenter des boucles.
Instructions de pile :
-
CALL (Call) - Appeler une fonction ou une sous-routine en empilant l’adresse de retour sur la pile.
-
PUSH (Push) - Empile des valeurs sur la pile, souvent utilisé pour passer des paramètres à des fonctions ou pour sauvegarder des registres.
-
POP (Pop) - Dépile des valeurs de la pile, utilisé pour récupérer des valeurs ou restaurer des registres après l’appel d’une fonction.
-
RET (Return) - Retourner d’une fonction en utilisant l’adresse de retour stockée sur la pile.
-
IRET (Interrupt Return) - Utilisé pour retourner à la routine d’interruption après avoir traité une interruption matérielle.
Instructions de manipulation de bits :
-
CMP (Compare) - Compare deux valeurs sans modifier les données, mais en modifiant les drapeaux (flags), ce qui est utile pour des sauts conditionnels.
-
XCHG (Exchange) - Cette instruction échange les contenus de deux opérandes, qu’ils soient dans des registres ou en mémoire.
-
BSWAP (Byte Swap) - Cette instruction effectue un échange d’octets dans un registre, ce qui est utile pour manipuler des données stockées dans l’ordre des octets inversé.
Instructions de gestion de chaînes (String) :
-
STOS (Store String) - Les instructions
STOS
sont utilisées pour stocker unoctet
(STOSB
), unmot
(STOSW
), ou undouble-mot
(STOSD en x86 ou STOSQ en x86-64) dans la mémoire à l’emplacement pointé par la source (ESI en x86, RSI en x86-64). -
LODS (Load String) - Les instructions
LODS
sont utilisées pour charger unoctet
(LODSB
), unmot
(LODSW
), ou unedouble-mot
(LODSD en x86 ou LODSQ en x86-64) depuis la mémoire à l’emplacement point
Gestion de la Mémoire
Introduction
Lorsque vous écrivez des programmes en ASM
pour les architectures x86
et x86-64
, vous travaillez souvent avec des données stockées en mémoire. Les octets jouent un rôle essentiel dans la manière dont ces données sont organisées et accédées.
Concepts de base
-
Bit : Un
bit
est la plus petite unité de données en informatique, et il peut avoirdeux valeurs
: 0 ou 1. En termes de mémoire, on peut considérer chaque bit comme un interrupteur pouvant être"allumé" (1)
ou"éteint" (0)
. -
Octet : Un
octet
est constitué de8 bits
, ce qui signifie qu’il peut représenter 256 combinaisons différentes (2^8). En programmation en ASM, l’octet est souvent utilisé comme unité de base pour manipuler et stocker des données en mémoire.
Alignement de la pile
Lorsque vous allouez de l’espace sur la pile (la pile est une zone de mémoire spéciale utilisée pour stocker temporairement des données et des adresses de retour de fonctions), vous devez faire attention à l’alignement des données. Cela signifie que vous devez vous assurer que les données que vous placez sur la pile commencent à des adresses mémoire qui sont multiples de la taille d’un octet (généralement 4 octets ou 8 octets pour x86 et x86-64).
Par exemple, si vous avez besoin de stocker un entier de 4 octets (32 bits) sur la pile en x86, vous devez vous assurer qu’il est placé à une adresse mémoire divisible par 4. Pour un entier de 8 octets (64 bits) en x86-64, vous devez vous assurer qu’il est placé à une adresse mémoire divisible par 8.
Cela garantit que l’accès aux données en mémoire est efficace et rapide, car les processeurs sont généralement optimisés pour accéder aux données alignées de cette manière. Si la pile est correctement alignée, l’adresse de la pile sera toujours un multiple de la taille du mot.
En résumé, l’alignement de la pile en ASM x86 et x86-64 est important pour garantir que les données sont stockées à des emplacements mémoire compatibles avec leur taille, en utilisant la notion d’octets comme unité fondamentale. Cela permet d’optimiser l’efficacité des accès mémoire et le fonctionnement global du programme.
Exemples allouer son propre espace mémoire au sein de la pile d’exécution
Exemple 1 - Alignement pour un entier 32 bits (x86) :
Supposons que vous souhaitiez allouer de l’espace sur la pile pour stocker un entier de 4 octets (32 bits) en ASM x86. Pour assurer un alignement approprié, voici comment vous pourriez le faire :
push ebp ; Sauvegarde de la valeur de la base de pile (ebp)
mov ebp, esp ; ebp pointe maintenant vers le sommet de la pile
sub esp, 4 ; Alloue 4 octets d'espace sur la pile
; votre code ici
add esp, 4 ; Restaure l'ESP en ajoutant 4 octets
pop ebp ; Restaure la valeur précédente de la base de la pile (ebp)
L’entier est placé à une adresse mémoire qui est un multiple de 4, car nous avons soustrait 4 de la valeur d’esp pour réserver l’espace. Cela assure un alignement adéquat.
Exemple 2 - Alignement pour un entier 64 bits (x86-64) :
Si vous travaillez en ASM x86-64 et que vous avez besoin de stocker un entier de 8 octets (64 bits) sur la pile, voici comment vous pourriez le faire en garantissant un alignement approprié :
push rbp ; Sauvegarde de la valeur de la base de pile (rbp)
mov rbp, rsp ; rbp pointe maintenant vers le sommet de la pile
sub rsp, 8 ; Alloue 8 octets d'espace sur la pile
; votre code ici
add rsp, 8 ; Restaure RSP en ajoutant 8 octets
pop rbp ; Restaure la valeur précédente de la base de pile (rbp)
De même, l’entier est placé à une adresse mémoire qui est un multiple de 8, car nous avons soustrait 8 de la valeur de rsp pour réserver l’espace.
En résumé, peu importe la taille de la donnée que vous souhaitez stocker sur la pile, vous devez vous assurer que son adresse de début est un multiple de l’unité d’alignement appropriée pour votre architecture (4 octets pour x86 et 8 octets pour x86-64, en général). Cela contribue à l’efficacité des accès mémoire et au bon fonctionnement du programme.
Problèmes de désalignement de la pile
L’un des moyens courants de provoquer un désalignement de la pile en utilisant les instructions push
et pop
est de les utiliser de manière déséquilibrée, c’est-à-dire en poussant plus d’éléments sur la pile que vous ne les retirez, ou vice versa. Voici un exemple en ASM x86 où un désalignement de la pile se produit :
section .data
section .text
global main
main:
; Équilibre correct entre push et pop
push eax
push ebx
push ecx
pop ecx
pop ebx
pop eax
; À ce stade, la pile est correctement alignée car le nombre de push
; est égal au nombre de pop.
; Déséquilibre entre push et pop
push eax
push ebx
pop ecx
; À ce stade, la pile est désalignée car il y a un élément de trop
; dans la pile (ebx) par rapport aux pop effectués.
; Vous pouvez effectuer d'autres opérations ici, mais la pile est désalignée.
; Pour réaligner la pile, vous devez ajouter un pop pour chaque push
pop ebx
pop eax
; À ce stade, la pile est réalignée.
; Terminer le programme
ret
Dans cet exemple, la pile est correctement alignée au début car le nombre de push
est égal au nombre de pop
. Cependant, après le déséquilibre entre push
et pop
, la pile devient désalignée car il reste un élément (ebx
) dans la pile qui n’a pas été retiré. Pour réaligner la pile, nous ajoutons ensuite un pop
pour chaque push
en trop.
Le désalignement de la pile peut entraîner des problèmes de comportement inattendu dans un programme, car les accès mémoire ultérieurs supposent généralement que la pile est correctement alignée. Par conséquent, il est essentiel de s’assurer que les instructions push
et pop
sont utilisées de manière équilibrée pour maintenir l’alignement de la pile.
Vérification de l’alignement de la pile
Dans la programmation assembleur, il est essentiel de vérifier l’alignement de la pile avant de poursuivre l’exécution du code. L’alignement de la pile est une précaution cruciale, car de nombreuses instructions processeur et fonctions d’appel système supposent que la pile est correctement alignée pour garantir un fonctionnement correct.
Pour effectuer cette vérification, nous utilisons les deux instructions suivantes dans cet exemple :
x86 (32 bits) :
mov eax, esp ; Charge l'adresse actuelle de la pile dans le registre EAX.
and eax, 0x03 ; Effectue un ET logique avec 0x03 pour examiner les 2 derniers bits.
x86-64 (64 bits) :
mov rax, rsp ; Charge l'adresse actuelle de la pile dans le registre RAX.
and rax, 0x07 ; Effectue un ET logique avec 0x07 pour examiner les 3 derniers bits.
En utilisant cette opération AND
, nous pouvons maintenant examiner les bits les moins significatifs (les bits de droite) de EAX
ou RAX
pour déterminer si la pile est correctement alignée. Voici ce que cela signifie :
-
Si les deux bits de droite sont à zéro, alors la pile est correctement alignée. Cela signifie que l’adresse
ESP
ouRSP
est un multiple de 4 (pour x86 32 bits) ou un multiple de 8 (pour x86-64 64 bits), respectivement. L’alignement sur une limite de mot double est assuré. -
En revanche, si l’un ou les deux bits de droite sont à un, cela indique que la pile n’est pas correctement alignée. Dans ce cas, des précautions supplémentaires doivent être prises pour garantir l’alignement correct de la pile, car un mauvais alignement peut potentiellement entraîner des erreurs lors de l’exécution de certaines instructions ou fonctions.
Ces vérifications d’alignement de pile sont importantes pour assurer la compatibilité et la stabilité du code assembleur, en particulier lorsque vous interagissez avec d’autres parties du système ou lorsque vous appelez des fonctions du système d’exploitation.