Pcasm Book FR
Pcasm Book FR
Pcasm Book FR
Langage d'assemblage PC
Paul A. Carter
23 juillet 2006
Machine Translated by Google
Copyright c 2001, 2002, 2003, 2004 par Paul Carter
Ceci peut être reproduit et distribué dans son intégralité (y compris cet avis d'auteur,
de copyright et d'autorisation), à condition qu'aucun frais ne soit facturé pour le
document luimême, sans le consentement de l'auteur. Cela inclut les extraits «
d'utilisation équitable » tels que les critiques et la publicité, et les œuvres dérivées
telles que les traductions.
Notez que cette restriction ne vise pas à interdire la facturation du service d'impression
ou de copie du document.
Les instructeurs sont encouragés à utiliser ce document comme ressource de classe ;
cependant, l'auteur apprécierait d'être avisé dans ce cas.
Machine Translated by Google
Contenu
Préface v
1. Introduction 1
1.1 Systèmes de numération . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1.1 Décimal . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1.2 Binaire . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1.3 Hexadécimal . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 Organisation informatique . . . . . . . . . . . . . . . . . . . . . 4
1.2.1 Mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2.2 Le processeur . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.2.3 La famille de processeurs 80x86 . . . . . . . . . . . . . . . . 6
1.2.4 Registres 8086 16 bits . 1.2.5 . . . . . . . . . . . . . . . . . . 7
Registres 80386 32 bits . . . . . . . . . . . . . . . . . . 8
1.2.6 Mode réel . 1.2.7 . . . . . . . . . . . . . . . . . . . . . . . 8
Mode protégé 16 bits . 1.2.8 Mode protégé . . . . . . . . . . . . . . . . 9
32 bits . . . . . . . . . . . . . . . . . . dix
1.2.9 Interruptions . . . . . . . . . . . . . . . . . . . . . . . . . dix
1.3 Langage d'assemblage . . . . . . . . . . . . . . . . . . . . . . . 11
1.3.1 Langage machine . . . . . . . . . . . . . . . . . . . . 11
1.3.2 Langage d'assemblage . . . . . . . . . . . . . . . . . . . . 11
1.3.3 Opérandes d'instruction . . . . . . . . . . . . . . . . . . . 12
1.3.4 Instructions de base . . . . . . . . . . . . . . . . . . . . 12
1.3.5 Directives . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.3.6 Entrée et sortie . . . . . . . . . . . . . . . . . . . . 16
1.3.7 Débogage . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.4 Création d'un programme . . . . . . . . . . . . . . . . . . . . . . . 18
1.4.1 Premier programme . . . . . . . . . . . . . . . . . . . . . . . 18
1.4.2 Dépendances du compilateur . . . . . . . . . . . . . . . . . . 22
1.4.3 Assemblage du code . . . . . . . . . . . . . . . . . . . 22
1.4.4 Compilation du code C . . . . . . . . . . . . . . . . . . 23
1.4.5 Liaison des fichiers objets . . . . . . . . . . . . . . . . . 23
1.4.6 Présentation d'un fichier de liste d'assemblys . . . . . . . . . 23
je
Machine Translated by Google
ii CONTENU
2 Langage d'assemblage de base 2.1 27
Travailler avec des nombres entiers . . . . . . . . . . . . . . . . . . . . . . 27
2.1.1 Représentation entière . . . . . . . . . . . . . . . . . . 27
2.1.2 Extension du signe . . . . . . . . . . . . . . . . . . . . . . 30
2.1.3 Arithmétique en complément à deux . . . . . . . . . . . . . 33
2.1.4 Exemple de programme . . . . . . . . . . . . . . . . . . . . 35
2.1.5 Arithmétique à précision étendue . . . . . . . . . . . . . 36
2.2 Ouvrages de contrôle . . . . . . . . . . . . . . . . . . . . . . . . 37
2.2.1 Comparaisons . . . . . . . . . . . . . . . . . . . . . . . 37
2.2.2 Instructions de branche . . . . . . . . . . . . . . . . . . . 38
2.2.3 Les instructions de boucle . . . . . . . . . . . . . . . . . . 41
2.3 Traduction des structures de contrôle standard . . . . . . . . . . . . 42
2.3.1 Si les instructions . . . . . . . . . . . . . . . . . . . . . . . 42
2.3.2 Boucles While . . . . . . . . . . . . . . . . . . . . . . . 43
2.3.3 Faire des boucles tant que . . . . . . . . . . . . . . . . . . . . . . 43
2.4 Exemple : Recherche de nombres premiers . . . . . . . . . . . . . . . 43
Opérations sur 3 bits 47
3.1 Opérations de décalage .. . . . . . . . . . . . . . . . . . . . . . . . 47
3.1.1 Décalages logiques . . . . . . . . . . . . . . . . . . . . . . . 47
3.1.2 Utilisation des équipes . . . . . . . . . . . . . . . . . . . . . . . . 48
3.1.3 Décalages arithmétiques . . . . . . . . . . . . . . . . . . . . . 48
3.1.4 Rotation des équipes . . . . . . . . . . . . . . . . . . . . . . . 49
3.1.5 Application simplifiée . . . . . . . . . . . . . . . . . . . . 49
3.2 Opérations booléennes au niveau des bits . . . . . . . . . . . . . . . . . . . 50
3.2.1 L'opération ET . . . . . . . . . . . . . . . . . . . 50
3.2.2 L'opération OU . . . . . . . . . . . . . . . . . . . . 50
3.2.3 L'opération XOR . . . . . . . . . . . . . . . . . . . 51
3.2.4 L'opération NON . . . . . . . . . . . . . . . . . . . 51
3.2.5 La commande TEST . . . . . . . . . . . . . . . . . . . 51
.
3.2.6 Utilisations des opérations sur les bits . . . . . . . . . . . . . . . . . 52
3.3 Éviter les branchements conditionnels . . . . . . . . . . . . . . . . . 53
3.4 Manipuler des bits en C . . . . . . . . . . . . . . . . . . . . . . 56
3.4.1 Les opérateurs bit à bit de C . . . . . . . . . . . . . . . 56
3.4.2 Utilisation des opérateurs bit à bit en C . . . . . . . . . . . . . . 56
3.5 Représentations Big et Little Endian . . . . . . . . . . . . . 57
3.5.1 Quand se soucier du petit et du gros boutiens . . . . . . 59
3.6 Comptage des bits . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
3.6.1 Première méthode . . . . . . . . . . . . . . . . . . . . . . . . 60
3.6.2 Deuxième méthode . . . . . . . . . . . . . . . . . . . . . . . . 61
3.6.3 Troisième méthode . . . . . . . . . . . . . . . . . . . . . . . 62
Machine Translated by Google
CONTENU iii
4 Sousprogrammes 65
4.1 Adressage indirect . . . . . . . . . . . . . . . . . . . . . . . . 65
4.2 Exemple de sousprogramme simple . . . . . . . . . . . . . . . . . . 66
4.3 La pile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
4.4 Les instructions CALL et RET . . . . . . . . . . . . . . . . 69
4.5 Conventions d'appel . . . . . . . . . . . . . . . . . . . . . . . 70
4.5.1 Passage de paramètres sur la pile . . . . . . . . . . . . 70
4.5.2 Variables locales sur la pile . . . . . . . . . . . . . . . 75
4.6 Programmes multimodules . . . . . . . . . . . . . . . . . . . . . 77
4.7 Interfacer Assembly avec C . . . . . . . . . . . . . . . . . . . 80
4.7.1 Sauvegarde des registres . . . . . . . . . . . . . . . . . . . . . . 81
4.7.2 Libellés des fonctions . . . . . . . . . . . . . . . . . . . . 82
4.7.3 Passer des paramètres . . . . . . . . . . . . . . . . . . . . 82
4.7.4 Calcul des adresses des variables locales . . . . . . . . . 82
4.7.5 Renvoyer des valeurs . . . . . . . . . . . . . . . . . . . . . 83
4.7.6 Autres conventions d'appel . . . . . . . . . . . . . . . . 83
4.7.7 Exemples . . . . . . . . . . . . . . . . . . . . . . . . . 85
4.7.8 Appel de fonctions C depuis l'assembly . . . . . . . . . . . 88
4.8 Sousprogrammes réentrants et récursifs . . . . . . . . . . . . . 89
4.8.1 Sousprogrammes récursifs . . . . . . . . . . . . . . . . . . 89
4.8.2 Examen des types de stockage de variables C . . . . . . . . . . . 91
5 Tableaux 95
5.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
5.1.1 Définition des tableaux . . . . . . . . . . . . . . . . . . . . . . 95
5.1.2 Accéder aux éléments des tableaux . . . . . . . . . . . . . . 96
5.1.3 Adressage indirect plus avancé . . . . . . . . . . . 98
5.1.4 Exemple . . . . . . . . . . . . . . . . . . . . . . . . . . 99
5.1.5 Tableaux multidimensionnels . . . . . . . . . . . . . . . . . 103
5.2 Instructions de tableau/chaîne . . . . . . . . . . . . . . . . . . . . 106
5.2.1 Lecture et écriture de la mémoire . . . . . . . . . . . . . . 106
5.2.2 Le préfixe de l'instruction REP . . . . . . . . . . . . . . . . 108
5.2.3 Instructions de chaîne de comparaison . . . . . . . . . . . . . 109
5.2.4 Les préfixes des instructions REPx . . . . . . . . . . . . . . 109
5.2.5 Exemple . . . . . . . . . . . . . . . . . . . . . . . . . . 111
6 virgule flottante 117
6.1 Représentation en virgule flottante . . . . . . . . . . . . . . . . . . 117
6.1.1 Nombres binaires non entiers . . . . . . . . . . . . . . 117
6.1.2 Représentation en virgule flottante IEEE . . . . . . . . . . . 119
6.2 Arithmétique en virgule flottante . . . . . . . . . . . . . . . . . . . . 122
6.2.1 Ajout . . . . . . . . . . . . . . . . . . . . . . . . . . 122
6.2.2 Soustraction . . . . . . . . . . . . . . . . . . . . . . . . 123
Machine Translated by Google
iv CONTENU
6.2.3 Multiplication et division . . . . . . . . . . . . . . . 123
6.2.4 Ramifications pour la programmation . . . . . . . . . . . . . 124
6.3 Le coprocesseur numérique . . . . . . . . . . . . . . . . . . . . 124
6.3.1 Matériel . . . . . . . . . . . . . . . . . . . . . . . . . 124
6.3.2 Consignes . . . . . . . . . . . . . . . . . . . . . . . . 125
6.3.3 Exemples . . . . . . . . . . . . . . . . . . . . . . . . . 130
6.3.4 Formule quadratique . . . . . . . . . . . . . . . . . . . . 130
6.3.5 Lecture d'un tableau à partir d'un fichier . . . . . . . . . . . . . . . . . 133
6.3.6 Recherche de nombres premiers . . . . . . . . . . . . . . . . . . . . . . 135
7 Structures et C++ 7.1 143
Structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
7.1.1 Présentation . . . . . . . . . . . . . . . . . . . . . . . 143
7.1.2 Alignement de la mémoire . . . . . . . . . . . . . . . . . . . . 145
7.1.3 Champs de bits . . . . . . . . . . . . . . . . . . . . . . . . . 146
7.1.4 Utilisation de structures en assemblage . . . . . . . . . . . . . . 150
7.2 Assembleur et C++ . . . . . . . . . . . . . . . . . . . . . . . 150
7.2.1 Surcharge et manipulation de noms . . . . . . . . . . . . 151
7.2.2 Références . . . . . . . . . . . . . . . . . . . . . . . . . 153
7.2.3 Fonctions en ligne . . . . . . . . . . . . . . . . . . . . . . 154
7.2.4 Cours . . . . . . . . . . . . . . . . . . . . . . . . . . 156
7.2.5 Héritage et polymorphisme . . . . . . . . . . . . . 166
7.2.6 Autres fonctionnalités C++ . . . . . . . . . . . . . . . . . . . 171
Un mode d'emploi 80x86 173
A.1 Instructions en virgule non flottante . . . . . . . . . . . . . . . . . 173
A.2 Instructions en virgule flottante . . . . . . . . . . . . . . . . . . . 179
Indice 181
Machine Translated by Google
Préface
But
Le but de ce livre est de donner au lecteur une meilleure compréhension du fonctionnement
réel des ordinateurs à un niveau inférieur à celui des langages de programmation comme Pascal.
En acquérant une compréhension plus approfondie du fonctionnement des ordinateurs, le lecteur
peut souvent être beaucoup plus productif en développant des logiciels dans des langages de
niveau supérieur tels que C et C++. Apprendre à programmer en langage assembleur est un
excellent moyen d'atteindre cet objectif. D'autres livres en langage d'assemblage pour PC enseignent
encore comment programmer le processeur 8086 que le PC d'origine utilisait en 1981 ! Le
processeur 8086 ne supportait que le mode réel. Dans ce mode, n'importe quel programme peut
adresser n'importe quelle mémoire ou périphérique de l'ordinateur. Ce mode n'est pas adapté à un
système d'exploitation sécurisé et multitâche. Ce livre explique plutôt comment programmer les
processeurs 80386 et ultérieurs en mode protégé (le mode dans lequel Windows et Linux
s'exécutent). Ce mode prend en charge les fonctionnalités attendues par les systèmes d'exploitation
modernes, telles que la mémoire virtuelle et la protection de la mémoire. Il existe plusieurs raisons
d'utiliser le mode protégé :
1. Il est plus facile de programmer en mode protégé qu'en mode réel 8086 utilisé par d'autres
livres.
2. Tous les systèmes d'exploitation PC modernes fonctionnent en mode protégé.
3. Il existe un logiciel gratuit qui fonctionne dans ce mode.
Le manque de manuels pour la programmation d'assemblage de PC en mode protégé est la
principale raison pour laquelle l'auteur a écrit ce livre.
Comme évoqué cidessus, ce texte utilise des logiciels libres/open source, à savoir l'assembleur
NASM et le compilateur DJGPP C/C++. Les deux sont disponibles en téléchargement sur Internet.
Le texte explique également comment utiliser le code assembleur NASM sous le système
d'exploitation Linux et avec les compilateurs C/C++ de Borland et Microsoft sous Windows. Des
exemples pour toutes ces plateformes peuvent être trouvés sur mon site Web : http://
www.drpaulcarter.com/pcasm. Vous devez télécharger l'exemple de code si vous souhaitez
assembler et exécuter de nombreux exemples de ce didacticiel.
v
Machine Translated by Google
vi PRÉFACE
Sachez que ce texte ne tente pas de couvrir tous les aspects de la programmation en
assembleur. L'auteur a essayé de couvrir les sujets les plus importants que tous les
programmeurs devraient connaître.
Remerciements
L'auteur tient à remercier les nombreux programmeurs du monde entier qui ont contribué
au mouvement Free/Open Source. Tous les programmes et même ce livre luimême ont été
produits à l'aide de logiciels libres. Plus précisément, l'auteur tient à remercier John S. Fine,
Simon Tatham, Julian Hall et d'autres pour avoir développé l'assembleur NASM sur lequel
tous les exemples de ce livre sont basés ; DJ Delorie pour le développement du compilateur
DJGPP C/C++ utilisé ; les nombreuses personnes qui ont contribué au compilateur GNU gcc
sur lequel DJGPP est basé ; Donald Knuth et d'autres pour avoir développé les langages de
composition TEX et LATEX 2ε qui ont été utilisés pour produire le livre ; Richard Stallman
(fondateur de la Free Software Foundation), Linus Torvalds (créateur du noyau Linux) et
d'autres qui ont produit le logiciel sousjacent utilisé par l'auteur pour produire ce travail.
Merci aux personnes suivantes pour les corrections :
• John S. Fine
• Marcelo Henrique Pinto de Almeida
• Sam Hopkins
• Nick D'Imperio
• Jérémie Laurent
• Ed Beroset
• Jerry Gembarowski
• Ziqiang Peng
• Eno Compton
• Josh I Cates
• Mik Mifflin
• Luc Wallis
• Gaku Ueda
• Brian Heward
Machine Translated by Google
vii
• Tchad Gorshing
• F. Gotti
• Bob Wilkinson
• Markus Koegel
• Louis Taber
• Dave Kiddell
• Edouard Horowitz
• Sébastien Le Ray
• Nehal Mistry
• Jianyue Wang
• Jérémie Kleer
• Marc Janicki
Ressources sur Internet
Page de l'auteur http://www.drpaulcarter.com/ Page NASM
SourceForge http://sourceforge.net/projects/nasm/ DJGPP http://
www.delorie.com/djgpp Linux Assembly http://
www.linuxassembly.org / L'art de l'assemblage http://
webster.cs.ucr.edu/ USENET comp.lang.asm.x86
Documentation Intel http://developer.intel.com/
design/Pentium4/documentation.htm
Retour
L'auteur accueille tout commentaire sur ce travail.
Courriel : [email protected]
Site Web : http://www.drpaulcarter.com/pcasm
Machine Translated by Google
viii PRÉFACE
Machine Translated by Google
Chapitre 1
Introduction
1.1 Systèmes de numération
La mémoire d'un ordinateur est constituée de nombres. La mémoire de l'ordinateur
ne stocke pas ces nombres en décimal (base 10). Parce que cela simplifie grandement
le matériel, les ordinateurs stockent toutes les informations dans un format binaire (base 2).
Revoyons d'abord le système décimal.
1.1.1 Décimal
Les nombres en base 10 sont composés de 10 chiffres possibles (09). Chaque
chiffre d'un nombre est associé à une puissance de 10 en fonction de sa position dans le
nombre. Par exemple:
234 = 2 × 102 + 3 × 101 + 4 × 100
1.1.2 Binaire
Les nombres en base 2 sont composés de 2 chiffres possibles (0 et 1). Chaque
chiffre d'un nombre est associé à une puissance de 2 en fonction de sa position dans le
nombre. (Un seul chiffre binaire est appelé un bit.) Par exemple :
4 3 2 1 0 110012 = 1 × 2 + 1 × 2 + 0 × 2 +
0 × 2 + 1 × 2
= 16 + 8 + 1
= 25
Cela montre comment le binaire peut être converti en décimal. Le tableau 1.1 montre
comment les premiers nombres sont représentés en binaire.
La figure 1.1 montre comment les chiffres binaires individuels (c'estàdire les bits) sont ajoutés.
Voici un exemple :
1
Machine Translated by Google
2 CHAPITRE 1 INTRODUCTION
Décimal Binaire 0 Décimal Binaire
0000 8 1000
1 0001 9 1001
2 0010 dix 1010
3 0011 11 1011
4 0100 12 1100
5 0101 13 1101
6 0110 14 1110
7 0111 15 1111
Tableau 1.1 : Décimal 0 à 15 en binaire
Pas de report précédent Portage précédent
0 0 1 +0 +1 0 1 1 0 0 +0 +1 1 1
+0 +1 1 0 1 0 +0 +1
0 1
c c c c
Figure 1.1 : Addition binaire (c signifie retenue)
110112
+100012
1011002
Si l'on considère la division décimale suivante :
1234 ÷ 10 = 123 r 4
il peut voir que cette division enlève le chiffre décimal le plus à droite du
nombre et décale les autres chiffres décimaux d'une position vers la droite. Partage
par deux effectue une opération similaire, mais pour les chiffres binaires du nombre.
Considérons la division binaire suivante1 :
11012 ÷ 102 = 1102 r 1
Ce fait peut être utilisé pour convertir un nombre décimal en son équivalent binaire
représentation comme le montre la figure 1.2. Cette méthode trouve le chiffre le plus à droite
d'abord, ce chiffre est appelé le bit le moins significatif (lsb). Le chiffre le plus à gauche est
appelé le bit le plus significatif (msb). L'unité de base de la mémoire est constituée de
8 bits et s'appelle un octet.
1L'indice 2 est utilisé pour montrer que le nombre est représenté en binaire et non en décimal
Machine Translated by Google
1.1. SYSTÈMES DE NOMBRE 3
Décimal Binaire
25 ÷ 2 = 12 r 1 11001 ÷ 10 = 1100 r 1
12 ÷ 2 = 6 r 0 1100 ÷ 10 = 110 r 0
6 ÷ 2 = 3 r 0 110 ÷ 10 = 11 r 0
3 ÷ 2 = 1 r 1 11 ÷ 10 = 1 r 1
1 ÷ 2 = 0 r 1 1 ÷ 10 = 0 r 1
Ainsi 2510 = 110012
Figure 1.2 : Conversion décimale
589 ÷ 16 = 36 r 13
36 ÷ 16 = 2 r 4
2 ÷ 16 = 0 r 2
Donc 589 = 24D16
Figure 1.3 :
1.1.3 Hexadécimal
Les nombres hexadécimaux utilisent la base 16. L'hexadécimal (ou hexadécimal en
abrégé) peut être utilisé comme raccourci pour les nombres binaires. Hex a 16 chiffres
possibles. Cela crée un problème car il n'y a pas de symboles à utiliser pour ces chiffres
supplémentaires après 9. Par convention, des lettres sont utilisées pour ces chiffres
supplémentaires. Les 16 chiffres hexadécimaux sont 09 puis A, B, C, D, E et F. Le chiffre
A est équivalent à 10 en décimal, B est 11, etc. Chaque chiffre d'un nombre hexadécimal
a une puissance de 16 associée à il. Exemple:
2BD16 = 2 × 162 + 11 × 161 + 13 × 160
= 512 + 176 + 13
= 701
Pour convertir de décimal en hexadécimal, utilisez la même idée que celle utilisée pour la
conversion binaire, sauf diviser par 16. Voir la figure 1.3 pour un exemple.
La raison pour laquelle l'hexagone est utile est qu'il existe un moyen très simple de convertir
Machine Translated by Google
4 CHAPITRE 1 INTRODUCTION
entre hexadécimal et binaire. Les nombres binaires deviennent rapidement volumineux et encombrants.
Hex fournit une manière beaucoup plus compacte de représenter le binaire.
Pour convertir un nombre hexadécimal en binaire, convertissez simplement chaque chiffre hexadécimal en un
Nombre binaire 4 bits. Par exemple, 24D16 est converti en 0010 0100 11012.
Notez que les zéros non significatifs des 4 bits sont importants ! Si le zéro non significatif
car le chiffre du milieu de 24D16 n'est pas utilisé, le résultat est faux. Conversion
du binaire à l'hexadécimal est tout aussi simple. On fait le processus en sens inverse. Convertir
chaque segment de 4 bits du binaire en hexadécimal. Commencez par le bon bout, pas par le
extrémité gauche du nombre binaire. Cela garantit que le processus utilise le bon
segments de 4 bits2 . Exemple:
110 0000 0101 1010 0111 11102
6 0 5 A 7 E16
Un nombre de 4 bits est appelé un quartet. Ainsi, chaque chiffre hexadécimal correspond à
une bouchée. Deux quartets font un octet et donc un octet peut être représenté par un
Numéro hexadécimal à 2 chiffres. La valeur d'un octet est comprise entre 0 et 11111111 en binaire, 0
à FF en hexadécimal et 0 à 255 en décimal.
1.2 Organisation informatique
1.2.1 Mémoire
La mémoire est mesurée en L'unité de base de la mémoire est un octet. Un ordinateur avec 32 mégaoctets
unités de kilooctets ( 2 dix = de mémoire peut contenir environ 32 millions d'octets d'informations. Chaque octet dans
1 024 octets), mégaoctets
la mémoire est étiquetée par un numéro unique connu sous le nom d'adresse comme Figure 1.4
( 2 20 = 1 048 576 octets) montre.
et gigaoctets ( 2 30 =
Figure 1.4 : Adresses mémoire
Souvent, la mémoire est utilisée dans des blocs plus grands que des octets uniques. Sur l'ordinateur
architecture, des noms ont été donnés à ces grandes sections de mémoire comme
Le tableau 1.2 montre.
Toutes les données en mémoire sont numériques. Les caractères sont stockés à l'aide
d'un code de caractère qui associe des nombres à des caractères. L'un des plus courants
Les codes de caractères sont appelés ASCII (American Standard Code for Information
Interchange). Un nouveau code plus complet qui remplace l'ASCII
est Unicode. Une différence essentielle entre les deux codes est que l'ASCII utilise
2
Si vous ne savez pas pourquoi le point de départ fait une différence, essayez de convertir l'exemple
commençant par la gauche.
Machine Translated by Google
1.2. ORGANISATION INFORMATIQUE 5
mot 2 octets mot double 4
octets mot quadruple 8
octets paragraphe 16 octets
Tableau 1.2 : Unités de mémoire
un octet pour coder un caractère, mais Unicode utilise deux octets (ou un mot) par caractère.
Par exemple, ASCII mappe l'octet 4116 (6510) au caractère majuscule A ; Unicode mappe le
mot 004116. Puisque l'ASCII utilise un octet, il est limité à seulement 256 caractères
différents3 . Unicode étend les valeurs ASCII aux mots et permet de représenter beaucoup
plus de caractères. Ceci est important pour représenter les caractères de toutes les langues
du monde.
1.2.2 Le processeur
L'unité centrale de traitement (CPU) est le dispositif physique qui exécute les instructions.
Les instructions exécutées par les processeurs sont généralement très simples.
Les instructions peuvent nécessiter que les données sur lesquelles elles agissent se trouvent dans des
emplacements de stockage spéciaux dans le processeur luimême, appelés registres. Le CPU peut accéder
aux données dans les registres beaucoup plus rapidement que les données en mémoire. Cependant, le
nombre de registres dans un processeur est limité, le programmeur doit donc veiller à ne conserver que les
données actuellement utilisées dans les registres.
Les instructions exécutées par un type de CPU constituent le langage machine du CPU.
Les programmes machine ont une structure beaucoup plus basique que les langages de
niveau supérieur. Les instructions en langage machine sont codées sous forme de nombres
bruts, et non dans des formats de texte conviviaux. Un processeur doit être capable de
décoder très rapidement le but d'une instruction pour fonctionner efficacement. Le langage
machine est conçu avec cet objectif à l'esprit, pour ne pas être facilement déchiffré par les
humains. Les programmes écrits dans d'autres langages doivent être convertis dans le
langage machine natif du processeur pour s'exécuter sur l'ordinateur. Un compilateur est un
programme qui traduit des programmes écrits dans un langage de programmation dans le
langage machine d'une architecture informatique particulière. En général, chaque type de
CPU a son propre langage machine unique. C'est une des raisons pour lesquelles les
programmes écrits pour un Mac ne peuvent pas fonctionner sur un PC de type IBM.
Les ordinateurs utilisent une horloge pour synchroniser l'exécution des instructions. GHz signifie gigahertz. L'horloge
émet des impulsions à une fréquence fixe (appelée vitesse d'horloge). Lorsque vous ou un milliard de cycles par seconde. Un
processeur 1,5 GHz achète
un ordinateur 1,5 GHz, 1,5 GHz est la fréquence de cette horloge4 . L'horloge a 1,5 milliard
d'impulsions d'horloge ne tient
pas compte des minutes et des secondes. Il bat simplement à une constante par seconde.
3
En fait, ASCII n'utilise que les 7 bits inférieurs et n'a donc que 128 valeurs différentes à utiliser.
4En fait, les impulsions d'horloge sont utilisées dans de nombreux composants différents d'un ordinateur. Le
d'autres composants utilisent souvent des vitesses d'horloge différentes de celles du processeur.
Machine Translated by Google
6 CHAPITRE 1 INTRODUCTION
taux. L'électronique du processeur utilise les battements pour effectuer correctement ses
opérations, comme la façon dont les battements d'un métronome aident à jouer de la musique au
bon rythme. Le nombre de battements (ou comme on les appelle généralement cycles) requis par
une instruction dépend de la génération et du modèle du processeur. Le nombre de cycles dépend
des instructions qui précèdent et d'autres facteurs également.
1.2.3 La famille de processeurs 80x86
Les PC de type IBM contiennent un processeur de la famille 80x86 d'Intel (ou un clone de
celuici). Les processeurs de cette famille ont tous des caractéristiques communes, notamment un
langage machine de base. Cependant, les membres les plus récents améliorent considérablement
les fonctionnalités.
8088,8086 : Ces CPU du point de vue de la programmation sont identiques.
C'étaient les processeurs utilisés dans les premiers PC. Ils fournissent plusieurs registres
16 bits : AX, BX, CX, DX, SI, DI, BP, SP, CS, DS, SS, ES, IP, FLAGS. Ils ne prennent en
charge que jusqu'à un mégaoctet de mémoire et ne fonctionnent qu'en mode réel. Dans ce
mode, un programme peut accéder à n'importe quelle adresse mémoire, même la mémoire
d'autres programmes ! Cela rend le débogage et la sécurité très difficiles ! De plus, la
mémoire programme doit être divisée en segments. Chaque segment ne peut pas dépasser
64 Ko.
80286 : Ce processeur était utilisé dans les PC de classe AT. Il ajoute de nouvelles instructions
au langage machine de base du 8088/86. Cependant, sa principale nouveauté est le mode
protégé 16 bits. Dans ce mode, il peut accéder jusqu'à 16 mégaoctets et empêcher les
programmes d'accéder à la mémoire de l'autre.
Cependant, les programmes sont toujours divisés en segments qui ne peuvent pas
dépasser 64 Ko.
80386 : Ce processeur a grandement amélioré le 80286. Tout d'abord, il étend de nombreux
registres pour contenir 32 bits (EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP, EIP) et ajoute
deux nouveaux registres 16 bits FS et GS. Il ajoute également un nouveau mode protégé
32 bits. Dans ce mode, il peut accéder jusqu'à 4 gigaoctets. Les programmes sont à
nouveau divisés en segments, mais maintenant chaque segment peut également avoir une
taille allant jusqu'à 4 gigaoctets !
80486/Pentium/Pentium Pro : Ces membres de la famille 80x86 ajoutent très peu de nouvelles
fonctionnalités. Ils accélèrent principalement l'exécution des instructions.
Pentium MMX : Ce processeur ajoute les instructions MMX (MultiMedia eXtensions) au Pentium.
Ces instructions peuvent accélérer les opérations graphiques courantes.
Machine Translated by Google
1.2. ORGANISATION INFORMATIQUE 7
HACHE
AH AL
Figure 1.5 : Le registre AX
Pentium II : Il s'agit du processeur Pentium Pro avec les instructions MMX ajoutées. (Le
Pentium III est essentiellement juste un Pentium II plus rapide.)
1.2.4 Registres 8086 16 bits
Le processeur 8086 d'origine fournissait quatre registres à usage général de 16
bits : AX, BX, CX et DX. Chacun de ces registres pourrait être décomposé en deux
registres de 8 bits. Par exemple, le registre AX pourrait être décomposé en registres AH
et AL comme le montre la figure 1.5. Le registre AH contient les 8 bits supérieurs (ou
supérieurs) de AX et AL contient les 8 bits inférieurs de AX.
Souvent, AH et AL sont utilisés comme registres indépendants d'un octet; cependant, il
est important de réaliser qu'ils ne sont pas indépendants d'AX. Changer la valeur de AX
changera AH et AL et vice versa. Les registres à usage général sont utilisés dans de
nombreuses instructions de déplacement de données et d'arithmétique.
Il existe deux registres d'index 16 bits : SI et DI. Ils sont souvent utilisés comme
pointeurs, mais peuvent être utilisés pour la plupart des mêmes objectifs que les
registres généraux. Cependant, ils ne peuvent pas être décomposés en registres de 8 bits.
Les registres BP et SP 16 bits sont utilisés pour pointer vers des données dans la
pile du langage machine et sont appelés respectivement le pointeur de base et le
pointeur de pile. Ceuxci seront discutés plus tard.
Les registres CS, DS, SS et ES 16 bits sont des registres de segment. Ils indiquent
quelle mémoire est utilisée pour les différentes parties d'un programme. CS signifie
Code Segment, DS pour Data Segment, SS pour Stack Segment et ES pour Extra
Segment. ES est utilisé comme registre de segment temporaire. Le détail de ces
registres se trouve dans les sections 1.2.6 et 1.2.7.
Le registre Instruction Pointer (IP) est utilisé avec le registre CS pour garder une
trace de l'adresse de la prochaine instruction à exécuter par la CPU. Normalement,
lorsqu'une instruction est exécutée, IP est avancé pour pointer vers l'instruction suivante
en mémoire.
Le registre FLAGS stocke des informations importantes sur les résultats d'une
instruction précédente. Ces résultats sont stockés sous forme de bits individuels dans
le registre. Par exemple, le bit Z vaut 1 si le résultat de l'instruction précédente était zéro
ou 0 sinon zéro. Toutes les instructions ne modifient pas les bits dans FLAGS, consultez
le tableau en annexe pour voir comment les instructions individuelles affectent le registre
FLAGS.
Machine Translated by Google
8 CHAPITRE 1 INTRODUCTION
1.2.5 80386 registres 32 bits
Les processeurs 80386 et ultérieurs ont des registres étendus. Par exemple, le registre AX 16 bits
est étendu à 32 bits. Pour être rétrocompatible, AX fait toujours référence au registre 16 bits et EAX est
utilisé pour faire référence au registre 32 bits étendu. AX correspond aux 16 bits inférieurs d'EAX, tout
comme AL correspond aux 8 bits inférieurs d'AX (et d'EAX). Il n'y a aucun moyen d'accéder directement
aux 16 bits supérieurs d'EAX. Les autres registres étendus sont EBX, ECX, EDX, ESI et EDI.
De nombreux autres registres sont également étendus. BP devient EBP ; SP devient ESP ; FLAGS
devient EFLAGS et IP devient EIP. Cependant, contrairement aux registres d'index et à usage général,
en mode protégé 32 bits (voir cidessous), seules les versions étendues de ces registres sont utilisées.
Les registres de segment sont toujours de 16 bits dans le 80386. Il existe également deux nouveaux
registres de segment : FS et GS. Leurs noms ne signifient rien.
Ce sont des registres de segments temporaires supplémentaires (comme ES).
L'une des définitions du terme mot fait référence à la taille des registres de données de la CPU. Pour
la famille 80x86, le terme est désormais un peu déroutant. Dans le tableau 1.2, on voit que le mot est
défini comme étant de 2 octets (ou 16 bits). On lui a donné cette signification lors de la première sortie du
8086. Lorsque le 80386 a été développé, il a été décidé de laisser la définition du mot inchangée, même
si la taille du registre a changé.
1.2.6 Mode réel
l'infâme adresse limite du DOS En mode réel, la mémoire est limitée à un seul mégaoctet (220 octets). Valide Alors, d'où vient
640K allant de (en hexadécimal) 00000 à FFFFF. Ces adresses nécessitent un 20 venir? Le numéro de bit du BIOS. De toute évidence, un
nombre de 20 bits ne rentrera dans aucun des 8086 requis par certains des registres 1M 16 bits. Intel a résolu ce problème en utilisant deux
valeurs 16 bits pour son code et
pour déterminer une adresse. La première valeur de 16 bits est appelée le sélecteur. Les dispositifs de
sélection d'articles tels que les
valeurs vidéo doivent être stockés dans des registres de segments. La deuxième valeur de 16 bits est appelée
le décalage. L'adresse physique
référencée par une paire sélecteur:décalage 32 bits est calculée par la formule
filtrer.
Sélecteur 16 + décalage
Multiplier par 16 en hexadécimal est facile, il suffit d'ajouter un 0 à droite du nombre.
Par exemple, l'adresse physique référencée par 047C:0048 est donnée par :
047C0
+0048
04808
En effet, la valeur du sélecteur est un numéro de paragraphe (voir Tableau 1.2).
Les adresses segmentées réelles présentent des inconvénients :
Machine Translated by Google
1.2. ORGANISATION INFORMATIQUE 9
• Une seule valeur de sélecteur ne peut référencer que 64 Ko de mémoire (la limite
supérieure du décalage de 16 bits). Que se passetil si un programme contient plus de
64 Ko de code ? Une seule valeur dans CS ne peut pas être utilisée pour toute
l'exécution du programme. Le programme doit être divisé en sections (appelées
segments) d'une taille inférieure à 64 Ko. Lorsque l'exécution passe d'un segment à un
autre, la valeur de CS doit être modifiée. Des problèmes similaires se produisent avec
de grandes quantités de données et le registre DS. Cela peut être très gênant !
• Chaque octet en mémoire n'a pas d'adresse segmentée unique. L'adresse physique
04808 peut être référencée par 047C:0048, 047D:0038, 047E:0028 ou 047B:0058. Cela
peut compliquer la comparaison des adresses segmentées.
1.2.7 Mode protégé 16 bits
Dans le mode protégé 16 bits du 80286, les valeurs du sélecteur sont interprétées
complètement différemment qu'en mode réel. En mode réel, une valeur de sélecteur est un
numéro de paragraphe de mémoire physique. En mode protégé, une valeur de sélecteur est
un index dans une table de descripteurs. Dans les deux modes, les programmes sont divisés
en segments. En mode réel, ces segments sont à des positions fixes dans la mémoire physique
et la valeur du sélecteur indique le numéro de paragraphe du début du segment. En mode
protégé, les segments ne sont pas à des positions fixes dans la mémoire physique. En fait, ils
ne doivent pas du tout être en mémoire !
Le mode protégé utilise une technique appelée mémoire virtuelle. L'idée de base d'un
système de mémoire virtuelle est de ne conserver que les données et le code en mémoire que
les programmes utilisent actuellement. Les autres données et codes sont stockés
temporairement sur le disque jusqu'à ce qu'ils soient à nouveau nécessaires. En mode protégé
16 bits, les segments sont déplacés entre la mémoire et le disque selon les besoins. Lorsqu'un
segment est remis en mémoire à partir du disque, il est très probable qu'il soit placé dans une
zone de mémoire différente de celle dans laquelle il se trouvait avant d'être déplacé vers le
disque. Tout cela est fait de manière transparente par le système d'exploitation. Le programme
n'a pas besoin d'être écrit différemment pour que la mémoire virtuelle fonctionne.
En mode protégé, chaque segment se voit attribuer une entrée dans une table de
descripteurs. Cette entrée contient toutes les informations que le système doit connaître sur le
segment. Ces informations incluent : sontelles actuellement en mémoire ; si en mémoire, où
estil ; autorisations d'accès (par exemple, en lecture seule). L'indice de l'entrée du segment
est la valeur du sélecteur qui est stockée dans les registres de segment.
Un gros inconvénient du mode protégé 16 bits est que les décalages sont toujours Un chroniqueur PC bien connu a qualifié
quantités 16 bits. En conséquence, les tailles de segment sont toujours limitées à 64 Ko au le processeur 286 de "mort
cérébrale".
maximum. Cela rend problématique l'utilisation de grands tableaux !
Machine Translated by Google
dix CHAPITRE 1 INTRODUCTION
1.2.8 Mode protégé 32 bits
Le 80386 a introduit le mode protégé 32 bits. Il y a deux différences majeures
férences entre 386 modes protégés 32 bits et 286 modes protégés 16 bits :
1. Les décalages sont étendus à 32 bits. Cela permet à un décalage d'aller jusqu'à 4 milliards. Ainsi,
les segments peuvent avoir des tailles allant jusqu'à 4 gigaoctets.
2. Les segments peuvent être divisés en unités plus petites de 4K appelées pages. Le système de
mémoire virtuelle fonctionne désormais avec des pages au lieu de segments.
Cela signifie que seules des parties de segment peuvent être en mémoire à un moment donné.
En mode 286 16 bits, soit le segment entier est en mémoire, soit aucun n'y est. Ce n'est pas
pratique avec les segments plus grands autorisés par le mode 32 bits.
Dans Windows 3.x, le mode standard fait référence au mode protégé 286 16 bits et le mode amélioré
fait référence au mode 32 bits. Windows 9X, Windows NT/2000/XP, OS/2 et Linux fonctionnent tous en mode
protégé paginé 32 bits.
1.2.9 Interruptions
Parfois, le déroulement normal d'un programme doit être interrompu pour traiter des événements qui
nécessitent une réponse rapide. Le matériel d'un ordinateur fournit un mécanisme appelé interruptions
pour gérer ces événements. Par exemple, lorsqu'une souris est déplacée, le matériel de la souris
interrompt le programme en cours pour gérer le mouvement de la souris (pour déplacer le curseur de la
souris, etc.). Les interruptions entraînent le passage du contrôle à un gestionnaire d'interruptions. Les
gestionnaires d'interruption sont des routines qui traitent l'interruption. Chaque type d'interruption se voit
attribuer un nombre entier. Au début de la mémoire physique, réside une table de vecteurs d'interruption
qui contient les adresses segmentées des gestionnaires d'interruption. Le nombre d'interruptions est
essentiellement un index dans cette table.
Les interruptions externes sont déclenchées depuis l'extérieur du CPU. (La souris est un exemple
de ce type.) De nombreux périphériques d'E/S déclenchent des interruptions (par exemple, clavier,
minuterie, lecteurs de disque, CDROM et cartes son). Les interruptions internes sont déclenchées à
partir de l'UC, soit à partir d'une erreur, soit de l'instruction d'interruption. Les interruptions d'erreur sont
également appelées interruptions. Les interruptions générées à partir de l'instruction d'interruption sont
appelées interruptions logicielles. DOS utilise ces types d'interruptions pour implémenter son API
(Application Programming Interface). Les systèmes d'exploitation plus modernes (tels que Windows et
5
UNIX) utilisent une interface basée sur C.
De nombreux gestionnaires d'interruptions rendent le contrôle au programme interrompu lorsqu'ils
ont terminé. Ils restaurent tous les registres aux mêmes valeurs qu'ils avaient avant l'interruption. Ainsi, le
programme interrompu s'exécute comme si de rien n'était (sauf qu'il a perdu quelques cycles CPU). Les
pièges ne reviennent généralement pas. Souvent, ils interrompent le programme.
5Cependant, ils peuvent utiliser une interface de niveau inférieur au niveau du noyau.
Machine Translated by Google
1.3. LANGAGE D'ASSEMBLAGE 11
1.3 Langage d'assemblage
1.3.1 Langage machine
Chaque type de processeur comprend son propre langage machine. Les instructions en
langage machine sont des nombres stockés sous forme d'octets en mémoire. Chaque
instruction a son propre code numérique unique appelé son code d'opération ou opcode en
abrégé. Les instructions du processeur 80x86 varient en taille. L'opcode est toujours au début
de l'instruction. De nombreuses instructions incluent également des données (par exemple,
des constantes ou des adresses) utilisées par l'instruction.
Le langage machine est très difficile à programmer directement. Déchiffrer la signification
des instructions codées numériquement est fastidieux pour l'homme.
Par exemple, l'instruction qui dit d'additionner les registres EAX et EBX et de stocker le résultat
dans EAX est codée par les codes hexadécimaux suivants :
03 C3
Ce n'est guère évident. Heureusement, un programme appelé assembleur peut effectuer ce
travail fastidieux pour le programmeur.
1.3.2 Langage d'assemblage
Un programme en langage assembleur est stocké sous forme de texte (tout comme un
programme en langage de niveau supérieur). Chaque instruction d'assemblage représente
exactement une instruction machine. Par exemple, l'instruction d'addition décrite cidessus
serait représentée en langage assembleur comme suit :
ajouter eax, ebx
Ici, la signification de l'instruction est beaucoup plus claire que dans le code machine.
Le mot add est un mnémonique pour l'instruction d'addition. La forme générale d'une instruction
d'assemblage est la suivante :
opérande(s) mnémonique(s)
Un assembleur est un programme qui lit un fichier texte contenant des instructions
d'assemblage et convertit l'assemblage en code machine. Les compilateurs sont des
programmes qui effectuent des conversions similaires pour les langages de programmation
de haut niveau. Un assembleur est beaucoup plus simple qu'un compilateur. Chaque instruction en langage assembleur Il a
informaticiens pour que fig représente directement une seule instruction machine. Les états fallu plusieurs années aux
de langage de haut niveau sur la
façon d'écrire sont beaucoup plus complexes et peuvent nécessiter de nombreuses instructions
machine. un compilateur !
Une autre différence importante entre les langages d'assemblage et de haut niveau est
que, puisque chaque type de CPU a son propre langage machine, il a également son propre
langage d'assemblage. Portage des programmes d'assemblage entre
Machine Translated by Google
12 CHAPITRE 1 INTRODUCTION
différentes architectures informatiques est beaucoup plus difficile que dans un langage de haut niveau.
Les exemples de ce livre utilisent le Netwide Assembler ou NASM en abrégé. Il est disponible
gratuitement sur Internet (voir la préface pour l'URL). Les assembleurs les plus courants sont Microsoft's
Assembler (MASM) ou Borland's Assembler (TASM). Il existe quelques différences dans la syntaxe
d'assemblage pour MASM/TASM et NASM.
1.3.3 Opérandes d'instruction
Les instructions de code machine ont un nombre et un type d'opérandes variables; cependant, en
général, chaque instruction ellemême aura un nombre fixe d'opérandes (0 à 3). Les opérandes
peuvent avoir les types suivants :
registre : ces opérandes font directement référence au contenu des registres du processeur.
ters.
mémoire : elles font référence aux données en mémoire. L'adresse des données peut être une constante
codée en dur dans l'instruction ou peut être calculée à l'aide de valeurs de registres. Les
adresses sont toujours des décalages depuis le début d'un segment.
immédiat : il s'agit de valeurs fixes répertoriées dans l'instruction ellemême.
Ils sont stockés dans l'instruction ellemême (dans le segment de code), pas dans le segment
de données.
implicite : ces opérandes ne sont pas explicitement affichés. Par exemple, l'instruction d'incrémentation
ajoute un à un registre ou à une mémoire. Celui est sousentendu.
1.3.4 Instructions de base
L'instruction la plus élémentaire est l'instruction MOV. Il déplace les données d'un
emplacement à un autre (comme l'opérateur d'affectation dans un langage de haut niveau).
Il prend deux opérandes :
mov destination, src
Les données spécifiées par src sont copiées dans dest. Une restriction est que les deux opérandes ne
peuvent pas être des opérandes de mémoire. Cela souligne une autre bizarrerie de l'assemblage. Il
existe souvent des règles quelque peu arbitraires sur la façon dont les diverses instructions sont
utilisées. Les opérandes doivent également avoir la même taille. La valeur de AX ne peut pas être
stockée dans BL.
Voici un exemple (les pointsvirgules commencent un commentaire) :
Machine Translated by Google
1.3. LANGAGE D'ASSEMBLAGE 13
L'instruction ADD est utilisée pour additionner des nombres entiers.
ajouter eax, 4 ; eax = eax + 4
ajouter al, ah ; al = al + ah
L'instruction SUB soustrait des nombres entiers.
sous bx, 10 ; bx = bx 10
sous ebx, édi ; ebx = ebx edi
Les instructions INC et DEC incrémentent ou décrémentent les valeurs d'une unité.
Puisque l'un est un opérande implicite, le code machine pour INC et DEC est
plus petit que pour les instructions ADD et SUB équivalentes.
1.3.5 Directives
Une directive est un artefact de l'assembleur et non du CPU. Ils sont généralement utilisés
soit pour demander à l'assembleur de faire quelque chose, soit pour informer le
assembleur de quelque chose. Ils ne sont pas traduits en code machine. Les utilisations
courantes des directives sont :
• définir des constantes
• définir la mémoire pour stocker les données dans
• regrouper la mémoire en segments
• inclure conditionnellement le code source
• inclure d'autres fichiers
Le code NASM passe par un préprocesseur comme C. Il a beaucoup de
les mêmes commandes de préprocesseur que C. Cependant, les directives de préprocesseur
de NASM commencent par un % au lieu d'un # comme en C.
La directive équi
La directive equ peut être utilisée pour définir un symbole. Les symboles sont nommés
constantes utilisables dans le programme assembleur. Le format est :
symbole valeur équi
Les valeurs de symbole ne peuvent pas être redéfinies ultérieurement.
Machine Translated by Google
14 CHAPITRE 1 INTRODUCTION
Mot Lettre
B
d'octet unitaire W
double mot D
mot quadruple Q
de dix octets J
Tableau 1.3 : Lettres pour les directives RESX et DX
La directive %define
Cette directive est similaire à la directive #define de C. C'est le plus souvent
utilisé pour définir des macros constantes comme en C.
%définir TAILLE 100
mouvement eax, TAILLE
Le code cidessus définit une macro nommée SIZE et montre son utilisation dans un MOV
instruction. Les macros sont plus flexibles que les symboles de deux manières. Macros
peuvent être redéfinis et peuvent être plus que de simples nombres constants.
Directives sur les données
Les directives de données sont utilisées dans les segments de données pour définir l'espace mémoire.
Il existe deux manières de réserver de la mémoire. La première façon définit seulement
espace pour les données ; la deuxième façon définit la pièce et une valeur initiale. La première
utilise l'une des directives RESX. Le X est remplacé par une lettre qui
détermine la taille de l'objet (ou des objets) qui sera stocké. Tableau 1.3
affiche les valeurs possibles.
La deuxième méthode (qui définit également une valeur initiale) utilise l'un des
Directives DX. Les lettres X sont les mêmes que celles des directives RESX.
Il est très courant de marquer les emplacements de mémoire avec des étiquettes. Les étiquettes permettent
un pour se référer facilement aux emplacements de mémoire dans le code. Cidessous plusieurs exemples :
L1 db 0 ; octet étiqueté L1 avec la valeur initiale 0
L2 dw 1000 ; mot étiqueté L2 avec la valeur initiale 1000
L3 db 110101b ; octet initialisé au binaire 110101 (53 en décimal)
L4 db 12h ; octet initialisé en hexadécimal 12 (18 en décimal)
L5 db 17o ; octet initialisé à 17 octal (15 en décimal)
L6 jj 1A92h ; mot double initialisé en hexadécimal 1A92
L7 resb 1 ; 1 octet non initialisé
L8 db "UN" ; octet initialisé au code ASCII pour A (65)
Les guillemets doubles et les guillemets simples sont traités de la même manière. Données consécutives
les définitions sont stockées séquentiellement en mémoire. C'estàdire que le mot L2 est stocké
immédiatement après L1 en mémoire. Des séquences de mémoire peuvent également être définies.
Machine Translated by Google
1.3. LANGAGE D'ASSEMBLAGE 15
L9 db 0, 1, 2, 3 "w", ; définit 4 octets
L10 db "o", "r", 'd', 0 'mot', 0 ; définit une chaîne C = "mot"
L11 db ; identique à L10
La directive DD peut être utilisée pour définir à la fois un nombre entier et une simple précision
constantes à virgule flottante6 . Cependant, le DQ ne peut être utilisé que pour définir des doubles
constantes à virgule flottante de précision.
Pour les grandes séquences, la directive TIMES de NASM est souvent utile. Cette direction
tive répète son opérande un nombre de fois spécifié. Par exemple,
L12 fois 100 db 0 ; équivalent à 100 (db 0)
L13 resw 100 ; réserve de la place pour 100 mots
N'oubliez pas que les étiquettes peuvent être utilisées pour faire référence à des données dans le code. Il y en a deux
manières dont une étiquette peut être utilisée. Si une étiquette simple est utilisée, elle est interprétée comme le
adresse (ou décalage) des données. Si l'étiquette est placée entre crochets
([]), il est interprété comme les données à l'adresse. Autrement dit, il faudrait
considérez une étiquette comme un pointeur vers les données et les déréférencements entre crochets
le pointeur comme le fait l'astérisque en C. (MASM/TASM suivent un autre
convention.) En mode 32 bits, les adresses sont en 32 bits. Voici quelques exemples:
La ligne 7 des exemples montre une propriété importante de NASM. L'assembleur ne garde pas
trace du type de données auquel une étiquette fait référence. C'est à
le programmeur pour s'assurer qu'il (ou elle) utilise correctement une étiquette. Plus tard
il sera courant de stocker des adresses de données dans des registres et d'utiliser le registre
comme une variable de pointeur en C. Encore une fois, aucune vérification n'est faite qu'un pointeur est
utilisé correctement. De cette façon, l'assemblage est beaucoup plus sujet aux erreurs que même C.
Considérez l'instruction suivante :
Cette instruction génère une erreur de taille d'opération non spécifiée. Pourquoi?
Parce que l'assembleur ne sait pas s'il doit stocker le 1 sous forme d'octet, de mot
ou double mot. Pour résoudre ce problème, ajoutez un spécificateur de taille :
6La virgule flottante simple précision équivaut à une variable flottante en C.
Machine Translated by Google
16 CHAPITRE 1 INTRODUCTION
Cela indique à l'assembleur de stocker un 1 au mot double qui commence à L6.
Les autres spécificateurs de taille sont : BYTE, WORD, QWORD et TWORD7 .
1.3.6 Entrée et sortie
L'entrée et la sortie sont des activités très dépendantes du système. Il s'agit
d'interfaçage avec le matériel du système. Les langages de haut niveau, comme C,
fournissent des bibliothèques standard de routines qui fournissent une interface de
programmation simple et uniforme pour les E/S. Les langages d'assemblage ne fournissent
aucune bibliothèque standard. Ils doivent soit accéder directement au matériel (ce qui est
une opération privilégiée en mode protégé), soit utiliser les routines de bas niveau fournies
par le système d'exploitation.
Il est très courant que les routines d'assemblage soient interfacées avec le C. L'un
des avantages de ceci est que le code d'assemblage peut utiliser les routines d'E/S de la
bibliothèque C standard. Cependant, il faut connaître les règles de transmission des
informations entre les routines utilisées par C. Ces règles sont trop compliquées pour être
couvertes ici. (Elles seront couvertes plus tard !) Pour simplifier les E/S, l'auteur a
développé ses propres routines qui cachent les règles complexes du C et fournissent une
interface beaucoup plus simple. Le tableau 1.4 décrit les routines fournies. Toutes les
routines conservent la valeur de tous les registres, à l'exception des routines de lecture.
Ces routines modifient la valeur du registre EAX. Pour utiliser ces routines, il faut inclure
un fichier contenant les informations dont l'assembleur a besoin pour les utiliser.
Pour inclure un fichier dans NASM, utilisez la directive de préprocesseur %include. La
ligne suivante inclut le fichier nécessaire aux routines d'E/S de l'auteur8 :
%include "asm_io.inc"
Pour utiliser l'une des routines d'impression, on charge EAX avec la valeur correcte
et on utilise une instruction CALL pour l'invoquer. L'instruction CALL est équivalente à un
appel de fonction dans un langage de haut niveau. Il saute l'exécution vers une autre
section de code, mais revient à son origine une fois la routine terminée.
L'exemple de programme cidessous montre plusieurs exemples d'appels à ces E/S
routines.
1.3.7 Débogage
La bibliothèque de l'auteur contient également quelques routines utiles pour le
débogage des programmes. Ces routines de débogage affichent des informations sur
l'état de l'ordinateur sans modifier l'état. Ces routines sont vraiment des macros
7
TWORD définit une zone de mémoire de dix octets. Le coprocesseur à virgule flottante utilise ce type
de données.
8Le fichier asm io.inc (et le fichier objet asm io requis par asm io.inc) se trouvent dans les
téléchargements de code d'exemple sur la page Web de ce didacticiel, http://www.drpaulcarter.com/pcasm
Machine Translated by Google
1.3. LANGAGE D'ASSEMBLAGE 17
imprimer en entier imprime à l'écran la valeur de l'entier stocké dans EAX
print char imprime à l'écran le caractère dont la valeur ASCII stockée dans AL print
string imprime à l'écran
le contenu de la chaîne à l'adresse stockée dans EAX. La chaîne doit être une chaîne
de type C (c'estàdire terminée par un caractère nul). imprime à
l'écran un caractère de nouvelle ligne. lit
imprimer un entier à partir du clavier et le stocke dans le registre
nl lire int EAX. lit un seul caractère du clavier et stocke son code ASCII
dans le registre EAX.
lire le caractère
Tableau 1.4 : Routines d'E/S d'assemblage
qui préservent l'état actuel du CPU, puis effectuent un appel de sousprogramme.
Les macros sont définies dans le fichier asm io.inc décrit cidessus. Macros
sont utilisés comme des instructions ordinaires. Les opérandes des macros sont séparés par
virgules.
Il existe quatre routines de débogage nommées dump regs, dump mem, dump stack
et vider les maths ; ils affichent respectivement les valeurs des registres, de la mémoire, de la
pile et du coprocesseur mathématique.
dump regs Cette macro imprime les valeurs des registres (en hexadécimal) de l'ordinateur
vers stdout (c'estàdire l'écran). Il affiche également les bits définis dans le registre
FLAGS9. Par exemple, si le drapeau zéro est 1, ZF est affiché. Si c'est 0, il n'est pas
affiché. Il prend un seul argument entier qui est également imprimé. Cela peut être
utilisé pour distinguer la sortie de différentes commandes dump regs.
dump mem Cette macro imprime les valeurs d'une région de la mémoire (en hexadécimal) et
aussi sous forme de caractères ASCII. Il prend trois arguments délimités par des
virgules. Le premier est un entier utilisé pour étiqueter la sortie (tout comme l'argument
dump regs). Le deuxième argument est l'adresse à afficher. (Cela peut être une
étiquette.) Le dernier argument est le nombre de paragraphes de 16 octets à afficher
après l'adresse. La mémoire affichée commencera à la limite du premier paragraphe
avant l'adresse demandée.
dump stack Cette macro imprime les valeurs sur la pile CPU. (La pile sera traitée au chapitre
4.) La pile est organisée en mots doubles et cette routine les affiche de cette façon. Il
faut trois virgules
9Le chapitre 2 traite de ce registre
Machine Translated by Google
18 CHAPITRE 1 INTRODUCTION
arguments délimités. Le premier est une étiquette entière (comme dump regs).
Le deuxième est le nombre de mots doubles à afficher sous l'adresse que contient le registre
EBP et le troisième argument est le nombre de mots doubles à afficher audessus de l'adresse
dans EBP.
dump math Cette macro imprime les valeurs des registres du coprocesseur mathématique. Il prend
un seul argument entier qui est utilisé pour étiqueter la sortie tout comme l'argument de dump
regs le fait.
1.4 Création d'un programme
Aujourd'hui, il est inhabituel de créer un programme autonome entièrement écrit en langage
assembleur. L'assemblage est généralement utilisé pour définir certaines routines critiques. Pourquoi?
Il est beaucoup plus facile de programmer dans un langage de plus haut niveau qu'en assembleur. De
plus, l'utilisation de l'assembleur rend un programme très difficile à porter sur d'autres platesformes.
En fait, il est rare d'utiliser l'assemblage du tout.
Alors, pourquoi quelqu'un devraitil apprendre l'assemblage ?
1. Parfois, le code écrit en assembleur peut être plus rapide et plus petit que
code généré par le compilateur.
2. L'assemblage permet d'accéder directement aux fonctionnalités matérielles du système qui
pourraient être difficiles ou impossibles à utiliser à partir d'un langage de niveau supérieur.
3. Apprendre à programmer en assembleur permet de mieux comprendre le fonctionnement des
ordinateurs.
4. Apprendre à programmer en assembleur aide à mieux comprendre comment
les compilateurs et les langages de haut niveau comme C fonctionnent.
Ces deux derniers points démontrent que l'apprentissage de l'assembleur peut être utile même si on
ne programme jamais dedans par la suite. En fait, l'auteur programme rarement en assembleur, mais
il utilise au quotidien les idées qu'il en a tirées.
1.4.1 Premier programme
Les premiers programmes de ce texte commenceront tous à partir du simple programme de pilote
C de la figure 1.6. Il appelle simplement une autre fonction nommée asm main.
C'est vraiment une routine qui sera écrite en assembleur. L'utilisation de la routine du pilote C présente
plusieurs avantages. Tout d'abord, cela permet au système C de configurer le programme pour qu'il
s'exécute correctement en mode protégé. Tous les segments et leurs registres de segments
correspondants seront initialisés par C. Le code assembleur n'a pas à se soucier de tout cela.
Deuxièmement, la bibliothèque C sera également disponible pour être utilisée par le code assembleur.
Les routines d'E/S de l'auteur prennent
Machine Translated by Google
1.4. CRÉATION D'UN PROGRAMME 19
1 entier principal()
2 {
3 int ret statut ;
4 ret status = asm main();
5 retourner l'état ret ;
6 }
Figure 1.6 : code pilote.c
avantage de cela. Ils utilisent les fonctions d'E/S du C (printf, etc.). Ce qui suit
montre un programme d'assemblage simple.
premier.asm
1 ; fichier : premier.asm
2 ; Premier programme d'assemblage. Ce programme demande deux nombres entiers comme
3 ; entrée et imprime leur somme.
4 ;
5 ; Pour créer un exécutable avec djgpp :
6 ; nasm f coff premier.asm
7 ; gcc o premier premier.o pilote.c asm_io.o
8
9 % incluent "asm_io.inc"
dix ;
11 ; les données initialisées sont placées dans le segment .data
12 ;
13 segments .données
14 ;
15 ; Ces étiquettes font référence aux chaînes utilisées pour la sortie
16 ;
17 prompt1 db 18 "Entrez un nombre : ", 0 ; n'oubliez pas le terminateur nul
prompt2 db 19 "Entrez un autre numéro : ", 0
outmsg1 db 20 "Vous avez entré", 0
"
outmsg2 db 21 et ", 0
outmsg3 db ", la somme de ceuxci est ", 0
22
23 ;
24 ; les données non initialisées sont placées dans le segment .bss
25 ;
26 segments .bss
27 ;
28 ; Ces étiquettes font référence à des mots doubles utilisés pour stocker les entrées
29 ;
Machine Translated by Google
20 CHAPITRE 1 INTRODUCTION
30 entrée1 resd 1
31 entrée2 resd 1
32
33 ;
34 ; le code est placé dans le segment .text
35 ;
36 segments .texte
37 global_asm_main
38 _asm_main :
39 entrez 0,0 pusha ; routine de configuration
40
41
42 mouvement
eax, prompt1 ; imprimer l'invite
43 appel print_string
44
48 mouvement
eax, invite2 print_string ; imprimer l'invite
49 appel
50
54 mouvement
eax, [entrée1] eax, ; eax = dword à l'entrée1
55 ajouter [entrée2] ebx, eax ; eax += dword à l'entrée2
56 mouvement ; ebx = eax
57
58 dump_regs 1 ; imprimer les valeurs de registre
59 dump_mem 2, outmsg1, 1 ; imprimer la mémoire
60 ;
61 ; prochain message de résultat imprimé sous forme de série d'étapes
62 ;
63 mouvement
eax, outmsg1
64 appel print_string eax, ; imprimer le premier message
65 mouvement
[entrée1]
66 appel print_int eax, ; imprimer l'entrée1
67 mouvement
outmsg2
68 appel print_string eax, ; imprimer le deuxième message
69 mouvement
[entrée2]
70 appel print_int eax, ; imprimer l'entrée2
71 mouvement
outmsg3
Machine Translated by Google
1.4. CRÉATION D'UN PROGRAMME 21
77 papa
78 mouvement eax, 0 ; revenir à C
79 partir
80 ret
premier.asm
La ligne 13 du programme définit une section du programme qui spécifie
mémoire à stocker dans le segment de données (dont le nom est .data). Seul
les données initialisées doivent être définies dans ce segment. Sur les lignes 17 à 21, plusieurs
les chaînes sont déclarées. Ils seront imprimés avec la bibliothèque C et doivent donc
se terminer par un caractère nul (code ASCII 0). N'oubliez pas qu'il y a un
grande différence entre 0 et '0'.
Les données non initialisées doivent être déclarées dans le segment bss (nommé .bss
à la ligne 26). Ce segment tire son nom d'un ancien opérateur d'assemblage basé sur UNIX qui
signifiait "bloc commencé par un symbole". Il y a aussi une pile
segment aussi. Il sera discuté plus tard.
Le segment de code est nommé historiquement .text. C'est là que les instructions
sont placés. Notez que l'étiquette de code de la routine principale (ligne 38) a une
préfixe de soulignement. Cela fait partie de la convention d'appel C. Cette convention spécifie
les règles que C utilise lors de la compilation du code. Il est très important
connaître cette convention lors de l'interfaçage du C et de l'assembleur. Plus tard, toute la
convention sera présentée ; cependant, pour l'instant, il suffit de savoir
que tous les symboles C (c'estàdire les fonctions et les variables globales) ont un trait de soulignement
préfixe qui leur est ajouté par le compilateur C. (Cette règle est spécifiquement pour
DOS/Windows, le compilateur Linux C n'ajoute rien aux noms des symboles C.)
La directive globale de la ligne 37 indique à l'assembleur de rendre l'asm main
label mondial. Contrairement au C, les étiquettes ont une portée interne par défaut. Ça signifie
que seul le code du même module peut utiliser l'étiquette. La directive mondiale
donne la portée externe de l'étiquette (ou des étiquettes) spécifiée. Ce type d'étiquette peut être
accessible par n'importe quel module du programme. Le module asm io déclare le
print int, et al. les étiquettes soient globales. C'est pourquoi on peut les utiliser dans
premier module.asm.
Machine Translated by Google
22 CHAPITRE 1 INTRODUCTION
1.4.2 Dépendances du compilateur
Le code assembleur cidessus est spécifique au compilateur gratuit DJGPP C/C++ basé
sur GNU10.11 Ce compilateur peut être téléchargé gratuitement sur Internet. Il nécessite un
PC 386 ou supérieur et fonctionne sous DOS, Windows 95/98 ou NT. Ce compilateur utilise
des fichiers objets au format COFF (Common Object File Format). Pour assembler à ce
format, utilisez le commutateur f coff avec nasm (comme indiqué dans les commentaires du
code cidessus). L'extension du fichier objet résultant sera o.
Le compilateur Linux C est également un compilateur GNU. Pour convertir le code ci
dessus pour qu'il s'exécute sous Linux, supprimez simplement les préfixes de soulignement
aux lignes 37 et 38. Linux utilise le format ELF (Executable and Linkable Format) pour les
fichiers objets. Utilisez le commutateur f elf pour Linux. Il produit également un objet
Le compilateur spécifique ex avec une extension o. de
nombreux fichiers, disponibles auprès de Borland C/C++ est un autre compilateur populaire. Il utilise le site Web de Microsoft
l'auteur, a le format OMF pour les fichiers objets. Utilisez le commutateur f obj pour les compilateurs Borland. déjà été modifié
en L'extension du fichier objet
sera obj. Le format OMF utilise un travail différent avec les directives de segment ent
appropriées que les autres
formats d'objet. Le compilateur de segments de données. (ligne 13) doit être remplacé par :
segment DATA public align=4 class=DATA use32
Le segment bss (ligne 26) doit être remplacé par :
segment BSS public align=4 class=BSS use32
Le segment de texte (ligne 36) doit être remplacé par :
segment TEXTE public align=1 class=CODE use32
De plus, une nouvelle ligne doit être ajoutée avant la ligne 36 :
groupe DGROUP BSS DATA
Le compilateur Microsoft C/C++ peut utiliser le format OMF ou le format Win32 pour les
fichiers objets. (Si un format OMF lui est attribué, il convertit les informations au format Win32
en interne.) Le format Win32 permet de définir des segments comme pour DJGPP et Linux.
Utilisez le commutateur f win32 pour sortir dans ce mode. L'extension du fichier objet sera obj.
1.4.3 Assemblage du code
La première étape consiste à assembler le code. Depuis la ligne de commande, tapez :
nasm f formatobjet premier.asm
10GNU est un projet de la Free Software Foundation (http://www.fsf.org)
11http://www.delorie.com/djgpp
Machine Translated by Google
1.4. CRÉATION D'UN PROGRAMME 23
où objectformat est soit coff , elf , obj ou win32 selon le compilateur C qui sera utilisé. (N'oubliez
pas que le fichier source doit également être modifié pour Linux et Borland.)
1.4.4 Compilation du code C
Compilez le fichier driver.c à l'aide d'un compilateur C. Pour DJGPP, utilisez :
gcc c pilote.c
Le commutateur c signifie simplement compiler, n'essayez pas encore de lier. Ce même
commutateur fonctionne également sur les compilateurs Linux, Borland et Microsoft.
1.4.5 Liaison des fichiers objets
La liaison est le processus consistant à combiner le code machine et les données dans
des fichiers objets et des fichiers de bibliothèque pour créer un fichier exécutable. Comme on
le verra cidessous, ce processus est compliqué.
Le code C nécessite la bibliothèque C standard et un code de démarrage spécial pour s'exécuter.
Il est beaucoup plus facile de laisser le compilateur C appeler l'éditeur de liens avec les
paramètres corrects que d'essayer d'appeler l'éditeur de liens directement. Par exemple, pour
lier le code du premier programme à l'aide de DJGPP, utilisez :
gcc o premier pilote.o premier.o asm io.o
Cela crée un exécutable appelé first.exe (ou juste first sous Linux).
Avec Borland, on utiliserait :
bcc32 premier.obj pilote.obj asm io.obj
Borland utilise le nom du premier fichier répertorié pour déterminer le nom de l'exécutable.
Ainsi, dans le cas cidessus, le programme serait nommé first.exe.
Il est possible de combiner l'étape de compilation et de liaison. Par exemple,
gcc o premier pilote.c premier.o asm io.o
Maintenant, gcc compilera driver.c puis créera un lien.
1.4.6 Comprendre un fichier de liste d'assemblage
Le commutateur l listingfile peut être utilisé pour indiquer à nasm de créer un fichier de
liste d'un nom donné. Ce fichier montre comment le code a été assemblé. Voici comment les
lignes 17 et 18 (dans le segment de données) apparaissent dans le fichier listing. (Les numéros
de ligne sont dans le fichier de liste ; cependant, notez que les numéros de ligne dans le fichier
source peuvent ne pas être les mêmes que les numéros de ligne dans le fichier de liste.)
Machine Translated by Google
24 CHAPITRE 1 INTRODUCTION
La première colonne de chaque ligne est le numéro de ligne et la seconde est le décalage
(en hexadécimal) des données du segment. La troisième colonne montre l'hexagone brut
valeurs qui seront stockées. Dans ce cas, les données hexadécimales correspondent à ASCII
codes. Enfin, le texte du fichier source s'affiche sur la ligne. Le
les décalages répertoriés dans la deuxième colonne ne sont très probablement pas les vrais décalages qui
les données seront placées dans le programme complet. Chaque module peut définir
ses propres étiquettes dans le segment de données (et les autres segments également). Dans le lien
(voir section 1.4.5), toutes ces définitions d'étiquettes de segments de données sont combinées
pour former un segment de données. Les nouveaux décalages finaux sont alors calculés par le
lieur.
Voici une petite section (lignes 54 à 56 du fichier source) du texte
segment dans le fichier listing :
94 0000002C A1[00000000] 95 mouvement
eax, [entrée1]
00000031 0305[04000000] ajouter eax, [entrée2]
96 00000037 89C3 mouvement ebx, eax
La troisième colonne affiche le code machine généré par l'assembly. Souvent
le code complet d'une instruction ne peut pas encore être calculé. Par exemple,
à la ligne 94, le décalage (ou l'adresse) de l'entrée 1 n'est pas connu tant que le code n'est pas
lié. L'assembleur peut calculer l'opcode pour l'instruction mov
(qui de la liste est A1), mais il écrit le décalage entre crochets
car la valeur exacte ne peut pas encore être calculée. Dans ce cas, un temporaire
un décalage de 0 est utilisé car input1 est au début de la partie du bss
segment défini dans ce fichier. N'oubliez pas que cela ne signifie pas que ce sera
au début du dernier segment bss du programme. Lorsque le code
est lié, le lieur insérera le décalage correct dans la position. Autre
les instructions, comme la ligne 96, ne font référence à aucune étiquette. Ici l'assembleur
peut calculer le code machine complet.
Représentation Big et Little Endian
Si l'on regarde attentivement la ligne 95, quelque chose semble très étrange à propos du
offset entre crochets du code machine. L'étiquette input2 est à
décalage 4 (tel que défini dans ce fichier) ; cependant, le décalage qui apparaît en mémoire
n'est pas 00000004, mais 04000000. Pourquoi ? Différents processeurs stockent plusieurs octets
entiers dans différents ordres en mémoire. Il existe deux méthodes populaires de
Endian se prononce comme le stockage d'entiers : big endian et little endian. Big endian est la méthode
Indien.
Machine Translated by Google
1.5. FICHIER DE SQUELETTE 25
cela semble le plus naturel. Le plus gros octet (c'estàdire le plus significatif) est stocké en premier,
puis le suivant, etc. Par exemple, le dword 00000004 serait stocké sous la forme des quatre octets 00
00 00 04. Les mainframes IBM, la plupart des processeurs RISC et les processeurs Motorola utilisent
tous ce gros méthode endian. Cependant, les processeurs basés sur Intel utilisent la méthode little
endian ! Ici, l'octet le moins significatif est stocké en premier. Ainsi, 00000004 est stocké en mémoire
sous la forme 04 00 00 00. Ce format est câblé dans le CPU et ne peut pas être modifié. Normalement,
le programmeur n'a pas à se soucier du format utilisé. Cependant, il y a des circonstances où c'est
important.
1. Lorsque des données binaires sont transférées entre différents ordinateurs (soit à partir de
fichiers, soit via un réseau).
2. Lorsque des données binaires sont écrites dans la mémoire sous la forme d'un entier multioctet
puis lus en tant qu'octets individuels ou vice versa.
L'endianité ne s'applique pas à l'ordre des éléments du tableau. Le premier élément d'un tableau
est toujours à l'adresse la plus basse. Cela s'applique aux chaînes (qui ne sont que des tableaux de
caractères). L'endianité s'applique toujours aux éléments individuels des tableaux.
1.5 Fichier squelette
La figure 1.7 montre un fichier squelette qui peut être utilisé comme point de départ pour écrire
des programmes d'assemblage.
Machine Translated by Google
26 CHAPITRE 1 INTRODUCTION
skel.asm
1 %inclut "asm_io.inc"
2 segments .données
3 ;
4 ; les données initialisées sont placées dans le segment de données ici
5 ;
6
7 segments .bss
8 ;
9 ; les données non initialisées sont placées dans le segment bss
dix ;
11
12 segments .texte
13 global_asm_main
14 _asm_main :
15 entrez 0,0 pusha ; routine de configuration
16
17
18 ;
19 ; le code est placé dans le segment de texte. Ne modifiez pas le code avant
20 ; ou après ce commentaire.
21 ;
22
23 papa
24 mouvement eax, 0 ; revenir à C
25 partir
26 ret
skel.asm
Figure 1.7 : Programme squelette
Machine Translated by Google
Chapitre 2
Langage d'assemblage de base
2.1 Travailler avec des entiers
2.1.1 Représentation entière
Les nombres entiers existent en deux types : non signés et signés. Les entiers
non signés (qui ne sont pas négatifs) sont représentés de manière binaire très
simple. Le nombre 200 sous la forme d'un entier non signé d'un octet serait
représenté par 11001000 (ou C8 en hexadécimal).
Les entiers signés (qui peuvent être positifs ou négatifs) sont représentés de
manière plus compliquée. Par exemple, considérons −56. +56 en tant qu'octet serait
représenté par 00111000. Sur papier, on pourrait représenter −56 comme −111000,
mais comment cela seraitil représenté en un octet dans la mémoire de l'ordinateur.
Comment le signe moins seraitil stocké ?
Il existe trois techniques générales qui ont été utilisées pour représenter des
entiers signés dans la mémoire d'un ordinateur. Toutes ces méthodes utilisent le bit
le plus significatif de l'entier comme bit de signe. Ce bit vaut 0 si le nombre est positif
et 1 s'il est négatif.
Magnitude signée
La première méthode est la plus simple et est appelée magnitude signée. Il
représente l'entier en deux parties. La première partie est le bit de signe et la seconde
est la grandeur de l'entier. Ainsi, 56 serait représenté par l'octet 00111000 (le bit de
signe est souligné) et 56 serait 10111000. La plus grande valeur d'octet serait
01111111 ou +127 et la plus petite valeur d'octet serait 11111111 ou 127. Pour
annuler une valeur, le bit de signe est inversé.
Cette méthode est simple, mais elle a ses inconvénients. Premièrement, il existe
deux valeurs possibles de zéro, +0 (00000000) et −0 (10000000). Puisque zéro n'est
ni positif ni négatif, ces deux représentations devraient agir de la même manière.
Cela complique la logique de l'arithmétique pour le CPU. Deuxièmement,
27
Machine Translated by Google
28 CHAPITRE 2. LANGAGE DE BASE DE L'ASSEMBLAGE
l'arithmétique générale est également compliquée. Si 10 est ajouté à −56, cela doit être
refondu en 10 soustrait par 56. Encore une fois, cela complique la logique du CPU.
Complément à un
La deuxième méthode est connue sous le nom de représentation en complément à un. Le
le complément à un d'un nombre est trouvé en inversant chaque bit du nombre.
(Une autre façon de voir les choses est que la nouvelle valeur de bit est 1 oldbitvalue.) Pour
Par exemple, le complément à un de 00111000 (+56) est 11000111. Dans la notation du
complément à un, le calcul du complément à un équivaut à la négation. Ainsi, 11000111 est la
représentation de −56. Notez que le bit de signe
a été automatiquement modifié par son complément à un et que, comme on pouvait s'y attendre,
prendre le complément à un deux fois donne le nombre d'origine. Pour ce qui est de
la première méthode, il y a deux représentations du zéro : 00000000 (+0) et
11111111 (−0). L'arithmétique avec ses nombres complémentaires est compliquée.
Il existe une astuce pratique pour trouver le complément à un d'un nombre dans
hexadécimal sans le convertir en binaire. L'astuce consiste à soustraire l'hexagone
chiffre à partir de F (ou 15 en décimal). Cette méthode suppose que le nombre de
bits dans le nombre est un multiple de 4. Voici un exemple : +56 est représenté
par 38 en hex. Pour trouver le complément à un, soustrayez chaque chiffre de F à
obtenir C7 en hexagone. Ceci est en accord avec le résultat cidessus.
Complément à deux
Les deux premières méthodes décrites ont été utilisées sur les premiers ordinateurs. Moderne
les ordinateurs utilisent une troisième méthode appelée représentation en complément à deux. Le
le complément à deux d'un nombre se trouve par les deux étapes suivantes :
1. Trouver le complément à un du nombre
2. Ajoutez un au résultat de l'étape 1
Voici un exemple utilisant 00111000 (56). On calcule d'abord le complément à un : 11000111.
Puis on ajoute un :
11000111
+ 1
11001000
Dans la notation du complément à deux, le calcul du complément à deux équivaut à la
négation d'un nombre. Ainsi, 11001000 est la représentation en complément à deux de −56. Deux
négations doivent reproduire le nombre original.
Le complément à deux surprenant répond à cette exigence. Prends les deux
Machine Translated by Google
2.1. TRAVAILLER AVEC DES ENTIERS 29
Représentation hexadécimale des nombres
0 00
1 01
127 7F
128 80
127 81
2 FE
1 FF
Tableau 2.1 : Représentation en complément à deux
complément de 11001000 en ajoutant un au complément à un.
00110111
+ 1
00111000
Lors de l'exécution de l'addition dans l'opération de complément à deux, l'addition du
bit le plus à gauche peut produire un report. Ce portage n'est pas utilisé.
N'oubliez pas que toutes les données sur l'ordinateur ont une taille fixe (en termes de
nombre de bits). L'ajout de deux octets produit toujours un octet comme résultat (tout
comme l'ajout de deux mots produit un mot, etc.). Cette propriété est importante pour la
notation en complément à deux. Par exemple, considérez zéro comme un nombre de
complément à deux sur un octet (00000000). Le calcul de son complément à deux produit
la somme:
11111111
+ 1
c 00000000
où c représente un report. (Plus tard, il sera montré comment détecter ce report, mais il
n'est pas stocké dans le résultat.) Ainsi, dans la notation en complément à deux, il n'y a
qu'un seul zéro. Cela rend l'arithmétique du complément à deux plus simple que les
méthodes précédentes.
En utilisant la notation de complément à deux, un octet signé peut être utilisé pour
représenter les nombres 128 à +127. Le tableau 2.1 montre quelques valeurs
sélectionnées. Si 16 bits sont utilisés, les nombres signés 32, 768 à +32, 767 peuvent
être représentés. +32, 767 est représenté par 7FFF, 32, 768 par 8000, 128 par FF80 et
1 par FFFF. Les nombres de complément à deux 32 bits vont de 2 milliards à +2 milliards
environ.
La CPU n'a aucune idée de ce qu'un octet particulier (ou un mot ou un double mot)
est censé représenter. L'assembleur n'a pas l'idée des types qu'un langage de haut niveau
possède. La façon dont les données sont interprétées dépend de l'instruction utilisée sur
les données. Que la valeur hexadécimale FF soit considérée comme représentant un 1
signé ou un +255 non signé dépend du programmeur. Le langage C
Machine Translated by Google
30 CHAPITRE 2. LANGAGE DE BASE DE L'ASSEMBLAGE
définit les types d'entiers signés et non signés. Cela permet à un compilateur C de déterminer les instructions
correctes à utiliser avec les données.
2.1.2 Extension du signe
En assemblage, toutes les données ont une taille spécifiée. Il n'est pas rare de devoir modifier la taille des
données pour les utiliser avec d'autres données. Diminuer la taille est le plus simple.
Diminution de la taille des données
Pour réduire la taille des données, supprimez simplement les bits les plus significatifs de
les données. Voici un exemple trivial :
Bien sûr, si le nombre ne peut pas être représenté correctement dans la plus petite taille, la diminution de
la taille ne fonctionne pas. Par exemple, si AX était 0134h (ou 308 en décimal), le code cidessus définirait
toujours CL sur 34h. Cette méthode fonctionne avec les nombres signés et non signés. Considérez les nombres
signés, si AX était FFFFh (−1 comme mot), alors CL serait FFh (−1 comme octet).
Cependant, notez que ce n'est pas correct si la valeur dans AX n'était pas signée !
La règle pour les nombres non signés est que tous les bits supprimés doivent être 0 pour que la conversion
soit correcte. La règle pour les nombres signés est que les bits supprimés doivent être soit tous des 1, soit tous
des 0. De plus, le premier bit non supprimé doit avoir la même valeur que les bits supprimés. Ce bit sera le
nouveau bit de signe de la plus petite valeur. Il est important qu'il soit identique au bit de signe d'origine !
Augmentation de la taille des données
L'augmentation de la taille des données est plus compliquée que la diminution. Considérez l'octet
hexadécimal FF. S'il est étendu à un mot, quelle valeur doit avoir le mot ? Cela dépend de la façon dont FF est
interprété. Si FF est un octet non signé (255 en décimal), alors le mot doit être 00FF ; cependant, s'il s'agit d'un
octet signé (−1 en décimal), le mot doit être FFFF.
En général, pour étendre un nombre non signé, on met tous les nouveaux bits du nombre étendu à 0.
Ainsi, FF devient 00FF. Cependant, pour étendre un nombre signé, il faut étendre le bit de signe. Cela signifie
que les nouveaux bits deviennent des copies du bit de signe. Puisque le bit de signe de FF est 1, les nouveaux
bits doivent également être tous des uns pour produire FFFF. Si le nombre signé 5A (90 en décimal) était
prolongé, le résultat serait 005A.
Machine Translated by Google
2.1. TRAVAILLER AVEC DES ENTIERS 31
Le 80386 fournit plusieurs instructions pour l'extension des numéros. N'oubliez pas que
l'ordinateur ne sait pas si un nombre est signé ou non signé. Il appartient au programmeur
d'utiliser la bonne instruction.
Pour les nombres non signés, on peut simplement mettre des zéros dans les bits
supérieurs en utilisant une instruction MOV. Par exemple, pour étendre l'octet dans AL à un
mot non signé dans AX :
mouvement
ah, 0 ; mettre à zéro les 8 bits supérieurs
Cependant, il n'est pas possible d'utiliser une instruction MOV pour convertir le mot non signé
dans AX en un double mot non signé dans EAX. Pourquoi pas? Il n'y a aucun moyen de
spécifier les 16 bits supérieurs d'EAX dans un MOV. Le 80386 résout ce problème en
fournissant une nouvelle instruction MOVZX. Cette instruction a deux opérandes.
La destination (premier opérande) doit être un registre 16 ou 32 bits. La source (deuxième
opérande) peut être un registre de 8 ou 16 bits ou un octet ou un mot de mémoire.
L'autre restriction est que la destination doit être plus grande que la source.
(La plupart des instructions exigent que la source et la destination aient la même taille.)
Voici quelques exemples:
movzx eax, hache ; s'étend ax dans eax ; prolonge
movzx eax, al movzx al en eax ; prolonge al en ax ;
hache, al movzx étend la hache dans ebx
ebx, hache
Pour les nombres signés, il n'y a pas de moyen facile d'utiliser l'instruction MOV dans
tous les cas. Le 8086 a fourni plusieurs instructions pour étendre les numéros signés.
Le signe d'instruction CBW (Convert Byte to Word) étend le registre AL en AX. Les opérandes
sont implicites. Le signe d'instruction CWD (Convert Word to Double word) étend AX en
DX:AX. La notation DX:AX signifie considérer les registres DX et AX comme un seul registre
de 32 bits avec les 16 bits supérieurs dans DX et les bits inférieurs dans AX. (Rappelezvous
que le 8086 n'avait pas de registres 32 bits !) Le 80386 a ajouté plusieurs nouvelles
instructions. Le signe d'instruction CWDE (Convert Word to Double word Extended) étend AX
en EAX. Le signe d'instruction CDQ (Convert Double word to Quad word) étend EAX en
EDX:EAX (64 bits !). Enfin, l'instruction MOVSX fonctionne comme MOVZX sauf qu'elle utilise
les règles des nombres signés.
Application à la programmation C
L'extension des entiers non signés et signés se produit également en C. Les variables en ANSI C ne définissent pas C
peut être déclarée comme signée ou non signée (int est signé). Considérez si le type char est signé ou non, cela dépend du
ligne 3, la variable a est étendue en utilisant les règles de chaque compilateur individuel pour code de la figure 2.1. À la
les valeurs non signées (en
utilisant MOVZX), mais à la ligne 4, les règles signées sont utilisées pour en décider. C'est
pourquoi pour b (en utilisant
MOVSX). le type est
explicitement défini dans la figure 2.1.
Machine Translated by Google
32 CHAPITRE 2. LANGAGE DE BASE DE L'ASSEMBLAGE
1 caractère non signé uchar = 0xFF ;
2 caractères signés schar = 0xFF ;
3 int a = (int ) uchar ; 4 int / a = 255 (0x000000FF) / /
b = (int ) schar ; b = −1 (0xFFFFFFFF) /
Figure 2.1 :
caractère
ch ; while( (ch = fgetc(fp )) != EOF ) { /
faire quelque chose avec ch
/ }
Figure 2.2 :
Il existe un bogue de programmation C commun qui est directement lié à cela
sujet. Considérez le code de la figure 2.2. Le prototype de fgetc() est :
int fgetc( FICHIER * );
On peut se demander pourquoi la fonction renvoietelle un int puisqu'elle lit des
caractères ? La raison en est qu'il renvoie normalement un char (étendu à une valeur
int en utilisant l'extension zéro). Cependant, il peut renvoyer une valeur qui n'est pas un
caractère, EOF. Il s'agit d'une macro généralement définie par −1. Ainsi, fgetc() renvoie
soit un caractère étendu à une valeur int (qui ressemble à 000000xx en hexadécimal)
ou EOF (qui ressemble à FFFFFFFF en hexadécimal).
Le problème de base avec le programme de la figure 2.2 est que fgetc() retourne
un int, mais cette valeur est stockée dans un char. C tronquera les bits d'ordre supérieur
pour adapter la valeur int au caractère. Le seul problème est que les nombres (en
hexadécimal) 000000FF et FFFFFFFF seront tous deux tronqués à l'octet FF. Ainsi, la
boucle while ne peut pas faire la distinction entre la lecture de l'octet FF du fichier et la
fin du fichier.
Ce que fait exactement le code dans ce cas dépend si char est signé ou non signé.
Pourquoi? Parce qu'à la ligne 2, ch est comparé à EOF.
Puisque EOF est une valeur int1
, ch sera étendu à un int de sorte que deux valeurs
comparées soient de la même taille2 . Comme le montre la figure 2.1, l'endroit où la
variable est signée ou non signée est très important.
Si char n'est pas signé, FF est étendu à 000000FF. Ceci est comparé à
EOF (FFFFFFFF) et trouvé différent. Ainsi, la boucle ne se termine jamais !
1
C'est une idée fausse courante que les fichiers ont un caractère EOF à leur fin. Ce n'est
pas vrai!
2La raison de cette exigence sera expliquée plus loin.
Machine Translated by Google
2.1. TRAVAILLER AVEC DES ENTIERS 33
Si char est signé, FF est étendu à FFFFFFFF. Cela se compare comme égal et la boucle
se termine. Cependant, étant donné que l'octet FF a peutêtre été lu dans le fichier, la boucle
pourrait se terminer prématurément.
La solution à ce problème est de définir la variable ch comme un int, pas un char. Lorsque
cela est fait, aucune troncature ou extension n'est effectuée à la ligne 2. À l'intérieur de la
boucle, il est prudent de tronquer la valeur car ch doit en fait être un simple octet.
2.1.3 Arithmétique en complément à deux
Comme on l'a vu précédemment, l'instruction d'addition effectue l'addition et la sous
instruction effectue la soustraction. Deux des bits du registre FLAGS définis par ces
instructions sont le drapeau de débordement et de retenue. L'indicateur de débordement est
défini si le véritable résultat de l'opération est trop grand pour tenir dans la destination de
l'arithmétique signée. Le drapeau de retenue est activé s'il y a une retenue dans le msb d'une
addition ou un emprunt dans le msb d'une soustraction. Ainsi, il peut être utilisé pour détecter
un débordement pour l'arithmétique non signée. Les utilisations du drapeau de portage pour
l'arithmétique signée seront vues sous peu. L'un des grands avantages du complément à 2
est que les règles d'addition et de soustraction sont exactement les mêmes que pour les
entiers non signés. Ainsi, add et sub peuvent être utilisés sur des entiers signés ou non signés.
002C 44
+ FFFF + (−1)
002B 43
Il y a un report généré, mais cela ne fait pas partie de la réponse.
Il existe deux instructions de multiplication et de division différentes. Tout d'abord, pour
utiliser plusieurs fois l'instruction MUL ou IMUL. L'instruction MUL est utilisée pour multiplier
des nombres non signés et IMUL est utilisée pour multiplier des entiers signés.
Pourquoi fautil deux instructions différentes ? Les règles de multiplication sont différentes
pour les nombres non signés et les nombres signés en complément à 2. Comment?
Considérez la multiplication de l'octet FF par luimême donnant un mot résultat.
En utilisant une multiplication non signée, cela donne 255 fois 255 ou 65025 (ou FE01 en
hexadécimal). En utilisant la multiplication signée, c'est −1 fois −1 ou 1 (ou 0001 en hexadécimal).
Il existe plusieurs formes d'instructions de multiplication. La forme la plus ancienne
ressemble à :
source multiple
La source est soit un registre, soit une référence mémoire. Il ne peut pas s'agir d'une valeur
immédiate. La multiplication exacte qui est effectuée dépend de la taille de l'opérande source.
Si l'opérande est de la taille d'un octet, il est multiplié par l'octet dans le registre AL et le
résultat est stocké dans les 16 bits de AX. Si la source est 16 bits, elle est multipliée par le
mot dans AX et le résultat 32 bits
Machine Translated by Google
34 CHAPITRE 2. LANGAGE DE BASE DE L'ASSEMBLAGE
destination source1 source2 Action
reg/mem8 AX = AL*source1
reg/mem16 DX:AX = AX*source1
reg/mem32 EDX:EAX = EAX*source1
reg16 reg/mem16 destination *= source1
reg32 reg/mem32 destination *= source1
reg16 immed8 dest *= immed8
reg32 immed8 reg16 destination *= immed8
immed16 dest *= immed16
reg32 immed32 reg16 destination *= immed32
reg/mem16 immed8 destination = source1*source2
reg32 reg/mem32 immed8 destination = source1*source2
reg16 reg/mem16 immed16 destination = source1*source2
reg32 reg/mem32 immed32 dest = source1*source2
Tableau 2.2 : Instructions imul
est stocké dans DX:AX. Si la source est 32 bits, elle est multipliée par EAX et le
Le résultat 64 bits est stocké dans EDX:EAX.
L'instruction IMUL a les mêmes formats que MUL, mais ajoute également quelques
d'autres formats d'instructions. Il existe deux et trois formats d'opérandes :
destination imul, source1
destination imul, source1, source2
Le tableau 2.2 montre les combinaisons possibles.
Les deux opérateurs de division sont DIV et IDIV. Ils effectuent non signé
et la division entière signée respectivement. Le format général est :
source div
Si la source est 8 bits, alors AX est divisé par l'opérande. Le quotient est
stocké dans AL et le reste dans AH. Si la source est 16 bits, alors DX:AX
est divisé par l'opérande. Le quotient est stocké dans AX et le reste
en DX. Si la source est 32 bits, alors EDX:EAX est divisé par l'opérande
et le quotient est stocké dans EAX et le reste dans EDX. L'IDIV
l'instruction fonctionne de la même manière. Il n'y a pas d'instructions IDIV spéciales comme
les spéciaux IMUL. Si le quotient est trop grand pour tenir dans son registre ou si le
diviseur est zéro, le programme est interrompu et se termine. Un très commun
l'erreur est d'oublier d'initialiser DX ou EDX avant la division.
L'instruction NEG nie son opérande unique en calculant ses deux
complément. Son opérande peut être n'importe quel registre 8 bits, 16 bits ou 32 bits ou
emplacement mémoire.
Machine Translated by Google
2.1. TRAVAILLER AVEC DES ENTIERS 35
2.1.4 Exemple de programme
math.asm
1 %inclut "asm_io.inc"
2 segments .data 3 invite ; Chaînes de sortie
db "Entrez un nombre : ", 0
4 square_msg 5 db "Le carré de l'entrée est ", 0
cube_msg 6 db "Le cube d'entrée est ", 0
cube25_msg 7 db "Cube d'entrée fois 25 est ", 0
quot_msg db "Quotient de cube/100 est ", 0
8 rem_msg db "Le reste du cube/100 est ", 0
9 neg_msg db "La négation du reste est ", 0
dix
11 segments .bss
12 entrée resd 1
13
14 segments .texte
15 global_asm_main
16 _asm_main :
17 entrez 0,0 pusha ; routine de configuration
18
19
20 mouvement
eax, invite
21 appel print_string
22
23 appel read_int
24 mouvement
[entrée], eax
25
34 mouvement ebx, eax
35 imul ebx, [entrée] eax, ; ebx *= [entrée]
36 mouvement
cube_msg
37 appel print_string
38 mouvement eax, ebx
39 appel print_int
40 appel print_nl
Machine Translated by Google
36 CHAPITRE 2. LANGAGE DE BASE DE L'ASSEMBLAGE
41
49 mouvement eax, ebx
50 CDQ ; initialiser edx par extension de signe
51 mouvement ex, 100 ; ne peut pas diviser par une valeur immédiate
52 idiv exx ; edx:eax / ecx
53 mouvement ecx, eax ; enregistrer le quotient dans ecx
54 mouvement
eax, quot_msg
55 appel print_string
56 mouvement eax, ecx
57 appel print_int
58 appel print_nl
59 mouvement
eax, rem_msg
60 appel print_string
61 mouvement eax, edx
62 appel print_int
63 appel print_nl
64
72 papa
73 mouvement eax, 0 ; revenir à C
74 partir
75 ret
math.asm
2.1.5 Arithmétique de précision étendue
Le langage d'assemblage fournit également des instructions qui permettent d'effectuer
addition et soustraction de nombres supérieurs à des mots doubles. Ces instructions utilisent le drapeau de
portage. Comme indiqué cidessus, les instructions ADD et SUB modifient le drapeau de report si un report ou
un emprunt sont générés, respectivement.
Machine Translated by Google
2.2. STRUCTURES DE CONTRÔLE 37
Ces informations stockées dans le drapeau de retenue peuvent être utilisées pour ajouter ou soustraire
grands nombres en divisant l'opération en un mot double plus petit (ou
plus petits).
Les instructions ADC et SBB utilisent ces informations dans le drapeau de retenue. Le
L'instruction ADC effectue l'opération suivante :
opérande1 = opérande1 + indicateur de retenue + opérande2
L'instruction SBB effectue:
opérande1 = opérande1 porter le drapeau opérande2
Comment sontils utilisés ? Considérez la somme des entiers 64 bits dans EDX: EAX et
EBX : ECX. Le code suivant stockerait la somme dans EDX:EAX :
La soustraction est très similaire. Le code suivant soustrait EBX:ECX de
EDX : EAX :
Pour les très grands nombres, une boucle peut être utilisée (voir Section 2.2). Pour un
boucle de somme, il serait pratique d'utiliser l'instruction ADC pour chaque itération
(au lieu de tous sauf la première itération). Cela peut être fait en utilisant le CLC
(CLear Carry) instruction juste avant le début de la boucle pour initialiser le report
drapeau à 0. Si le drapeau de retenue est 0, il n'y a pas de différence entre l'ADD et
Instructions de l'ADC. La même idée peut également être utilisée pour la soustraction.
2.2 Ouvrages de contrôle
Les langages de haut niveau fournissent des structures de contrôle de haut niveau (par exemple, le if
et while) qui contrôlent le thread d'exécution. Le langage d'assemblage ne fournit pas de structures
de contrôle aussi complexes. Il utilise à la place le
goto infâme et utilisé de manière inappropriée peut entraîner un code spaghetti ! Cependant, il est
possible d'écrire des programmes structurés en langage assembleur. Le
procédure de base consiste à concevoir la logique du programme en utilisant le haut niveau familier
contrôler les structures et traduire la conception dans l'assemblage approprié
langage (un peu comme le ferait un compilateur).
2.2.1 Comparaisons
Les structures de contrôle décident quoi faire sur la base de comparaisons de données. Dans
l'assemblage, le résultat d'une comparaison est stocké dans le registre FLAGS pour être
Machine Translated by Google
38 CHAPITRE 2. LANGAGE DE BASE DE L'ASSEMBLAGE
utilisé plus tard. Le 80x86 fournit l'instruction CMP pour effectuer des comparaisons.
Le registre FLAGS est défini sur la base de la différence des deux opérandes de l'instruction
CMP. Les opérandes sont soustraits et les FLAGS sont définis en fonction du résultat, mais
le résultat n'est stocké nulle part. Si vous avez besoin du résultat, utilisez SUB au lieu de
l'instruction CMP.
Pour les entiers non signés, il y a deux drapeaux (bits dans le registre FLAGS) qui sont
importants : les drapeaux zéro (ZF) et retenue (CF). Le drapeau zéro est mis (1) si la
différence résultante serait nulle. Le drapeau de retenue est utilisé comme drapeau
d'emprunt pour la soustraction. Prenons une comparaison comme :
cmp vgauche, vdroite
La différence vleft vright est calculée et les drapeaux sont définis en conséquence. Si la
différence de CMP est nulle, vleft = vright, alors ZF est activé (c'estàdire 1) et le CF est
désactivé (c'estàdire 0). Si vleft > vright, alors ZF est désactivé et CF est désactivé (pas
d'emprunt). Si vleft < vright, alors ZF est désactivé et CF est activé (emprunter).
Pour les entiers signés, trois drapeaux sont importants : le drapeau zéro Pourquoi SF
= OF si (ZF), le drapeau de débordement (OF) et le drapeau de signe (SF). Le drapeau de débordement vleft > vright ? S'il
est défini si le résultat d'une opération déborde (ou déborde). L'indicateur de signe n'est pas un débordement, puis est
activé si le résultat d'une opération est négatif. Si vleft = vright, la différence ZF sera définie (comme pour les entiers non
signés). Si vleft > vright, ZF
est désactivé et la valeur correcte et doit SF = OF. Si vleft < vright, ZF est désactivé et SF =
OF. être non négatif.
Ainsi,
SF = OF = 0. Cependant, N'oubliez pas que d'autres instructions peuvent également modifier le registre FLAGS,
CMP. s'il y a un pas seulement
débordement, la différence
n'aura pas la bonne valeur 2.2.2 Consignes de branche
(et sera en fait négative).
Ainsi, SF = OF = 1. Les instructions de branchement peuvent transférer l'exécution à des points arbitraires
d'un programme. En d'autres termes, ils agissent comme un goto. Il existe deux types de
branches : inconditionnelle et conditionnelle. Une branche inconditionnelle est comme un
goto, elle crée toujours la branche. Un branchement conditionnel peut ou non effectuer le
branchement en fonction des drapeaux dans le registre FLAGS. Si un branchement
conditionnel ne fait pas le branchement, le contrôle passe à l'instruction suivante.
L'instruction JMP (abréviation de saut) crée des branches inconditionnelles. Son
argument unique est généralement une étiquette de code à l'instruction vers laquelle se
brancher. L'assembleur ou l'éditeur de liens remplacera l'étiquette par l'adresse correcte
de l'instruction. C'est une autre des opérations fastidieuses que l'assembleur effectue pour
faciliter la vie du programmeur. Il est important de réaliser que l'instruction immédiatement
après l'instruction JMP ne sera jamais exécutée à moins qu'une autre instruction ne s'y
branche !
Il existe plusieurs variantes de l'instruction de saut :
COURT Ce saut a une portée très limitée. Il ne peut monter ou descendre que de 128
octets en mémoire. L'avantage de ce type est qu'il utilise moins
Machine Translated by Google
2.2. STRUCTURES DE CONTRÔLE 39
JZ branches uniquement si ZF est défini
Branches JNZ uniquement si ZF n'est pas défini
JO bifurque uniquement si OF est défini
Branchements JNO uniquement si OF n'est pas défini
Branches JS uniquement si SF est défini
Branches JNS uniquement si SF n'est pas défini
JC branches uniquement si CF est défini
Branches JNC uniquement si CF n'est pas défini
JP branches uniquement si PF est défini
Branches JNP uniquement si PF n'est pas défini
Tableau 2.3 : Branchements conditionnels simples
mémoire que les autres. Il utilise un seul octet signé pour stocker le déplacement du saut. Le
déplacement est le nombre d'octets à déplacer en avant ou en arrière. (Le déplacement est ajouté à
EIP). Pour spécifier un saut court, utilisez le motclé SHORT juste avant l'étiquette dans l'instruction
JMP.
NEAR Ce saut est le type par défaut pour les branches inconditionnelles et conditionnelles, il peut être utilisé
pour sauter à n'importe quel emplacement dans un segment. En fait, le 80386 prend en charge deux
types de sauts rapprochés. On utilise deux octets pour le déplacement. Cela permet de monter ou
descendre d'environ 32 000 octets. L'autre type utilise quatre octets pour le déplacement, ce qui permet
bien sûr de se déplacer vers n'importe quel emplacement dans le segment de code. Le type à quatre
octets est la valeur par défaut en mode protégé 386. Le type à deux octets peut être spécifié en plaçant
le motclé WORD avant l'étiquette dans l'instruction JMP.
FAR Ce saut permet au contrôle de passer à un autre segment de code. C'est un
chose très rare à faire en mode protégé 386.
Les étiquettes de code valides suivent les mêmes règles que les étiquettes de données. Les étiquettes de
code sont définies en les plaçant dans le segment de code devant l'instruction qu'elles étiquettent. Un deux
points est placé à la fin de l'étiquette à son point de définition. Le côlon ne fait pas partie du nom.
Il existe de nombreuses instructions de branchement conditionnel différentes. Ils prennent également une
étiquette de code comme opérande unique. Les plus simples regardent juste un seul indicateur dans le registre
FLAGS pour déterminer s'il faut brancher ou non.
Voir le tableau 2.3 pour une liste de ces instructions. (PF est le drapeau de parité qui indique l'impair ou l'égalité
du nombre de bits définis dans les 8 bits inférieurs du résultat.)
Le pseudocode suivant :
Machine Translated by Google
40 CHAPITRE 2. LANGAGE DE BASE DE L'ASSEMBLAGE
si ( EAX == 0 )
EBX = 1 ;
autre
EBX = 2 ;
pourrait s'écrire en assembleur comme suit :
D'autres comparaisons ne sont pas si faciles en utilisant les branches conditionnelles dans
Tableau 2.3. Pour illustrer, considérons le pseudocode suivant :
si ( EAX >= 5 )
EBX = 1 ;
autre
EBX = 2 ;
Si EAX est supérieur ou égal à cinq, le ZF peut être activé ou désactivé et
SF sera égal à OF. Voici le code d'assemblage qui teste ces conditions
(en supposant que EAX est signé):
1 cmp eax, 5
2 js se connecter ; goto signon si SF = 1
3 jo bloc d'autre ; goto elseblock si OF = 1 et SF = 0
4 jmp puis bloquer ; goto thenblock si SF = 0 et OF = 0
5 code d'accès :
6 jo puis bloquer ; goto thenblock si SF = 1 et OF = 1
7 bloc d'autre:
8 mouvement ebx, 2
9 jmp suivant
10 puis bloquer :
11 mouvement ebx, 1
12 suivant :
Le code cidessus est très maladroit. Heureusement, le 80x86 fournit des instructions de
branchement supplémentaires pour rendre ce type de tests beaucoup plus facile. Là
sont des versions signées et non signées de chacun. Le tableau 2.4 montre ces instructions. Les
branches égales et non égales (JE et JNE) sont les mêmes pour
entiers signés et non signés. (En fait, JE et JNE sont vraiment identiques
Machine Translated by Google
2.2. STRUCTURES DE CONTRÔLE 41
Signé Non signé
JE branches si vleft = vright JE branches si vleft = vright
Branches JNE si vleft = vright Branches JNE si vleft = vright
JL, JNGE branches si vleft < vright JB, JNAE branches si vleft < vright
Branches JLE, JNG si vleft ≤ vright Branches JBE, JNA si vleft ≤ vright
Branches JG, JNLE si vleft > vright Branches JA, JNBE si vleft > vright
Branches JGE, JNL si vleft ≥ vright Branches JAE, JNB si vleft ≥ vright
Tableau 2.4 : Instructions de comparaison signées et non signées
à JZ et JNZ, respectivement.) Chacune des autres instructions de branche a
deux synonymes. Par exemple, regardez JL (saut inférieur à) et JNGE (saut
pas supérieur ou égal à). Ce sont les mêmes instructions car :
x < y = non(x ≥ y)
Les branches non signées utilisent A pour dessus et B pour dessous au lieu de L et G.
En utilisant ces nouvelles instructions de branchement, le pseudocode cidessus peut être
traduit à l'assemblage beaucoup plus facile.
1 cmp eax, 5
2 jge puis bloquer
3 mouvement ebx, 2
4 jmp suivant
5 puis bloquer :
6 mouvement ebx, 1
7 suivant :
2.2.3 Les instructions de boucle
Le 80x86 fournit plusieurs instructions conçues pour implémenter pour like
boucles. Chacune de ces instructions prend une étiquette de code comme opérande unique.
LOOP Décrémente ECX, si ECX = 0, bifurque vers l'étiquette
LOOPE, LOOPZ Décrémente ECX (le registre FLAGS n'est pas modifié), si
ECX = 0 et ZF = 1, branches
LOOPNE, LOOPNZ Décrémente ECX (FLAGS inchangé), si ECX =
0 et ZF = 0, branches
Les deux dernières instructions de boucle sont utiles pour les boucles de recherche séquentielles. Le
pseudocode suivant :
Machine Translated by Google
42 CHAPITRE 2. LANGAGE DE BASE DE L'ASSEMBLAGE
somme = 0 ;
pour( i=10; i >0; i−− )
somme += je ;
pourrait être traduit en assemblage par :
2.3 Traduction des structures de contrôle standard
Cette section examine comment les structures de contrôle standard de haut niveau
les langages peuvent être implémentés en langage assembleur.
2.3.1 Si les instructions
Le pseudocode suivant :
si (état)
puis bloquer ;
autre
bloc d'autre ;
pourrait être implémenté comme suit :
1 ; code pour définir FLAGS
2 jxx else_block ; sélectionnez xx pour que les branches si la condition est fausse
3 ; code pour puis bloquer
4 jmp endif
5 else_block :
6 ; code pour le bloc d'autre
7 finif :
S'il n'y a pas d'autre, alors la branche de bloc else peut être remplacée par un
branche à endif.
1 ; code pour définir FLAGS
2 jxx endif ; code ; sélectionnez xx pour que les branches si la condition est fausse
3 pour puis bloquer
4 finif :
Machine Translated by Google
2.4. EXEMPLE : TROUVER DES NOMBRES PREMIERS 43
2.3.2 Boucles While
La boucle while est une boucle testée en haut :
tandis que ( condition) {
corps de boucle ;
}
Cela pourrait se traduire par :
1 pendant que :
2 ; code pour définir FLAGS en fonction de la condition
3 jxx ; en attendant ; sélectionnez xx pour que les branches soient fausses
4 corps de boucle
5 jmp tandis que
6 pendant ce temps :
2.3.3 Faire des boucles tant que
La boucle do while est une boucle testée par le bas :
faire {
corps de boucle ;
} tandis que ( condition);
Cela pourrait se traduire par :
1 faire :
2 ; corps de boucle
3 ; code pour définir FLAGS en fonction de la condition
4 faire
jxx ; sélectionnez
xx pour que les branches soient vraies
2.4 Exemple : Trouver des nombres premiers
Cette section examine un programme qui trouve des nombres premiers. Rappeler que
les nombres premiers sont divisibles par seulement 1 et euxmêmes. Il n'y a pas
formule pour ce faire. La méthode de base utilisée par ce programme est de trouver le
facteurs de tous les nombres impairs3 inférieurs à une limite donnée. Si aucun facteur ne peut être trouvé pour
un nombre impair, il est premier. La figure 2.3 montre l'algorithme de base écrit en
C
Voici la version d'assemblage :
3
2 est le seul nombre premier pair.
Machine Translated by Google
44 CHAPITRE 2. LANGAGE DE BASE DE L'ASSEMBLAGE
1 supposition non signée ; / estimation actuelle pour premier /
2 facteur non signé ; / facteur de conjecture possible /
3 limite non signée ; / trouve les nombres premiers jusqu'à cette valeur /
4
5 printf(”Trouver les nombres premiers jusqu'à : ”);
6 scanf(”%u”, &limit);
7 printf ("2\n"); / traite les deux premiers nombres premiers comme /
8 printf ("3\n"); / cas particulier /
9 deviner = 5 ; / estimation initiale /
10 tandis que (supposez <= limite) {
11 / cherche un facteur de conjecture /
12 facteur = 3 ;
13 tandis que (facteur facteur < deviner &&
14 deviner % facteur != 0 )
15 facteur += 2 ;
16 si ( devinez % facteur != 0 )
17 printf ("%d\n", devinez);
18 devinez += 2 ; / ne regarde que les nombres impairs /
19 }
Figure 2.3 :
premier.asm
1 %inclut "asm_io.inc"
2 segments .données
3 Message db "Trouver les nombres premiers jusqu'à : ", 0
4
5 segments .bss
6 Limite resd 1 ; trouver les nombres premiers jusqu'à cette limite
7 Devinez resd 1 ; la conjecture actuelle pour premier
8
9 segments .text
dix global_asm_main
11 _asm_main :
12 entrez 0,0 pusha ; routine de configuration
13
14
15 mouvement
eax, Message
16 appel print_string
17 appel read_int ; scanf("%u", & limite );
18 mouvement [Limite], eax
19
Machine Translated by Google
2.4. EXEMPLE : TROUVER DES NOMBRES PREMIERS 45
52 appel print_nl
53 end_if :
54 ajouter dword [Deviner], 2 ; devinez += 2
55 jmp while_limit
56 end_while_limit :
57
58 papa
59 mouvement eax, 0 ; revenir à C
60 partir
61 ret
premier.asm
Machine Translated by Google
46 CHAPITRE 2. LANGAGE DE BASE DE L'ASSEMBLAGE
Machine Translated by Google
chapitre 3
Opérations sur les bits
3.1 Opérations de quart
Le langage d'assemblage permet au programmeur de manipuler l'individu
bits de données. Une opération de bit commune est appelée un décalage. Une opération de décalage
déplace la position des bits de certaines données. Les changements peuvent être soit vers
vers la gauche (c'estàdire vers les bits les plus significatifs) ou vers la droite (les moins
bits significatifs).
3.1.1 Changements logiques
Un décalage logique est le type de décalage le plus simple. Il se déplace d'une manière très
simple. La figure 3.1 montre un exemple d'un nombre décalé d'un seul octet.
Original 1 1 1 0 1 0 1 0
Décalage à gauche 1 1 0 1 0 1 0 0
Décalage à droite 0 1 1 1 0 1 0 1
Figure 3.1 : Décalages logiques
Notez que les nouveaux bits entrants sont toujours zéro. Les instructions SHL et SHR sont
utilisées pour effectuer respectivement des décalages logiques à gauche et à droite. Ces
les instructions permettent de se déplacer de n'importe quel nombre de positions. Le nombre de
les positions à décaler peuvent être soit une constante, soit être stockées dans le registre CL.
Le dernier bit décalé des données est stocké dans le drapeau de retenue. Voilà quelque
exemples de codes :
1 mouvement hache, 0C123H
2 shl hache, ; décaler 1 bit vers la gauche, ax = 8246H, CF = 1
3 shr 1 hache, ; décaler 1 bit vers la droite, ax = 4123H, CF = 0
4 shr 1 hache, ; décaler 1 bit vers la droite, ax = 2091H, CF = 1
5 mouvement 1 hache, 0C123H
47
Machine Translated by Google
48 CHAPITRE 3. OPÉRATIONS BIT
3.1.2 Utilisation des équipes
La multiplication et la division rapides sont les utilisations les plus courantes d'un décalage
opérations. Rappelons que dans le système décimal, la multiplication et la division
par une puissance de dix sont simples, il suffit de décaler les chiffres. Il en est de même pour les puissances
de deux en binaire. Par exemple, pour doubler le nombre binaire 10112 (ou 11
en décimal), décaler une fois vers la gauche pour obtenir 101102 (ou 22). Le quotient d'un
la division par une puissance de deux est le résultat d'un décalage vers la droite. Pour diviser par seulement 2,
utiliser un seul décalage vers la droite ; diviser par 4 (22 ), décaler à droite de 2 places ; diviser par
8 (23 ), décalage de 3 positions vers la droite, etc. Les instructions de décalage sont très basiques et
sont beaucoup plus rapides que les instructions MUL et DIV correspondantes !
En fait, les décalages logiques peuvent être utilisés pour multiplier et diviser des valeurs non
signées. Ils ne fonctionnent généralement pas pour les valeurs signées. Considérez les 2 octets
valeur FFFF (signé 1). S'il est logiquement décalé à droite une fois, le résultat est
7FFF soit +32, 767 ! Un autre type de décalage peut être utilisé pour les valeurs signées.
3.1.3 Décalages arithmétiques
Ces décalages sont conçus pour permettre aux nombres signés d'être rapidement multipliés et
divisés par des puissances de 2. Ils assurent que le bit de signe est traité
correctement.
SAL Shift Arithmetic Left Cette instruction est juste un synonyme de SHL. Il
est assemblé dans exactement le même code machine que SHL. Aussi long
comme le bit de signe n'est pas modifié par le décalage, le résultat sera correct.
SAR Shift Arithmetic Right Il s'agit d'une nouvelle instruction qui ne change pas
le bit de signe (c'estàdire le msb) de son opérande. Les autres bits sont décalés
comme d'habitude sauf que les nouveaux bits qui entrent par la gauche sont des copies
du bit de signe (c'estàdire que si le bit de signe est 1, les nouveaux bits sont également 1).
Ainsi, si un octet est décalé avec cette instruction, seuls les 7 bits inférieurs
sont décalés. Comme pour les autres décalages, le dernier bit décalé est stocké dans
le drapeau porteur.
1 mouvement hache, 0C123H
2 sel hache, 1 ; ax = 8246H, CF = 1
3 sel hache, 1 ; ax = 048CH, CF = 1
4 Sar hache, 2 ; ax = 0123H, CF = 0
Machine Translated by Google
3.1. OPÉRATIONS DE QUART 49
3.1.4 Rotation des équipes
Les instructions de décalage de rotation fonctionnent comme des décalages logiques sauf que les bits perdus
d'une extrémité des données sont décalées de l'autre côté. Ainsi, les données sont
traité comme s'il s'agissait d'une structure circulaire. Les deux instructions de rotation les plus simples
sont ROL et ROR qui effectuent respectivement des rotations à gauche et à droite. Tout comme
pour les autres décalages, ces décalages laissent la copie du dernier bit décalée
dans le drapeau de portage.
1 mouvement hache, 0C123H
2 rôle hache, 1 ; ax = 8247H, CF = 1
3 rôle hache, 1 ; ax = 048FH, CF = 1
4 rôle hache, 1 ; ax = 091EH, CF = 0
5 ror hache, 2 ; ax = 8247H, CF = 1
6 ror haches, 1 ; ax = C123H, CF = 1
Il existe deux instructions de rotation supplémentaires qui décalent les bits dans le
données et le drapeau de report nommé RCL et RCR. Par exemple, si le registre AX
est tourné avec ces instructions, les 17 bits constitués de AX et de la retenue
drapeau sont tournés.
1 mouvement hache, 0C123H
2 cc ; effacer le drapeau de retenue (CF = 0)
3 RCL hache, 1 ; ax = 8246H, CF = 1
4 RCL hache, 1 ; ax = 048DH, CF = 1
5 RCL hache, 1 ; ax = 091BH, CF = 0
6 RCR hache, 2 ; ax = 8246H, CF = 1
7 RCR haches, 1 ; ax = C123H, CF = 0
3.1.5 Application simplifiée
Voici un extrait de code qui compte le nombre de bits "activés"
(c'estàdire 1) dans le registre EAX.
50 CHAPITRE 3. OPÉRATIONS BIT
XYX ET Y
0 0 0
0 1 0
dix 0
1 1 1
Tableau 3.1 : L'opération ET
1 0 1 0 1 0 1 0
ET 1 1 0 0 1 0 0 1
1 0 0 0 1 0 0 0
Figure 3.2 : AND sur un octet
Le code cidessus détruit la valeur d'origine de EAX (EAX vaut zéro à la fin de
la boucle). Si l'on souhaitait conserver la valeur de EAX, la ligne 4 pourrait être remplacée
avec rol eax, 1.
3.2 Opérations booléennes au niveau des bits
Il existe quatre opérateurs booléens courants : AND, OR, XOR et NOT.
Une table de vérité montre le résultat de chaque opération pour chaque valeur possible de
ses opérandes.
3.2.1 L'opération ET
Le résultat du ET de deux bits n'est 1 que si les deux bits valent 1, sinon le
résultat est 0 comme le montre la table de vérité du tableau 3.1.
Les processeurs supportent ces opérations comme des instructions qui agissent
indépendamment sur tous les bits de données en parallèle. Par exemple, si le contenu de AL
et BL sont combinés en ET, l'opération ET de base est appliquée à chacun des
les 8 paires de bits correspondants dans les deux registres comme le montre la figure 3.2.
Cidessous un exemple de code :
1 mouvement hache, 0C123H
2 et hache, 82F6H ; hache = 8022H
3.2.2 L'opération OU
Le OU inclusif de 2 bits est 0 uniquement si les deux bits sont 0, sinon le résultat est
1 comme le montre la table de vérité du tableau 3.2. Cidessous un exemple de code :
1 mouvement hache, 0C123H
2 ou hache, 0E831H ; hache = E933H
Machine Translated by Google
3.2. OPÉRATIONS BOOLÉENNES BITWISE 51
XYX OU Y
0 0 0
0 1 1
dix 1
1 1 1
Tableau 3.2 : L'opération OU
XYX XOR Y
0 0 0
0 1 1
dix 1
1 1 0
Tableau 3.3 : L'opération XOR
3.2.3 L'opération XOR
Le OU exclusif de 2 bits est 0 si et seulement si les deux bits sont égaux, sinon le
le résultat est 1 comme le montre la table de vérité du tableau 3.3. Cidessous un exemple de code :
1 mouvement hache, 0C123H
2 xor hache, 0E831H ; hache = 2912H
3.2.4 L'opération NON
L'opération NOT est une opération unaire (c'estàdire qu'elle agit sur un opérande,
pas deux comme les opérations binaires telles que ET). Le PAS d'un peu est le
valeur opposée du bit comme le montre la table de vérité du tableau 3.4. Cidessous un
exemple de code :
1 mouvement hache, 0C123H
2 pas hache ; hache = 3EDCH
Notez que le NOT trouve le complément à un. Contrairement à l'autre au niveau du bit
opérations, l'instruction NOT ne modifie aucun des bits de FLAGS
enregistrer.
3.2.5 La commande TEST
L'instruction TEST effectue une opération ET, mais ne stocke pas
le résultat. Il définit uniquement le registre FLAGS en fonction de ce que le résultat serait
être (un peu comme la façon dont l'instruction CMP effectue une soustraction mais ne définit que
DRAPEAUX). Par exemple, si le résultat était zéro, ZF serait défini.
Machine Translated by Google
52 CHAPITRE 3. OPÉRATIONS BIT
X PAS X
0 1
1 0
Tableau 3.4 : L'opération NON
Allumez le bit i OU le nombre avec 2i (qui est le binaire
nombre avec juste le bit i allumé)
Éteignez le bit i ET le nombre avec le nombre binaire
avec seulement peu de temps. Cet opérande est souvent
appelé un masque
Complément bit i XOR le nombre avec 2i
Tableau 3.5 : Utilisations des opérations booléennes
3.2.6 Utilisations des opérations sur les bits
Les opérations sur les bits sont très utiles pour manipuler des bits de données individuels
sans modifier les autres bits. Le tableau 3.5 montre trois utilisations courantes de
ces opérations. Vous trouverez cidessous un exemple de code mettant en œuvre ces idées.
1 mouvement hache, 0C123H
2 ou hache, ; activer le bit 3, ax = C12BH
3 et 8 haches, ; désactiver le bit 5, ax = C10BH
4 xor 0FFDFH ; inverser le bit 31, ax = 410BH
5 ou hache, 8000H ; activer le quartet, ax = 4F0BH
6 et hache, 0F00H ; désactiver le grignotage, ax = 4F00H
7 xor hache, 0FFF0H ; inverser les quartets, ax = BF0FH
8 xor ; complément à 1, ax = 40F0H
hache, 0F00FH hache, 0FFFFH
L'opération ET peut également être utilisée pour trouver le reste d'une division
par une puissance de deux. Pour trouver le reste d'une division par 2i , Et le
nombre avec un masque égal à 2i − 1. Ce masque contiendra les uns du bit 0
jusqu'au bit i − 1. Ce sont justement ces bits qui contiennent le reste. Le résultat
de l'AND gardera ces bits et mettra les autres à zéro. Vient ensuite un extrait
de code qui trouve le quotient et le reste de la division de 100 par 16.
En utilisant le registre CL, il est possible de modifier des bits arbitraires de données. Suivant est
un exemple qui définit (active) un bit arbitraire dans EAX. Le numéro de la
bit à définir est stocké dans BH.
Machine Translated by Google
3.3. ÉVITER LES BRANCHES CONDITIONNELLES 53
Figure 3.3 : Comptage de bits avec ADC
Éteindre un peu est juste un peu plus difficile.
Le code pour compléter un bit arbitraire est laissé en exercice au lecteur.
Il n'est pas rare de voir l'instruction déroutante suivante dans un 80x86
programme:
Un nombre XOR avec luimême donne toujours zéro. Cette consigne est utilisée
car son code machine est plus petit que l'instruction MOV correspondante.
3.3 Éviter les branchements conditionnels
Les processeurs modernes utilisent des techniques très sophistiquées pour exécuter le code comme
Aussi vite que possible. Une technique courante est connue sous le nom d'exécution spéculative.
Cette technique utilise les capacités de traitement parallèle du processeur pour
exécuter plusieurs instructions à la fois. Les branches conditionnelles présentent un problème
avec cette idée. Le processeur, en général, ne sait pas si le
branche sera prise ou non. S'il est pris, un ensemble différent d'instructions sera
être exécuté que s'il n'est pas pris. Les processeurs tentent de prédire si le
branche sera prise. Si la prédiction est erronée, le processeur a gaspillé
il est temps d'exécuter le mauvais code.
Machine Translated by Google
54 CHAPITRE 3. OPÉRATIONS BIT
Une façon d'éviter ce problème est d'éviter d'utiliser des branches conditionnelles
quand c'est possible. L'exemple de code dans 3.1.5 fournit un exemple simple d'où
on pourrait faire ça. Dans l'exemple précédent, les bits "on" du registre EAX
sont comptés. Il utilise une branche pour ignorer l'instruction INC. La figure 3.3 montre
comment la branche peut être supprimée en utilisant l'instruction ADC pour ajouter le
porter le drapeau directement.
Les instructions SETxx permettent de supprimer des branches dans certains
cas. Ces instructions définissent la valeur d'un registre d'octets ou d'un emplacement mémoire
à zéro ou à un en fonction de l'état du registre FLAGS. Les personnages
après SET sont les mêmes caractères utilisés pour les branches conditionnelles. Si la
condition correspondante du SETxx est vrai, le résultat stocké est un, si
false un zéro est stocké. Par exemple,
setz al ; AL = 1 si le drapeau Z est défini, sinon 0
En utilisant ces instructions, on peut développer des techniques astucieuses qui calculent des
valeurs sans branches.
Par exemple, considérons le problème de trouver le maximum de deux valeurs.
L'approche standard pour résoudre ce problème serait d'utiliser un CMP et d'utiliser
une branche conditionnelle pour agir sur laquelle la valeur était plus grande. Le programme exemple
cidessous montre comment le maximum peut être trouvé sans aucune branche.
1 ; fichier : max.asm
2 % incluent "asm_io.inc"
Données à 3 segments
4
5 message1 db "Entrez un numéro : ",0
6 message2 db "Entrez un autre numéro : ", 0
7 message3 db "Le plus grand nombre est : ", 0
8
9 segments .bss
dix
11 entrée1 resd 1 ; premier numéro saisi
12
13 segments .text
14 global_asm_main
15 _asm_main :
16 entrez 0,0 pusha ; routine de configuration
17
18
19 mouvement
eax, message1 ; imprimer le premier message
20 appel print_string
21 appel read_int ; saisir le premier chiffre
Machine Translated by Google
3.3. ÉVITER LES BRANCHES CONDITIONNELLES 55
22 mouvement
[entrée1], eax
23
24 mouvement
eax, message2 ; imprimer le deuxième message
25 appel print_string
26 appel read_int ; saisir le deuxième chiffre (en eax)
27
38 mouvement
eax, message3 ; imprimer le résultat
39 appel print_string
40 mouvement eax, ecx
41 appel print_int
42 appel print_nl
43
44 papa
45 mouvement eax, 0 ; revenir à C
46 partir
47 ret
L'astuce consiste à créer un masque de bits qui peut être utilisé pour sélectionner le bon
valeur pour le maximum. L'instruction SETG de la ligne 30 met BL à 1 si le
la deuxième entrée est le maximum ou 0 sinon. Ce n'est pas tout à fait le masque de bit
voulu. Pour créer le masque de bits requis, la ligne 31 utilise l'instruction NEG
sur l'ensemble du registre EBX. (Notez que EBX a été mis à zéro plus tôt.) Si
EBX vaut 0, cela ne fait rien ; cependant, si EBX vaut 1, le résultat est les deux
représentation complémentaire de 1 ou 0xFFFFFFFF. C'est juste le masque de bits
requis. Le code restant utilise ce masque de bits pour sélectionner l'entrée correcte
comme maximum.
Une autre astuce consiste à utiliser l'instruction DEC. Dans le code cidessus, si le
NEG est remplacé par un DEC, encore une fois le résultat sera 0 ou 0xFFFFFFFF.
Cependant, les valeurs sont inversées par rapport à l'utilisation de l'instruction NEG.
Machine Translated by Google
56 CHAPITRE 3. OPÉRATIONS BIT
3.4 Manipulation de bits en C
3.4.1 Les opérateurs bit à bit de C
Contrairement à certains langages de haut niveau, C fournit des opérateurs pour bitwise
opérations. L'opération AND est représentée par l'opérateur binaire &1 .
L'opération OU est représentée par le binaire | opérateur. L'opération XOR est représentée par
l'opérateur binaire ^. Et l'opération NOT est
représenté par l'opérateur unaire ~.
Les opérations de décalage sont effectuées par les opérateurs binaires << et >> de C.
L'opérateur << effectue des décalages vers la gauche et l'opérateur >> effectue des décalages vers la droite
changements. Ces opérateurs prennent deux opérandes. L'opérande de gauche est la valeur à
décalage et l'opérande de droite est le nombre de bits à décaler. Si la valeur
to shift est un type non signé, un décalage logique est effectué. Si la valeur est un signe
type (comme int), alors un décalage arithmétique est utilisé. Cidessous quelques exemples C
code utilisant ces opérateurs :
1 entier court s ; 2 / supposons que short int est 16−bit /
u courts non signés ;
3 s = 1 ; 4u / s = 0xFFFF (complément à 2) /
= 100 ; 5 u = u / u = 0x0064 /
| 0x0100 ; 6 s = s & / u = 0x0164 /
0xFFF0 ; / s = 0xFFF0 /
ˆ
7 s = s tu ; / s = 0xFE94 /
8 u = u << 3; 9 s = s / u = 0x0B20 (décalage logique ) /
>> 2 ; / s = 0xFFA5 (décalage arithmétique ) /
3.4.2 Utiliser les opérateurs bit à bit en C
Les opérateurs au niveau du bit sont utilisés en C aux mêmes fins qu'ils le sont
utilisé en langage assembleur. Ils permettent de manipuler des morceaux individuels de
données et peut être utilisé pour la multiplication et la division rapides. En fait, une puce
Le compilateur C utilisera automatiquement un décalage pour une multiplication telle que x *= 2.
3
De nombreux systèmes d'exploitation API2 (tels que POSIX et Win32) contiennent
fonctions qui utilisent des opérandes dont les données sont codées sous forme de bits. Par exemple,
Les systèmes POSIX conservent les autorisations de fichiers pour trois types d'utilisateurs différents :
user (un meilleur nom serait owner ), group et autres. Chaque type de
l'utilisateur peut être autorisé à lire, écrire et/ou exécuter un fichier. Pour
changer les permissions d'un fichier nécessite que le programmeur C manipule
bits individuels. POSIX définit plusieurs macros pour vous aider (voir Tableau 3.6). Le
1Cet opérateur est différent des opérateurs binaires && et unaires & !
2Interface de programmation d'applications
3
signifie Interface de système d'exploitation portable pour les environnements informatiques. Une
norme développée par l'IEEE basée sur UNIX.
Machine Translated by Google
3.5. REPRÉSENTATIONS BIG ET LITTLE ENDIAN 57
Macro Signification
L'utilisateur S IRUSR peut lire
L'utilisateur S IWUSR peut écrire
L'utilisateur S IXUSR peut exécuter
Le groupe S IRGRP peut lire
Le groupe S IWGRP peut écrire
Le groupe S IXGRP peut exécuter
S IROTH que les autres peuvent lire
S IWOTH autres peuvent écrire
S IXOTH autres peuvent exécuter
Tableau 3.6 : Macros d'autorisation de fichier POSIX
La fonction chmod peut être utilisée pour définir les autorisations du fichier. Cette fonction prend deux
paramètres, une chaîne avec le nom du fichier sur lequel agir et un entier4 avec les bits appropriés définis
pour les autorisations souhaitées. Par exemple, le code cidessous définit les autorisations pour permettre
au propriétaire du fichier de le lire et d'y écrire, aux utilisateurs du groupe de lire le fichier et les autres n'y
ont pas accès.
chmod(”foo”, S IRUSR | S IWUSR | S IRGRP );
La fonction POSIX stat peut être utilisée pour connaître les bits d'autorisation actuels pour le fichier.
Utilisé avec la fonction chmod, il est possible de modifier certaines permissions sans en changer d'autres.
Voici un exemple qui supprime l'accès en écriture aux autres et ajoute l'accès en lecture au propriétaire
du fichier. Les autres autorisations ne sont pas modifiées.
3.5 Représentations Big et Little Endian
Le chapitre 1 a introduit le concept de représentations big et little endian de données multioctets.
Cependant, l'auteur a constaté que ce sujet confond beaucoup de gens. Cette section couvre le sujet plus
en détail.
Le lecteur se souviendra que l'endianité fait référence à l'ordre dans lequel les octets individuels (et
non les bits) d'un élément de données multioctets sont stockés en mémoire.
Le big endian est la méthode la plus simple. Il stocke d'abord l'octet le plus significatif, puis l'octet
significatif suivant et ainsi de suite. En d'autres termes, les gros bits sont stockés en premier. Little Endian
stocke les octets à l'opposé
4En fait un paramètre de type mode t qui est un typedef vers un type intégral.
Machine Translated by Google
58 CHAPITRE 3. OPÉRATIONS BIT
if ( p[0] == 0x12 ) printf
("Big Endian Machine\n"); sinon printf
("Machine Little Endian\n");
Figure 3.4 : Comment déterminer l'endianité
ordre (le moins significatif en premier). La famille de processeurs x86 utilise une représentation little
endian.
Par exemple, considérons le mot double représentant 1234567816. Dans la représentation big
endian, les octets seraient stockés sous la forme 12 34 56 78. Dans la représentation little endian,
les octets seraient stockés sous la forme 78 56 34 12.
Le lecteur se demande probablement en ce moment pourquoi un concepteur de puces sensé
utiliserait une représentation little endian ? Les ingénieurs d'Intel étaientils des sadiques pour avoir
infligé ces représentations déroutantes à des multitudes de programmeurs ? Il semblerait que le
processeur doive faire un travail supplémentaire pour stocker les octets en arrière dans la mémoire
comme celuici (et pour les annuler lors de la lecture en mémoire). La réponse est que le processeur
ne fait aucun travail supplémentaire pour écrire et lire la mémoire en utilisant le format Little Endian.
Il faut se rendre compte que le CPU est composé de nombreux circuits électroniques qui fonctionnent
simplement sur des valeurs binaires. Les bits (et les octets) ne sont pas dans l'ordre nécessaire
dans le CPU.
Considérez le registre AX à 2 octets. Il peut être décomposé en registres à un seul octet : AH
et AL. Il y a des circuits dans le CPU qui maintiennent les valeurs de AH et AL. Les circuits ne sont
dans aucun ordre dans un processeur. Autrement dit, les circuits pour AH ne sont ni avant ni après
les circuits pour AL. Une instruction mov qui copie la valeur de AX dans la mémoire copie la valeur
de AL puis AH. Ce n'est pas plus difficile pour le CPU que de stocker d'abord AH.
Le même argument s'applique aux bits individuels d'un octet. Ils ne sont pas vraiment dans
n'importe quel ordre dans les circuits du CPU (ou de la mémoire d'ailleurs).
Cependant, étant donné que les bits individuels ne peuvent pas être adressés dans le CPU ou la
mémoire, il n'y a aucun moyen de savoir (ou de se soucier) de l'ordre dans lequel ils semblent être
conservés en interne par le CPU.
Le code C de la figure 3.4 montre comment le caractère endian d'un CPU peut être déterminé.
Le pointeur p traite le mot variable comme un tableau de caractères à deux éléments. Ainsi, p[0]
s'évalue au premier octet du mot en mémoire qui dépend de l'endianness du CPU.
Machine Translated by Google
3.5. REPRÉSENTATIONS BIG ET LITTLE ENDIAN 59
1 endian inversé non signé ( x non signé )
2 {
3 inversé non signé ;
4 const caractère non signé xp = (const caractère non signé ) &x;
5 caractère non signé ip = (caractère non signé ) & invert;
6
7 ip [0] = xp [3] ; / inverser les octets individuels /
8 ip [1] = xp [2] ;
9 ip [2] = xp [1] ;
dix ip [3] = xp [0] ;
11
12 retour inverse ; / retourne les octets inversés /
13 }
Figure 3.5 : Fonction Invert Endian
3.5.1 Quand se soucier de Little et Big Endian
Pour une programmation typique, l'endianité du processeur n'est pas significative.
Le moment le plus courant où cela est important est lorsque des données binaires sont
transférées entre différents systèmes informatiques. Il s'agit généralement soit d'utiliser certains
type de support de données physique (tel qu'un disque) ou un réseau. Depuis les données ASCII Avec l'avènement des jeux de
est un octet, l'endianness n'est pas un problème pour lui. caractères multioctets, comme
Tous les entêtes TCP/IP internes stockent des entiers au format big endian (appelé UNICODE, l'endianness est
ordre des octets du réseau). Les bibliothèques TCP/IP fournissent des fonctions C pour traiter important pour un texte pair
données. UNICODE prend en charge
problèmes d'endianness de manière portable. Par exemple, la fonction htonl() convertit un mot
soit endianness et a
double (ou un entier long) du format hôte au format réseau. Le
un mécanisme de spécification
ntohl () effectue la transformation inverse.5 Pour un big endian quelle endianité est en train d'être
système, les deux fonctions renvoient simplement leur entrée inchangée. Ceci permet utilisé pour représenter les données.
un pour écrire des programmes réseau qui se compileront et s'exécuteront correctement sur n'importe quel
système indépendamment de son endianness. Pour plus d'informations sur l'endi anness et la
programmation réseau, voir l'excellent livre de W. Richard Steven,
Programmation réseau UNIX.
La figure 3.5 montre une fonction C qui inverse l'endianness d'un double
mot. Le processeur 486 a introduit une nouvelle instruction machine nommée BSWAP
qui inverse les octets de n'importe quel registre 32 bits. Par exemple,
bswapedx ; échanger des octets d'edx
L'instruction ne peut pas être utilisée sur des registres 16 bits. Cependant, le XCHG
5En fait, inverser l'endianité d'un entier inverse simplement les octets ; ainsi, passer de grand à
petit ou de petit à grand est la même opération. Donc ces deux fonctions
faire la même chose.
Machine Translated by Google
60 CHAPITRE 3. OPÉRATIONS BIT
1 nombre entier de bits ( données entières non signées )
2 {
3 entier cnt = 0 ;
4
5 tandis que( données != 0 ) {
6 données = données & (données − 1);
7 cnt++;
8 }
9 retour cnt ;
10 }
Figure 3.6 : Comptage de bits : première méthode
L'instruction peut être utilisée pour échanger les octets des registres 16 bits qui peuvent être
décomposé en registres de 8 bits. Par exemple:
3.6 Comptage des bits
Plus tôt, une technique simple a été donnée pour compter le nombre
de bits qui sont "activés" dans un mot double. Cette section se penche sur d'autres
méthodes pour faire cela comme un exercice en utilisant les opérations sur les bits décrites dans
ce chapitre.
3.6.1 Première méthode
La première méthode est très simple, mais pas évidente. La figure 3.6 montre la
code.
Comment fonctionne cette méthode ? A chaque itération de la boucle, un bit est
désactivé dans les données. Lorsque tous les bits sont désactivés (c'estàdire lorsque les données sont à zéro), le
la boucle s'arrête. Le nombre d'itérations nécessaires pour rendre les données nulles est égal à
le nombre de bits dans la valeur d'origine des données.
La ligne 6 est l'endroit où un peu de données est désactivé. Comment cela marchetil? Considérer
la forme générale de la représentation binaire des données et le 1 le plus à droite
dans cette représentation. Par définition, chaque bit après ce 1 doit être égal à zéro.
Maintenant, quelle sera la représentation binaire des données 1 ? Les bits à la
à gauche du 1 le plus à droite sera le même que pour les données, mais au point de
le plus à droite 1 les bits seront le complément des bits de données d'origine. Pour
exemple:
données = xxxxx10000
données 1 = xxxxx01111
Machine Translated by Google
3.6. COMPTER LES BITS 61
1 nombre de bits d'octets de caractères non signés statiques [256] ; / table de recherche /
2
3 void initialize count bits ()
4 {
5 entier _ , je
, données;
7 pour( je = 0; je < 256; je++ ) {
8 cnt = 0 ;
9 données = je ;
18 bits de comptage int ( données entières non signées )
19 {
20 const caractère non signé octet = ( caractère non signé ) & data;
21
22 renvoie le nombre de bits d'octet [octet [0]] + le nombre de bits d'octet [octet[1]] +
23 nombre de bits d'octet [octet [2]] + nombre de bits d'octet [octet [3]] ;
24 }
Figure 3.7 : Deuxième méthode
où les x sont les mêmes pour les deux nombres. Lorsque les données sont combinées par ET avec
data 1, le résultat mettra à zéro le 1 le plus à droite dans data et laissera tous les autres
bits inchangés.
3.6.2 Deuxième méthode
Une table de recherche peut également être utilisée pour compter les bits d'un double arbitraire
mot. L'approche directe serait de précalculer le nombre
de bits pour chaque mot double et stockezle dans un tableau. Cependant, il y a
deux problèmes liés à cette approche. Il y a environ 4 milliards de doubles
valeurs des mots ! Cela signifie que le tableau sera très grand et que l'initialisation
cela prendra également beaucoup de temps. (En fait, à moins que l'on ne veuille réellement
utiliser le tableau plus de 4 milliards de fois, plus de temps sera nécessaire pour initialiser
le tableau qu'il faudrait pour calculer simplement le nombre de bits en utilisant la méthode
un!)
Machine Translated by Google
62 CHAPITRE 3. OPÉRATIONS BIT
Une méthode plus réaliste précalculerait le nombre de bits pour toutes les valeurs d'octets
possibles et les stockerait dans un tableau. Ensuite, le mot double peut être divisé en valeurs de
quatre octets. Les nombres de bits de ces quatre valeurs d'octets sont recherchés dans le tableau
et additionnés pour trouver le nombre de bits du double mot d'origine. La figure 3.7 montre le
code à implémenter cette approche.
La fonction initialize count bits doit être appelée avant le premier appel à la fonction count
bits. Cette fonction initialise le tableau global de comptage de bits d'octets. La fonction de comptage
de bits considère la variable de données non pas comme un mot double, mais comme un tableau
de quatre octets. Le pointeur dword agit comme un pointeur vers ce tableau de quatre octets.
Ainsi, dword[0] est l'un des octets de données (soit l'octet le moins significatif, soit l'octet le plus
significatif selon que le matériel est petit ou gros boutiste, respectivement.) Bien sûr, on pourrait
utiliser une construction comme :
(données >> 24) & 0x000000FF
trouver la valeur de l'octet le plus significatif et des valeurs similaires pour les autres octets ;
cependant, ces constructions seront plus lentes qu'une référence de tableau.
Un dernier point, une boucle for pourrait facilement être utilisée pour calculer la somme des
lignes 22 et 23. Mais, une boucle for inclurait la surcharge d'initialisation d'un index de boucle, de
comparaison de l'index après chaque itération et d'incrémentation de l'index. Le calcul de la
somme comme la somme explicite de quatre valeurs sera plus rapide.
En fait, un compilateur intelligent convertirait la version de la boucle for en somme explicite. Ce
processus de réduction ou d'élimination des itérations de boucle est une technique d'optimisation
du compilateur connue sous le nom de déroulement de boucle.
3.6.3 Troisième méthode
Il existe encore une autre méthode astucieuse pour compter les bits activés dans les
données. Cette méthode additionne littéralement les uns et les zéros des données.
Cette somme doit être égale au nombre de un dans les données. Par exemple, envisagez de
compter les un dans un octet stocké dans une variable nommée data. La première étape consiste
à effectuer l'opération suivante :
données = (données & 0x55) + ((données >> 1) & 0x55);
Qu'estce que cela fait? La constante hexadécimale 0x55 est 01010101 en binaire. Dans le
premier opérande de l'addition, les données sont combinées par ET avec ceci, les bits aux
positions binaires impaires sont extraits. Le deuxième opérande ((data >> 1) & 0x55) déplace
d'abord tous les bits aux positions paires vers une position impaire et utilise le même masque
pour extraire ces mêmes bits. Maintenant, le premier opérande contient les bits impairs et le
deuxième opérande les bits pairs de données. Lorsque ces deux opérandes sont additionnés, les
bits de données pairs et impairs sont additionnés. Par exemple, si les données sont 101100112,
alors :
Machine Translated by Google
3.6. COMPTER LES BITS 63
1 int count bits (unsigned int x )
2 {
3 masque int non signé statique [] = { 0x55555555,
4 0x33333333,
5 0x0F0F0F0F,
6 0x00FF00FF,
7 0x0000FFFF } ;
8 int je ;
9 décalage entier ; / nombre de positions à décaler vers la droite /
dix
11 for( i=0, shift =1; i < 5; i++, shift = 2 )
12 x = (x & mask[i]) + ( ( x >> shift) & mask[i ] );
13 retourner x ;
14 }
Figure 3.8 : Méthode 3
données & 010101012 00 01 00 01
+ (données >> 1) & 010101012 ou + 01 01 00 01
01 10 00 10
L'addition sur la droite montre les bits réels additionnés. Le
les bits de l'octet sont divisés en quatre champs de 2 bits pour montrer qu'il existe réellement
quatre ajouts indépendants sont effectués. Depuis le plus ces sommes
peut être est deux, il n'y a aucune possibilité que la somme déborde de son champ et
corrompre l'une des sommes de l'autre champ.
Bien sûr, le nombre total de bits n'a pas encore été calculé. Cependant, la même technique
qui a été utilisée cidessus peut être utilisée pour calculer le
total en une série d'étapes similaires. La prochaine étape serait :
données = (données & 0x33) + ((données >> 2) & 0x33);
Poursuivant l'exemple cidessus (rappelezvous que les données sont maintenant 011000102):
données & 001100112 0010 0010
+ (donnée >> 2) & 001100112 ou + 0001 0000
0011 0010
Maintenant, il y a deux champs de 4 bits qui sont ajoutés indépendamment.
L'étape suivante consiste à additionner ces deux sommes de bits pour former le résultat final.
résultat:
données = (données & 0x0F) + ((données >> 4) & 0x0F);
En utilisant l'exemple cidessus (avec des données égales à 001100102) :
données & 000011112 00000010
+ (donnée >> 4) & 000011112 ou + 00000011
00000101
Machine Translated by Google
64 CHAPITRE 3. OPÉRATIONS BIT
Maintenant, les données sont 5, ce qui est le résultat correct. La figure 3.8 montre une implémentation
de cette méthode qui compte les bits dans un double mot. Il utilise un pour
boucle pour calculer la somme. Il serait plus rapide de dérouler la boucle ; Cependant, le
loop permet de mieux comprendre comment la méthode se généralise à différentes tailles de données.
Machine Translated by Google
Chapitre 4
Sousprogrammes
Ce chapitre examine l'utilisation de sousprogrammes pour créer des programmes modulaires et
pour s'interfacer avec des langages de haut niveau (comme C). Les fonctions et procédures sont
exemples de langage de haut niveau de sousprogrammes.
Le code qui appelle un sousprogramme et le sousprogramme luimême doivent être en accord
sur la façon dont les données seront transmises entre eux. Ces règles sur la façon dont les données seront
à passer sont appelées conventions d'appel. Une grande partie de ce chapitre sera
traiter les conventions d'appel C standard qui peuvent être utilisées pour l'interface
sousprogrammes d'assemblage avec des programmes C. Ceci (et d'autres conventions) souvent
passer les adresses des données (c'estàdire les pointeurs) pour permettre au sousprogramme d'accéder
les données en mémoire.
4.1 Adressage indirect
L'adressage indirect permet aux registres d'agir comme des variables de pointeur. Pour indiquer
qu'un registre doit être utilisé indirectement comme pointeur, il est entouré de
crochets ([]). Par exemple:
Parce que AX contient un mot, la ligne 3 lit un mot à partir de l'adresse stockée
dans EBX. Si AX était remplacé par AL, un seul octet serait lu.
Il est important de réaliser que les registres n'ont pas de types comme les variables
en C. Ce vers quoi EBX est supposé pointer est entièrement déterminé par
instructions sont utilisées. De plus, même le fait qu'EBX soit un pointeur est
entièrement déterminé par les instructions utilisées. Si EBX est utilisé
incorrectement, souvent il n'y aura pas d'erreur d'assembleur ; cependant le programme
ne fonctionnera pas correctement. C'est l'une des nombreuses raisons pour lesquelles l'assemblage
la programmation est plus sujette aux erreurs que la programmation de haut niveau.
65
Machine Translated by Google
66 CHAPITRE 4. SOUSPROGRAMMES
Tous les 32 bits à usage général (EAX, EBX, ECX, EDX) et index (ESI,
EDI) les registres peuvent être utilisés pour l'adressage indirect. En général, le 16 bits et
Les registres 8 bits ne peuvent pas l'être.
4.2 Exemple de sousprogramme simple
Un sousprogramme est une unité de code indépendante qui peut être utilisée à partir de
différentes parties d'un programme. En d'autres termes, un sousprogramme est comme une fonction
en C. Un saut peut être utilisé pour invoquer le sousprogramme, mais le retour de cadeaux
un problème. Si le sousprogramme doit être utilisé par différentes parties du programme,
il doit revenir à la section de code qui l'a invoqué. Ainsi, le saut
retour du sousprogramme ne peut pas être codé en dur sur une étiquette. Le code cidessous
montre comment cela pourrait être fait en utilisant la forme indirecte de l'instruction JMP.
Cette forme d'instruction utilise la valeur d'un registre pour déterminer où
pour sauter à (ainsi, le registre agit un peu comme un pointeur de fonction en C.) Ici
est le premier programme du chapitre 1 réécrit pour utiliser un sousprogramme.
sous1.asm
1 ; fichier : sub1.asm
2 ; Exemple de programme de sousprogramme
3 % incluent "asm_io.inc"
4
Données à 5 segments
6 prompt1 db 7 "Entrez un nombre : ", 0 ; n'oubliez pas le terminateur nul
prompt2 db 8 "Entrez un autre numéro : ", 0
outmsg1 db 9 "Vous avez entré", 0
"
outmsg2 db 10 et ", 0
outmsg3 db ", la somme de ceuxci est ", 0
11
12 segments .bss
13 entrée1 resd 1
14 entrée2 resd 1
15
16 segments .texte
17 global_asm_main
18 _asm_main :
19 entrez 0,0 pusha ; routine de configuration
20
21
22 mouvement
eax, prompt1 ; imprimer l'invite
23 appel print_string
24
25 mouvement
ebx, entrée1 ; stocker l'adresse de input1 dans ebx
Machine Translated by Google
4.2. EXEMPLE DE SOUSPROGRAMME SIMPLE 67
29 mouvement
eax, invite2 print_string ; imprimer l'invite
30 appel
31
32 mouvement
ebx, entrée2
33 mouvement ecx, $ + 7 court ; ecx = cette adresse + 7
34 jmp get_int
35
36 mouvement
eax, [entrée1] eax, ; eax = dword à l'entrée1
37 ajouter [entrée2] ebx, eax ; eax += dword à l'entrée2
38 mouvement ; ebx = eax
39
40 mouvement
eax, outmsg1
41 appel print_string eax, ; imprimer le premier message
42 mouvement
[entrée1]
43 appel print_int eax, ; imprimer l'entrée1
44 mouvement
outmsg2
45 appel print_string eax, ; imprimer le deuxième message
46 mouvement
[entrée2]
47 appel print_int eax, ; imprimer l'entrée2
48 mouvement
outmsg3
49 appel print_string eax, ebx ; imprimer le troisième message
50 mouvement
54 papa
55 mouvement eax, 0 ; revenir à C
56 partir
57 ret
58 ; sousprogramme get_int
59 ; Paramètres:
60 ; ebx adresse de dword dans laquelle stocker l'entier
61 ; ecx adresse de l'instruction à laquelle retourner
62 ; Remarques:
63 ; la valeur de eax est détruite
64 get_int :
65 appel read_int
66 mouvement [ebx], eax ; stocker l'entrée dans la mémoire
67 jmp exx ; revenir à l'appelant
sous1.asm
Machine Translated by Google
68 CHAPITRE 4. SOUSPROGRAMMES
Le sousprogramme get int utilise une convention d'appel simple, basée sur les registres. Il
s'attend à ce que le registre EBX contienne l'adresse du DWORD à
stocker le numéro entré dans et le registre ECX pour contenir l'adresse de code
de l'instruction vers laquelle revenir. Aux lignes 25 à 28, le label ret1 est utilisé
pour calculer cette adresse de retour. Aux lignes 32 à 34, l'opérateur $ est utilisé pour
calculer l'adresse de retour. L'opérateur $ renvoie l'adresse actuelle pour
la ligne sur laquelle il apparaît. L'expression $ + 7 calcule l'adresse du
Instruction MOV à la ligne 36.
Ces deux calculs d'adresse de retour sont délicats. La première
nécessite la définition d'une étiquette pour chaque appel de sousprogramme. La deuxième
méthode ne nécessite pas d'étiquette, mais nécessite une réflexion approfondie. Si un proche
saut a été utilisé à la place d'un saut court, le nombre à ajouter à $ ne serait pas
avoir 7 ans ! Heureusement, il existe un moyen beaucoup plus simple d'invoquer des sousprogrammes. Ce
méthode utilise la pile.
4.3 La pile
De nombreux processeurs ont un support intégré pour une pile. Un stack est un LastIn
Liste des premiers sortis (LIFO). La pile est une zone de mémoire organisée
dans cette mode. L'instruction PUSH ajoute des données à la pile et le POP
l'instruction supprime les données. Les données supprimées sont toujours les dernières données ajoutées
(c'est pourquoi on l'appelle une liste du dernier entré, premier sorti).
Le registre de segment SS spécifie le segment qui contient la pile
(généralement, il s'agit du même segment dans lequel les données sont stockées). Le registre ESP
contient l'adresse des données qui seraient supprimées de la pile.
Ces données sont dites en haut de la pile. Les données ne peuvent être ajoutées que dans
unités de mots doubles. Autrement dit, on ne peut pas pousser un seul octet sur la pile.
L'instruction PUSH insère un double mot1 sur la pile en soustrayant
4 depuis ESP puis stocke le mot double dans [ESP]. L'instruction POP
lit le mot double à [ESP] puis ajoute 4 à ESP. Le code cidessous
montre comment ces instructions fonctionnent et suppose que l'ESP est initialement
1000H.
1 pousser dword 1 ; 1 stocké à 0FFCh, ESP = 0FFCh
2 pousser dword 2 ; 2 stocké à 0FF8h, ESP = 0FF8h
3 pousser dword 3 ; 3 stocké à 0FF4h, ESP = 0FF4h
4 populaire
eax ; EAX = 3, ESP = 0FF8h
5 populaire
ebx ; EBX = 2, ESP = 0FFCh
6 populaire
exx ; ECX = 1, ESP = 1000h
1En fait, les mots peuvent aussi être poussés, mais en mode protégé 32 bits, il est préférable de travailler
avec seulement des mots doubles sur la pile.
Machine Translated by Google
4.4. LES INSTRUCTIONS D'APPEL ET DE RET 69
La pile peut être utilisée comme emplacement pratique pour stocker temporairement des données.
Il est également utilisé pour effectuer des appels de sousprogrammes, passer des paramètres et des
variables locales.
Le 80x86 fournit également une instruction PUSHA qui pousse les valeurs des registres EAX, EBX,
ECX, EDX, ESI, EDI et EBP (pas dans cet ordre).
L'instruction POPA peut être utilisée pour les retirer tous.
4.4 Les instructions CALL et RET
Le 80x86 fournit deux instructions qui utilisent la pile pour rendre les sousprogrammes d'appel
rapides et faciles. L'instruction CALL fait un saut inconditionnel vers un sousprogramme et pousse
l'adresse de l'instruction suivante sur la pile. L'instruction RET supprime une adresse et saute à cette
adresse. Lors de l'utilisation de ces instructions, il est très important qu'une personne gère correctement
la pile afin que le bon numéro soit extrait par l'instruction RET !
Le programme précédent peut être réécrit pour utiliser ces nouvelles instructions en modifiant les
lignes 25 à 34 pour être :
mouvement
ebx, input1 appel
get_int
mouvement
ebx, appel input2
get_int
et changez le sousprogramme get int en :
get_int :
appeler read_int [ebx], eax
mouvement
ret
CALL et RET présentent plusieurs avantages :
• C'est plus simple !
• Il permet d'imbriquer facilement les appels de sousprogrammes. Notez que les appels get int lisent
int. Cet appel pousse une autre adresse sur la pile. À la fin de la lecture du code de int se trouve
un RET qui supprime l'adresse de retour et revient en arrière pour obtenir le code de int. Ensuite,
lorsque le RET de get int est exécuté, il supprime l'adresse de retour qui revient à asm main. Cela
fonctionne correctement en raison de la propriété LIFO de la pile.
Machine Translated by Google
70 CHAPITRE 4. SOUSPROGRAMMES
N'oubliez pas qu'il est très important de supprimer toutes les données qui sont poussées
sur la pile. Par exemple, considérez ce qui suit :
1 get_int :
2 call read_int [ebx],
3 mouvement eax push eax
4 ret
5 ; apparaît la valeur EAX, pas l'adresse de retour !!
Ce code ne reviendrait pas correctement !
4.5 Conventions d'appel
Lorsqu'un sousprogramme est appelé, le code appelant et le sousprogramme (l'appelé)
doivent s'entendre sur la manière de transmettre des données entre eux. Les langages de
haut niveau ont des méthodes standard pour transmettre des données appelées conventions
d'appel. Pour que le code de haut niveau s'interface avec le langage d'assemblage, le code
du langage d'assemblage doit utiliser les mêmes conventions que le langage de haut niveau.
Les conventions d'appel peuvent différer d'un compilateur à l'autre ou peuvent varier selon la
façon dont le code est compilé (par exemple, si les optimisations sont activées ou non). Une
convention universelle est que le code sera appelé avec une instruction CALL et reviendra via un
RET.
Tous les compilateurs PC C prennent en charge une convention d'appel qui sera décrite
dans le reste de ce chapitre par étapes. Ces conventions permettent de créer des sous
programmes réentrants. Un sousprogramme réentrant peut être appelé en tout point d'un
programme en toute sécurité (même à l'intérieur du sousprogramme luimême).
4.5.1 Passer des paramètres sur la pile
Les paramètres d'un sousprogramme peuvent être passés sur la pile. Ils sont poussés
sur la pile avant l'instruction CALL. Tout comme en C, si le paramètre doit être modifié par le
sousprogramme, l'adresse des données doit être transmise, pas la valeur. Si la taille du
paramètre est inférieure à un mot double, il doit être converti en un mot double avant d'être
poussé.
Les paramètres de la pile ne sont pas extraits par le sousprogramme, mais ils sont
accessibles à partir de la pile ellemême. Pourquoi?
• Puisqu'elles doivent être poussées sur la pile avant l'instruction CALL, l'adresse de
retour devrait d'abord être retirée (puis repoussée à nouveau).
• Souvent, les paramètres devront être utilisés à plusieurs endroits dans le sous
programme. Habituellement, ils ne peuvent pas être conservés dans un registre pour
l'ensemble du sousprogramme et devraient être stockés en mémoire. Les laisser
Machine Translated by Google
4.5. CONVENTIONS D'APPEL 71
ESP + 4 Paramètre
ESP Adresse de retour
Graphique 4.1 :
ESP + 8 Paramètre
ESP + 4 Adresse de retour
Données du sousprogramme ESP
Figure 4.2 :
sur la pile conserve une copie des données en mémoire accessible
à tout moment du sousprogramme.
Considérez un sousprogramme qui reçoit un seul paramètre sur la pile. Lors de l'utilisation d'un habillage indirect, le
Lorsque le sousprogramme est appelé, la pile ressemble à la figure 4.1. Le paramètre est processeur 80x86 accède à
accessible par adressage indirect ([ESP+4] 2
). différents segments en
fonction de
Si la pile est également utilisée à l'intérieur du sousprogramme pour stocker des données, le nombre
les registres sont utilisés dans
nécessaire d'être ajouté à l'ESP va changer. Par exemple, la figure 4.2 montre ce que
expression d'adressage indirect.
la pile ressemble si un DWORD est poussé dans la pile. Maintenant, le paramètre est
ESP (et EBP)
à ESP + 8 et non ESP + 4. Ainsi, il peut être très sujet aux erreurs d'utiliser ESP lorsque utiliser le segment de pile pendant
paramètres de référencement. Pour résoudre ce problème, le 80386 fournit un autre EAX, EBX, ECX et
s'enregistrer pour utiliser : EBP. Le seul but de ce registre est de référencer des données sur EDX utilise le segment de données.
empiler. La convention d'appel C exige qu'un sousprogramme enregistre d'abord le Cependant, c'est généralement
sans importance pour la plupart
valeur d'EBP sur la pile, puis définissez EBP pour qu'il soit égal à ESP. Ceci permet
des programmes en mode protégé,
ESP pour changer au fur et à mesure que les données sont poussées ou retirées de la pile sans modification
car pour eux les données
EBP. A la fin du sousprogramme, la valeur originale de EBP doit être
et les segments de pile sont les
restauré (c'est pourquoi il est enregistré au début du sousprogramme.) Figure 4.3 même.
montre la forme générale d'un sousprogramme qui suit ces conventions.
Les lignes 2 et 3 de la figure 4.3 constituent le prologue général d'un sousprogramme.
Les lignes 5 et 6 constituent l'épilogue. La figure 4.4 montre à quoi ressemble la pile
comme immédiatement après le prologue. Maintenant, le paramètre peut être accessible avec
[EBP + 8] à n'importe quel endroit du sousprogramme sans se soucier de ce
else a été poussé sur la pile par le sousprogramme.
Une fois le sousprogramme terminé, les paramètres qui ont été poussés sur le
la pile doit être retirée. La convention d'appel C spécifie que l'appelant
le code doit le faire. D'autres conventions sont différentes. Par exemple, le Pascal
convention d'appel spécifie que le sousprogramme doit supprimer le paramètre
2
Il est légal d'ajouter une constante à un registre lors de l'utilisation de l'adressage indirect. Plus
des expressions compliquées sont également possibles. Ce sujet est traité dans le chapitre suivant
Machine Translated by Google
72 CHAPITRE 4. SOUSPROGRAMMES
1 sousprogramme_étiquette :
2 pousser ebp ; enregistrer la valeur EBP d'origine sur la pile
3 mouvement
ebp, esp ; ; nouveau EBP = ESP
4 code de sousprogramme
5 pop ebp ; restaurer la valeur EBP d'origine
6 ret
Figure 4.3 : Formulaire de sousprogramme général
ESP + 8 EBP + 8 Paramètre
ESP + 4 EBP + 4 Adresse de retour
L'ESP a sauvé lEBP
'EBP
Figure 4.4 :
ters. (Il existe une autre forme de l'instruction RET qui rend cela facile à
faire.) Certains compilateurs C prennent également en charge cette convention. Le mot clé pascal est
utilisé dans le prototype et la définition de la fonction pour dire au compilateur de
utiliser cette convention. En fait, la convention stdcall que MS Windows
L'utilisation des fonctions API C fonctionne également de cette façon. Quel est l'avantage de cette manière ?
C'est un peu plus efficace que la convention C. Pourquoi toutes les fonctions C
pas utiliser cette convention, alors? En général, C permet à une fonction d'avoir un nombre variable
d'arguments (par exemple, les fonctions printf et scanf). Pour ces
types de fonctions, l'opération pour supprimer les paramètres de la pile
variera d'un appel de la fonction à l'autre. La convention C permet
les instructions pour effectuer cette opération pouvant être facilement modifiées à partir d'un seul appel
au suivant. La convention Pascal et stdcall rend cette opération très
difficile. Ainsi, la convention Pascal (comme le langage Pascal) ne
permettre ce type de fonction. MS Windows peut utiliser cette convention car aucune
de ses fonctions API prennent un nombre variable d'arguments.
La figure 4.5 montre comment un sousprogramme utilisant la convention d'appel C
être appelé. La ligne 3 supprime le paramètre de la pile en manipulant directement le pointeur de
pile. Une instruction POP pourrait également être utilisée pour cela, mais
exigerait que le résultat inutile soit stocké dans un registre. En fait, pour cela
cas particulier, de nombreux compilateurs utiliseraient une instruction POP ECX pour supprimer
le paramètre. Le compilateur utiliserait un POP au lieu d'un ADD parce que le
ADD nécessite plus d'octets pour l'instruction. Cependant, le POP change également
La valeur d'ECX ! Voici un autre exemple de programme avec deux sousprogrammes qui
utilisez les conventions d'appel C décrites cidessus. Ligne 54 (et autres lignes)
montre que plusieurs segments de données et de texte peuvent être déclarés dans un seul
Machine Translated by Google
4.5. CONVENTIONS D'APPEL 73
1 pousser dword 1 ; passer 1 en paramètre
2 appel amusant
3 ajouter esp, 4 ; supprimer le paramètre de la pile
Figure 4.5 : Exemple d'appel de sousprogramme
fichier source. Ils seront combinés en segments de données et de texte uniques dans
le processus de liaison. Fractionnement des données et du code en segments distincts
permettent de définir les données qu'un sousprogramme utilise à proximité du code du
sousprogramme.
sous3.asm
1 %inclut "asm_io.inc"
2
Données à 3 segments
4 somme jj 0
6 segments .bss
7 entrée resd 1
8
9 ;
dix ; algorithme de pseudocode
11 ; je = 1 ;
12 ; somme = 0 ;
13 ; while( get_int(i, &input), input != 0 ) {
14 ; somme += entrée ;
15 ; je++ ;
16 ; }
17 ; print_sum(num);
18 segments .texte
19 global_asm_main
20 _asm_main :
21 entrez 0,0 pusha ; routine de configuration
22
23
74 CHAPITRE 4. SOUSPROGRAMMES
30
31 mouvement
eax, [entrée]
32 cmp eax, 0
33 je end_while
34
37 inc. edx
38 jmp boucle while_courte
39
40 end_while :
41 pousser dword [somme] ; pousser la valeur de la somme sur la pile
42 l'appel print_sum
43 populaire
exx ; supprimer [somme] de la pile
44
45 papa
46 partir
47 ret
48
49 ; sousprogramme get_int
50 ; Paramètres (dans l'ordre poussé sur la pile)
51 ; nombre d'entrées (à [ebp + 12])
52 ; adresse du mot dans lequel stocker l'entrée (à [ebp + 8])
53 ; Remarques:
54 ; les valeurs de eax et ebx sont détruites
55 segments .données
56 invite db ") Entrez un nombre entier (0 pour quitter) : ", 0
57
58 segments .text
59 get_int :
60 pousser ebp
61 mouvement
ebp, esp
62
63 mouvement
eax, [ebp + 12]
64 appel print_int
65
66 mouvement
eax, invite
67 appel print_string
68
69 appel read_int
70 mouvement
ebx, [ebp + 8]
71 mouvement [ebx], eax ; stocker l'entrée dans la mémoire
Machine Translated by Google
4.5. CONVENTIONS D'APPEL 75
72
73 populaire ebp
74 ret ; revenir à l'appelant
75
76 ; sousprogramme print_sum
77 ; imprime la somme
78 ; Paramètre:
79 ; somme à imprimer (à [ebp+8])
80 ; Remarque : détruit la valeur de eax
81 ;
82 segments .données
83 résultat db "La somme est ", 0
84
85 segments .texte
86 print_sum :
87 pousser ebp
88 mouvement
ebp, esp
89
90 mouvement eax, résultat
91 appel print_string
92
93 mouvement
eax, [ebp+8]
94 appel print_int
95 appel print_nl
96
97 populaire ebp
98 ret
sous3.asm
4.5.2 Variables locales sur la pile
La pile peut être utilisée comme emplacement pratique pour les variables locales. C'est
exactement où C stocke les variables normales (ou automatiques dans le jargon C). En utilisant le
stack pour les variables est important si l'on souhaite que les sousprogrammes soient réentrants.
Un sousprogramme réentrant fonctionnera s'il est appelé à n'importe quel endroit, y compris le
sousprogramme luimême. En d'autres termes, les sousprogrammes réentrants peuvent être invoqués
récursivement. L'utilisation de la pile pour les variables permet également d'économiser de la mémoire. Données non stockées
sur la pile utilise de la mémoire depuis le début du programme jusqu'au
fin du programme (C appelle ces types de variables globales ou statiques). Données
stockés sur la pile n'utilisent la mémoire que lorsque le sousprogramme qu'ils sont définis
pour est actif.
Les variables locales sont stockées juste après la valeur EBP enregistrée dans la pile.
Ils sont alloués en soustrayant le nombre d'octets requis d'ESP
Machine Translated by Google
76 CHAPITRE 4. SOUSPROGRAMMES
1 sousprogramme_étiquette :
2 pousser ebp ; enregistrer la valeur EBP d'origine sur la pile
3 mouvement
ebp, esp ; nouveau EBP = ESP
4 sous esp, LOCAL_BYTES ; = # octets nécessaires aux locaux
5 ; code de sousprogramme
6 mouvement
esp, ebp ebp ; désaffecter les habitants
7 populaire ; restaurer la valeur EBP d'origine
8 ret
Figure 4.6 : Formulaire général de sousprogramme avec variables locales
1 void calc sum( int n, int sump )
2 {
3 int je , somme = 0 ;
4
5 pour( je=1; je <= n; je++ )
6 somme += je ;
7 puisard = somme ;
8 }
Figure 4.7 : Version C de la somme
dans le prologue du sousprogramme. La figure 4.6 montre le nouveau sousprogramme
squelette. Le registre EBP est utilisé pour accéder aux variables locales. Prendre en compte
Fonction C dans la Figure 4.7. La figure 4.8 montre comment le sousprogramme équivalent
peut être écrit en assembleur.
La figure 4.9 montre à quoi ressemble la pile après le prologue du programme de la figure 4.8. Cette
section de la pile qui contient les paramètres,
les informations de retour et le stockage des variables locales s'appellent un cadre de pile. Chaque
Malgré le fait que l'appel ENTER d'une fonction C crée un nouveau cadre de pile sur la pile.
et LAISSER simplifier le
Le prologue et l'épilogue d'un sousprogramme peuvent être simplifiés en utilisant
prologue et épilogue ils
ne sont pas très souvent utilisés. deux instructions spéciales conçues spécifiquement à cet effet. Le
Pourquoi? Parce qu'ils sont L'instruction ENTER exécute le code de prologue et l'instruction LEAVE exécute le
plus lent que l'équivalent épilogue. L'instruction ENTER prend deux opérandes immédiats. Pour le
consignes plus simples ! Ce Convention d'appel C, le deuxième opérande est toujours 0. Le premier opérande est
est un exemple où le nombre d'octets nécessaires aux variables locales. L'instruction LEAVE n'a pas
on ne peut supposer qu'un
opérandes. La figure 4.10 montre comment ces instructions sont utilisées. Notez que le
une séquence d'instructions est
le squelette du programme (Figure 1.7) utilise également ENTER et LEAVE.
plus rapide qu'une instruction
multiple.
Machine Translated by Google
4.6. PROGRAMMES MULTIMODULES 77
1 cal_sum :
2 pousser ebp
3 mouvement
ebp, esp
4 sous esp, 4 ; faire place à la somme locale
5
6 mouvement
dword [ebp 4], 0 ebx, 1 ; somme = 0
7 mouvement ; ebx (i) = 1
8 for_loop :
9 jnle ebx, [ebp+8] cmp ; estce que je <= n ?
dix end_for
11
16 fin_pour :
17 mouvement
ebx, [ebp+12] eax, ; ebx = puisard
18 mouvement
[ebp4] [ebx], eax ; eax = somme
19 mouvement
; * puisard = somme ;
20
21 mouvement
esp, ebp
22 populaire ebp
23 ret
Figure 4.8 : Version assemblée de sum
4.6 Programmes multimodules
Un programme multimodule est un programme composé de plus d'un fichier objet.
Tous les programmes présentés ici sont des programmes multimodules. Ils
se composait du fichier objet du pilote C et du fichier objet de l'assemblage (plus le
fichiers objets de la bibliothèque C). Rappelezvous que l'éditeur de liens combine les fichiers objets en
un seul programme exécutable. L'éditeur de liens doit correspondre aux références faites
à chaque étiquette dans un module (c'estàdire fichier objet) à sa définition dans un autre
module. Pour que le module A utilise une étiquette définie dans le module B, le
une directive externe doit être utilisée. Après la directive extern vient une virgule
liste délimitée d'étiquettes. La directive dit à l'assembleur de traiter ces
libellés comme externes au module. Autrement dit, ce sont des étiquettes qui peuvent être utilisées
dans ce module, mais sont définis dans un autre. Le fichier asm io.inc définit le
lire les routines int, etc. comme externes.
En assemblage, les étiquettes ne sont pas accessibles en externe par défaut. Si une étiquette
Machine Translated by Google
78 CHAPITRE 4. SOUSPROGRAMMES
ESP + 16 EBP + 12 puisard
ESP + 12 EBP + 8 n
ESP + 8 EBP + 4 Adresse de retour
ESP + 4 EBP enregistré EBP
ESP EBP 4 somme
Figure 4.9 :
1 sousprogramme_étiquette :
2 entrez LOCAL_BYTES, 0 ; code ; = # octets nécessaires aux locaux
3 de sousprogramme
4 partir
5 ret
Figure 4.10 : Formulaire général de sousprogramme avec variables locales en utilisant ENTER et
PARTIR
accessible depuis d'autres modules que celui dans lequel il est défini, il doit
être déclaré global dans son module. La directive globale le fait. Ligne 13
de la liste des programmes squelette de la figure 1.7 montre l'étiquette principale asm
étant défini comme global. Sans cette déclaration, il y aurait un lien
erreur. Pourquoi? Parce que le code C ne pourrait pas faire référence à l'interne
étiquette principale asm.
Vient ensuite le code de l'exemple précédent, réécrit pour utiliser deux modules.
Les deux sousprogrammes (get int et print sum) sont dans un fichier source séparé
que la routine principale asm.
main4.asm
1 %inclut "asm_io.inc"
2
Données à 3 segments
4 somme jj 0
5
6 segments .bss
7 entrée resd 1
8
9 segments .text
dix global_asm_main
11
externe get_int, print_sum
12 _asm_main :
13 entrez 0,0 pusha ; routine de configuration
14
Machine Translated by Google
4.6. PROGRAMMES MULTIMODULES 79
15
23 mouvement
eax, [entrée]
24 cmp eax, 0
25 je end_while
26
29 inc. edx
30 jmp boucle while_courte
31
32 end_while :
33 pousser dword [somme] ; pousser la valeur de la somme sur la pile
34 appeler print_sum
35 populaire
exx ; supprimer [somme] de la pile
36
37 papa
38 partir
39 ret
main4.asm
sous4.asm
1 %inclut "asm_io.inc"
2
Données à 3 segments
4 invites de base de données ") Entrez un nombre entier (0 pour quitter) : ", 0
5
6 segments .text
7 global get_int, print_sum
8 get_int :
9 entrez 0,0
dix
11 mouvement
eax, [ebp + 12]
12 appel print_int
13
14 mouvement
eax, invite
15 appel print_string
16
Machine Translated by Google
80 CHAPITRE 4. SOUSPROGRAMMES
17 appel read_int
18 mouvement
ebx, [ebp + 8]
19 mouvement [ebx], eax ; stocker l'entrée dans la mémoire
20
21 partir
22 ret ; revenir à l'appelant
23
Données 24 segments
25 résultat db "La somme est ", 0
26
27 segments .texte
28 print_sum :
29 entrez 0,0
30
31 mouvement eax, résultat
32 appel print_string
33
34 mouvement
eax, [ebp+8]
35 appel print_int
36 appel print_nl
37
38 partir
39 ret
sous4.asm
L'exemple précédent n'a que des étiquettes de code globales ; cependant, les données mondiales
les étiquettes fonctionnent exactement de la même manière.
4.7 Interfacer l'assemblage avec C
Aujourd'hui, très peu de programmes sont entièrement écrits en assembleur. Compilateurs
sont très bons pour convertir du code de haut niveau en code machine efficace. Depuis
il est beaucoup plus facile d'écrire du code dans un langage de haut niveau, c'est plus populaire.
De plus, le code de haut niveau est bien plus portable que l'assemblage !
Lorsque l'assemblage est utilisé, il n'est souvent utilisé que pour de petites parties du code.
Cela peut être fait de deux manières : en appelant des sousprogrammes d'assemblage à partir de C ou
assemblage en ligne. L'assemblage en ligne permet au programmeur de placer l'assemblage
instructions directement dans le code C. Cela peut être très pratique ; cependant, il y
sont des inconvénients à l'assemblage en ligne. Le code assembleur doit être écrit en
le format utilisé par le compilateur. Aucun compilateur pour le moment ne supporte les NASM
format. Différents compilateurs nécessitent différents formats. Borland et Microsoft
nécessite le format MASM. DJGPP et gcc de Linux nécessitent le format GAS3. Le
3GAS est l'assembleur utilisé par tous les compilateurs GNU. Il utilise la syntaxe AT&T qui
Machine Translated by Google
4.7. ENSEMBLE D'INTERFACE AVEC C 81
1 segment .données
2x _ jj 0
3 formats db "x = %d\n", 0
4
5 segments .text
6 ...
7 appuyez sur dword [x] ; pousser la valeur de x
8 pousser le format dword ; adresse push de la chaîne de format
9 appelez _printf ; notez le soulignement !
dix ajouter esp, 8 ; supprimer les paramètres de la pile
Figure 4.11 : Appel à printf
la technique d'appel d'un sousprogramme d'assemblage est beaucoup plus standardisée sur
le PC.
Les routines d'assemblage sont généralement utilisées avec C pour les raisons suivantes :
• Un accès direct est nécessaire aux fonctionnalités matérielles de l'ordinateur qui sont
difficile ou impossible d'accès depuis C.
• La routine doit être aussi rapide que possible et le programmeur peut
optimiser le code mieux que le compilateur ne le peut.
La dernière raison n'est plus aussi valable qu'elle l'était autrefois. La technologie du compilateur a
amélioré au fil des ans et les compilateurs peuvent souvent générer du code très efficace
(surtout si les optimisations du compilateur sont activées). Les inconvénients de
routines d'assemblage sont : une portabilité et une lisibilité réduites.
La plupart des conventions d'appel C ont déjà été spécifiées. Cependant,
il y a quelques fonctionnalités supplémentaires qui doivent être décrites.
4.7.1 Sauvegarde des registres
Tout d'abord, C suppose qu'un sousprogramme conserve les valeurs des éléments suivants Le motclé register peut
être utilisé dans une déclaration
registres : EBX, ESI, EDI, EBP, CS, DS, SS, ES. Cela ne veut pas dire que
le sousprogramme ne peut pas les modifier en interne. Au lieu de cela, cela signifie que si de variable C pour suggérer au
compilateur qu'il utilise un
il change leurs valeurs, il doit restaurer leurs valeurs d'origine avant que le
registre pour cette variable au
le sousprogramme revient. Les valeurs EBX, ESI et EDI ne doivent pas être modifiées
lieu d'un emplacement mémoire.
car C utilise ces registres pour les variables de registre. Habituellement, la pile est Cellesci sont connues sous le nom de
utilisé pour sauvegarder les valeurs d'origine de ces registres. variables de registre. Les
compilateurs modernes le font
est très différent des syntaxes relativement similaires de MASM, TASM et NASM. automatiquement sans nécessiter
Aucune suggestion.
Machine Translated by Google
82 CHAPITRE 4. SOUSPROGRAMMES
EBP + 12 valeur de x
EBP + 8 adresse de la chaîne de format
EBP + 4 Adresse de retour
EBP EBP enregistré
Figure 4.12 : Pile à l'intérieur de printf
4.7.2 Libellés des fonctions
La plupart des compilateurs C ajoutent un seul caractère de soulignement( ) au
début des noms des fonctions et des variables globales/statiques. Par exemple, une
fonction nommée f se verra attribuer le label f. Ainsi, s'il s'agit d'une routine d'assemblage,
elle doit être étiquetée f, et non f. Le compilateur Linux gcc ne préfixe aucun caractère.
Sous les exécutables Linux ELF, on utiliserait simplement l'étiquette f pour la fonction C f.
Cependant, le gcc de DJGPP ajoute un trait de soulignement. Notez que dans le squelette
du programme d'assemblage (Figure 1.7), l'étiquette de la routine principale est asm main.
4.7.3 Passer des paramètres
Sous la convention d'appel C, les arguments d'une fonction sont poussés
sur la pile dans l'ordre inverse de leur apparition dans l'appel de fonction.
Considérez l'instruction C suivante : printf("x = %d\n",x); La figure 4.11 montre comment
ceci serait compilé (montré dans le format NASM équivalent).
La figure 4.12 montre à quoi ressemble la pile après le prologue à l'intérieur de la fonction
printf. La fonction printf est l'une des fonctions de la bibliothèque C qui peut prendre
n'importe quel nombre d'arguments. Les règles des conventions d'appel C qu'il n'est pas
nécessaire d'utiliser ont été spécifiquement écrites pour autoriser ces types de fonctions. Étant donné que l' assembly
d'adresse pour traiter un ar de la chaîne de format est poussé en dernier, son emplacement sur la pile sera toujours au
nombre binaire d'argu EBP + 8, quel que soit le nombre de paramètres passés à la fonction. Les ments en C. Le code
printf stdarg.h peut alors
examiner la chaîne de format pour déterminer le nombre de paramètres de macros de définition
de fichier d'entête qui
auraient dû être passés et les rechercher sur la pile. qui peut être utilisé pour traiter Bien
sûr, si une erreur est
valeur du mot double commise,
à [EBP printf("x = %d\n"), le printf les code de manière portable. Voir tout imprimera toujours la
+ 12]. Cependant, ce sera un bon livre C pour plus de détails. ne pas être la valeur de x !
4.7.4 Calcul des adresses des variables locales
Trouver l'adresse d'une étiquette définie dans les segments data ou bss est simple.
Fondamentalement, l'éditeur de liens fait cela. Cependant, calculer l'adresse d'une
variable locale (ou paramètre) sur la pile n'est pas aussi simple.
Cependant, c'est un besoin très courant lors de l'appel de sousprogrammes. Considérons
le cas de la transmission de l'adresse d'une variable (appelonsla x) à une fonction
Machine Translated by Google
4.7. ENSEMBLE D'INTERFACE AVEC C 83
(appelonsle foo). Si x est situé à EBP − 8 sur la pile, on ne peut pas simplement
utiliser:
mouvement
eax, ebp 8
Pourquoi? La valeur que MOV stocke dans EAX doit être calculée par l'assembleur (c'est
àdire qu'elle doit finalement être une constante). Cependant, il existe une instruction qui
effectue le calcul souhaité. Il s'appelle LEA (pour Load Effective Address). Ce qui suit
calcule l'adresse de x et la stocke dans EAX :
léa eax, [ebp 8]
Désormais, EAX contient l'adresse de x et peut être poussé sur la pile lors de l'appel de la
fonction foo. Ne soyez pas confus, il semble que cette instruction lit les données à [EBP−8] ;
Cependant, ce n'est pas vrai. L'instruction LEA ne lit jamais la mémoire ! Il ne calcule que
l'adresse qui serait lue par une autre instruction et stocke cette adresse dans son premier
opérande de registre.
Puisqu'il ne lit en fait aucune mémoire, aucune désignation de taille de mémoire (par
exemple dword) n'est nécessaire ou autorisée.
4.7.5 Renvoyer des valeurs
Les fonctions C non vides renvoient une valeur. Les conventions d'appel C spécifient
comment cela est fait. Les valeurs de retour sont transmises via des registres. Tous les
types entiers (char, int, enum, etc.) sont retournés dans le registre EAX. S'ils sont inférieurs
à 32 bits, ils sont étendus à 32 bits lorsqu'ils sont stockés dans EAX.
(La façon dont ils sont étendus dépend s'il s'agit de types signés ou non signés.) Les
valeurs 64 bits sont renvoyées dans la paire de registres EDX:EAX. Les valeurs de pointeur
sont également stockées dans EAX. Les valeurs à virgule flottante sont stockées dans le
registre ST0 du coprocesseur mathématique. (Ce registre est décrit dans le chapitre sur les
virgules flottantes.)
4.7.6 Autres conventions d'appel
Les règles cidessus décrivent la convention d'appel C standard supportée par tous
les compilateurs C 80x86. Souvent, les compilateurs prennent également en charge d'autres
conventions d'appel. Lors de l'interface avec le langage d'assemblage, il est très important
de savoir quelle convention d'appel le compilateur utilise lorsqu'il appelle votre fonction.
Habituellement, la valeur par défaut est d'utiliser la convention d'appel standard ; cependant,
ce n'est pas toujours le cas4 . Les compilateurs qui utilisent plusieurs conventions ont
souvent des commutateurs de ligne de commande qui peuvent être utilisés pour modifier
4Le compilateur Watcom C est un exemple de compilateur qui n'utilise pas la convention standard
par défaut. Voir l'exemple de fichier de code source pour Watcom pour plus de détails
Machine Translated by Google
84 CHAPITRE 4. SOUSPROGRAMMES
la convention par défaut. Ils fournissent également des extensions à la syntaxe C pour
attribuer explicitement des conventions d'appel à des fonctions individuelles. Cependant,
ces extensions ne sont pas standardisées et peuvent varier d'un compilateur à l'autre.
Le compilateur GCC autorise différentes conventions d'appel. La convention d'une
fonction peut être explicitement déclarée en utilisant l'extension d'attribut. Par exemple,
pour déclarer une fonction void qui utilise la convention d'appel standard nommée f qui
prend un seul paramètre int, utilisez la syntaxe suivante pour son prototype :
void f ( int ) attribut ((cdecl));
GCC prend également en charge la convention d'appel standard. La fonction cidessus
pourrait être déclarée pour utiliser cette convention en remplaçant le cdecl par stdcall.
La différence entre stdcall et cdecl est que stdcall nécessite que le sousprogramme
supprime les paramètres de la pile (comme le fait la convention d'appel Pascal). Ainsi, la
convention stdcall ne peut être utilisée qu'avec des fonctions qui prennent un nombre fixe
d'arguments (c'estàdire qui ne sont pas comme printf et scanf).
GCC prend également en charge un attribut supplémentaire appelé regparm qui
indique au compilateur d'utiliser des registres pour transmettre jusqu'à 3 arguments entiers
à une fonction au lieu d'utiliser la pile. Il s'agit d'un type d'optimisation courant pris en
charge par de nombreux compilateurs.
Borland et Microsoft utilisent une syntaxe commune pour déclarer les conventions
d'appel. Ils ajoutent les mots clés cdecl et stdcall au C. Ces mots clés agissent comme
des modificateurs de fonction et apparaissent immédiatement avant le nom de la fonction
dans un prototype. Par exemple, la fonction f cidessus serait définie comme suit pour
Borland et Microsoft :
void cdecl f ( int );
Il y a des avantages et des inconvénients à chacune des conventions d'appel. Les
principaux avantages de la convention cdecl sont qu'elle est simple et très flexible. Il peut
être utilisé pour tout type de fonction C et de compilateur C. L'utilisation d'autres
conventions peut limiter la portabilité du sousprogramme. Son principal inconvénient est
qu'il peut être plus lent que certains autres et utiliser plus de mémoire (puisque chaque
invocation de la fonction nécessite du code pour supprimer les paramètres de la pile).
L'avantage de la convention stdcall est qu'elle utilise moins de mémoire que cdecl.
Aucun nettoyage de pile n'est requis après l'instruction CALL. Son principal inconvénient
est qu'il ne peut pas être utilisé avec des fonctions qui ont un nombre variable d'arguments.
L'avantage d'utiliser une convention qui utilise des registres pour passer des
paramètres entiers est la vitesse. Le principal inconvénient est que la convention est plus
complexe. Certains paramètres peuvent être dans des registres et d'autres sur la pile.
Machine Translated by Google
4.7. ENSEMBLE D'INTERFACE AVEC C 85
4.7.7 Exemples
Voici un exemple qui montre comment une routine d'assemblage peut être interfacée
à un programme C. (Notez que ce programme n'utilise pas le squelette d'assemblage
(Figure 1.7) ou le module driver.c.)
main5.c
1 #include <stdio.h>
2 / prototype de routine d'assemblage /
3 void calc sum( int int ) ,attribut ((cdecl));
4
5 entier principal ( vide )
6 {
7 int n, somme ;
8
9 printf(”Somme des entiers jusqu'à : ”);
dix scanf(”%d”, &n);
11 calc sum(n, &sum);
12 printf ("La somme est %d\n", somme);
13 renvoie 0 ;
14 }
main5.c
sous5.asm
1 ; sousprogramme _calc_sum
2 ; trouve la somme des nombres entiers de 1 à n
3 ; Paramètres:
4 ; n à quoi résumer (à [ebp + 8])
5 ; sump pointeur vers int dans lequel stocker la somme (à [ebp + 12])
6 ; pseudocode C :
7 ; void calc_sum( int n, int * puisard )
8 ; {
9 ; int je, somme = 0 ;
dix ; pour( je=1; je <= n; je++ )
11 ; somme += je ;
12 ; * puisard = somme ;
13 ; }
14
15 segments .text
16 global _calc_sum
17 ;
Machine Translated by Google
86 CHAPITRE 4. SOUSPROGRAMMES
Somme des nombres entiers jusqu'à : 10
Vidage de la pile # 1
EBP = BFFFFB70 ESP = BFFFFB68
+16 BFFFFB80 080499EC
+12 BFFFFB7C BFFFFB80
+8 BFFFFB78 0000000A
+4 BFFFFB74 08048501
+0 BFFFFB70 BFFFFB88
4 BFFFFB6C 00000000
8 BFFFFB68 4010648C
La somme est de 55
Figure 4.13 : Exemple d'exécution du programme sub5
18 ; variable locale:
19 ; somme à [ebp4]
20 _calc_sum :
21 entrez 4,0 ; faire de la place pour la somme sur la pile
22 poussez ebx ; IMPORTANT!
23
24 mouvement
dword [ebp4],0 ; somme = 0
25 dump_stack 1, 2, 4 ; imprimer la pile de ebp8 à ebp+16
26 mouvement exx, 1 ; ecx estce que je suis en pseudocode
27 for_loop :
28 cmp ecx, [ebp+8] ; cmp je et n
29 jnle end_for ; sinon i <= n, quitter
30
35 fin_pour :
36 mouvement
ebx, [ebp+12] eax, ; ebx = puisard
37 mouvement
[ebp4] [ebx], eax ; eax = somme
38 mouvement
39
40 populaire
ebx ; restaurer ebx
41 partir
42 ret
sous5.asm
Machine Translated by Google
4.7. ENSEMBLE D'INTERFACE AVEC C 87
Pourquoi la ligne 22 de sub5.asm estelle si importante ? Parce que la convention d'appel C
exige que la valeur de EBX ne soit pas modifiée par l'appel de fonction. Si
ceci n'est pas fait, il est très probable que le programme ne fonctionnera pas correctement.
La ligne 25 montre comment fonctionne la macro dump stack. Rappelons que le
le premier paramètre est juste une étiquette numérique, et les deuxième et troisième paramètres
déterminer le nombre de mots doubles à afficher respectivement en dessous et audessus d'EBP.
La figure 4.13 montre un exemple d'exécution du programme. Pour ce dépotoir,
on peut voir que l'adresse du dword pour stocker la somme est BFFFFB80 (à
EBP + 12); le nombre à additionner est 0000000A (à EBP + 8); le retour
l'adresse de la routine est 08048501 (à EBP + 4); la valeur EBP enregistrée est
BFFFFB88 (chez EBP) ; la valeur de la variable locale est 0 à (EBP 4) ; et
enfin la valeur EBX enregistrée est 4010648C (à EBP 8).
La fonction calc sum pourrait être réécrite pour renvoyer la somme comme son retour
valeur au lieu d'utiliser un paramètre de pointeur. Puisque la somme est une intégrale
valeur, la somme doit être laissée dans le registre EAX. Ligne 11 du main5.c
le fichier serait changé en :
somme = calc somme(n);
De plus, le prototype de la somme calc aurait besoin d'être modifié. Cidessous le code
d'assemblage modifié :
sous6.asm
1 ; sousprogramme _calc_sum
2 ; trouve la somme des nombres entiers de 1 à n
3 ; Paramètres:
4 ; n à quoi résumer (à [ebp + 8])
5 ; Valeur de retour :
6 ; valeur de la somme
7 ; pseudocode C :
8 ; int calc_sum( int n )
9 ; {
dix int je, somme = 0 ;
11 ; ; pour( je=1; je <= n; je++ )
12 ; somme += je ;
13 ; somme de retour ;
14 ; }
15 segments .text
16 global _calc_sum
17 ;
18 ; variable locale:
19 ; somme à [ebp4]
20 _calc_sum :
21 entrez 4,0 ; faire de la place pour la somme sur la pile
Machine Translated by Google
88 CHAPITRE 4. SOUSPROGRAMMES
1 segment .données
2 formats db "%d", 0
3
4 segments .text
5 ...
6 léa eax, [ebp16]
7 pousser eax
8 pousser le format dword
9 appeler _scanf
dix ajouter esp, 8
11 ...
Figure 4.14 : Appel de scanf depuis l'assembly
22
23 mouvement
dword [ebp4],0 ; somme = 0
24 mouvement ex, 1 ; ecx estce que je suis en pseudocode
25 for_loop :
26 cmp ecx, [ebp+8] ; cmp je et n
27 jnle end_for ; sinon i <= n, quitter
28
33 fin_pour :
34 mouvement
eax, [ebp4] ; eax = somme
35
36 partir
37 ret
sous6.asm
4.7.8 Appel de fonctions C depuis l'assembly
Un grand avantage de l'interfaçage du C et de l'assemblage est qu'il permet d'accéder à la
grande bibliothèque C et aux fonctions écrites par l'utilisateur. Pour
exemple, et si on voulait appeler la fonction scanf pour lire dans un entier
du clavier ? La figure 4.14 montre le code pour faire cela. Un très important
le point à retenir est que scanf suit la norme d'appel C à la lettre.
Cela signifie qu'il conserve les valeurs des registres EBX, ESI et EDI ;
cependant, les registres EAX, ECX et EDX peuvent être modifiés ! En effet, EAX
sera certainement modifié, car il contiendra la valeur de retour du scanf
Machine Translated by Google
4.8. SOUSPROGRAMMES RÉENTRANTS ET RÉCURSIFS 89
appel. Pour d'autres exemples d'utilisation de l'interfaçage avec C, regardez le code dans asm io.asm
qui a été utilisé pour créer asm io.obj.
4.8 Sousprogrammes réentrants et récursifs
Un sousprogramme réentrant doit satisfaire les propriétés suivantes :
• Il ne doit modifier aucune instruction de code. Dans un langage de haut niveau, cela serait
difficile, mais en assembleur, il n'est pas difficile pour un programme d'essayer de modifier son
propre code. Par exemple:
mot mov [cs:$+7], 5 ; copier 5 dans le mot 7 octets devant ; la déclaration
ajouter hache, 2 précédente passe de 2 à 5 !
Ce code fonctionnerait en mode réel, mais dans les systèmes d'exploitation en mode protégé,
le segment de code est marqué en lecture seule. Lorsque la première ligne cidessus s'exécute,
le programme sera abandonné sur ces systèmes. Ce type de programmation est mauvais pour
de nombreuses raisons. Il est déroutant, difficile à maintenir et ne permet pas le partage de
code (voir cidessous).
• Il ne doit pas modifier les données globales (telles que les données dans les données et le bss
segments). Toutes les variables sont stockées sur la pile.
Il y a plusieurs avantages à écrire du code réentrant.
• Un sousprogramme réentrant peut être appelé de manière récursive.
• Un programme réentrant peut être partagé par plusieurs processus. Sur de nombreux systèmes
d'exploitation multitâches, s'il y a plusieurs instances d'un programme en cours d'exécution,
une seule copie du code est en mémoire. Les bibliothèques partagées et les DLL (Dynamic
Link Libraries) utilisent également cette idée.
5
• Les sousprogrammes réentrants fonctionnent beaucoup mieux en multithread pro
grammes. Windows 9x/NT et la plupart des systèmes d'exploitation de type UNIX (So laris,
Linux, etc.) prennent en charge les programmes multithreads.
4.8.1 Sousprogrammes récursifs
Ces types de sousprogrammes s'appellent euxmêmes. La récursivité peut être directe ou
indirecte. La récursivité directe se produit lorsqu'un sousprogramme, disons foo, s'appelle luimême
à l'intérieur du corps de foo. La récursivité indirecte se produit lorsqu'un sousprogramme n'est pas
appelé directement par luimême, mais par un autre sousprogramme qu'il appelle. Par exemple, le
sousprogramme foo pourrait appeler bar et bar pourrait appeler foo.
5Un programme multithread a plusieurs threads d'exécution. c'estàdire le programme
ellemême est multitâche.
Machine Translated by Google
90 CHAPITRE 4. SOUSPROGRAMMES
1 ; trouve n!
2 segments .text
3 _fait global
4_fait :
5 entrez 0,0
6
7 mouvement
eax, [ebp+8] eax, ; eax = n
8 cmp 1
9 jbe term_cond ; si n <= 1, terminer
dix dec eax
11 pousser eax
12 appeler _fait ; eax = fait(n1)
13 populaire
exx ; répondre en eax
14 mul dword [ebp+8] court ; edx:eax = eax * [ebp+8]
15 jmp end_fact
16 term_cond :
17 mouvement eax, 1
18 fin_fait :
19 partir
20 ret
Figure 4.15 : Fonction factorielle récursive
Les sousprogrammes récursifs doivent avoir une condition de terminaison. Quand cela
condition est vraie, plus aucun appel récursif n'est effectué. Si une routine récursive
n'a pas de condition de terminaison ou la condition ne devient jamais vraie,
la récursivité ne finira jamais (un peu comme une boucle infinie).
La figure 4.15 montre une fonction qui calcule les factorielles de manière récursive. Il
peut être appelé depuis C avec :
x = fait (3); / trouver 3 ! /
La figure 4.16 montre à quoi ressemble la pile à son point le plus profond pour ce qui précède
appel de fonction.
Les figures 4.17 et 4.18 montrent un autre exemple récursif plus compliqué
en C et en assemblage, respectivement. Quelle est la sortie pour f(3) ? Note
que l'instruction ENTER crée un nouveau i sur la pile pour chaque récursif
appel. Ainsi, chaque instance récursive de f a sa propre variable indépendante i.
Définir i comme un mot double dans le segment de données ne fonctionnerait pas de la même manière.
Machine Translated by Google
4.8. SOUSPROGRAMMES RÉENTRANTS ET RÉCURSIFS 91
n(3)
n=3 images
Adresse de retour
EBP enregistré
n(2)
n=2 trame Adresse de retour
EBP enregistré
n(1)
n=1 image Adresse de retour
EBP enregistré
Figure 4.16 : Cadres de pile pour la fonction factorielle
1 vide f ( int x ) 2 {
3 int je ;
4 for( je=0; je < x; je++ )
5 { printf(”%d\n”, je);
6 Fi ); }
7
8 }
Figure 4.17 : Autre exemple (version C)
4.8.2 Examen des types de stockage de variables C
C fournit plusieurs types de stockage de variables.
global Ces variables sont définies en dehors de toute fonction et sont stockées à des
emplacements de mémoire fixes (dans les segments data ou bss) et existent du début
du programme jusqu'à la fin. Par défaut, ils sont accessibles depuis n'importe quelle
fonction du programme ; cependant, s'ils sont déclarés comme statiques, seules les
fonctions d'un même module peuvent y accéder (c'estàdire qu'en termes
d'assemblage, l'étiquette est interne et non externe).
statique Ce sont des variables locales d'une fonction qui sont déclarées statiques.
(Malheureusement, C utilise le motclé static à deux fins différentes !)
Ces variables sont également stockées à des emplacements de mémoire fixes (dans data ou
bss), mais ne sont accessibles directement que dans les fonctions dans lesquelles elles sont
définies.
Machine Translated by Google
92 CHAPITRE 4. SOUSPROGRAMMES
1 %define i ebp4
2 %define x ebp+8 3 ; macros utiles
segment .data
format 5 db "%d", 10, 0 4 ; 10 = '\n'
segment .text
6 global_f
7 externe _printf
8_f :
9 entrez 4,0 ; allouer de la place sur la pile pour i
dix
11 mouvement dmot [i], 0 ; je = 0
12 lp :
13 mouvement eax, [i] eax, ; estce que je < x ?
14 cmp [x]
15 jnl arrêter
16
17 push eax push ; appeler printf
18 format
19 appeler _printf
20 ajouter esp, 8
21
22 pousser dword [i] appeler ; appeler f
23 _f
24 populaire
eax
25
30 ret
Figure 4.18 : Autre exemple (version assemblage)
Machine Translated by Google
4.8. SOUSPROGRAMMES RÉENTRANTS ET RÉCURSIFS 93
automatique Il s'agit du type par défaut d'une variable C définie dans une fonction. Ces
variables sont allouées sur la pile lorsque la fonction dans laquelle elles sont définies
est invoquée et sont désallouées lorsque la fonction revient.
Ainsi, ils n'ont pas d'emplacements de mémoire fixes.
registre Ce mot clé demande au compilateur d'utiliser un registre pour les données de cette
variable. Ce n'est qu'une demande. Le compilateur n'a pas à l'honorer. Si l'adresse
de la variable est utilisée n'importe où dans le programme, elle ne sera pas honorée
(puisque les registres n'ont pas d'adresse). De plus, seuls les types intégraux simples
peuvent être des valeurs de registre. Les types structurés ne peuvent pas l'être ; ils
ne rentreraient pas dans un registre ! Les compilateurs C transforment souvent
automatiquement les variables automatiques normales en variables de registre sans
aucune indication du programmeur.
volatile Ce mot clé indique au compilateur que la valeur de la variable peut changer à tout
moment. Cela signifie que le compilateur ne peut faire aucune hypothèse sur le
moment où la variable est modifiée. Souvent, un compilateur peut stocker
temporairement la valeur d'une variable dans un registre et utiliser le registre à la
place de la variable dans une section de code. Il ne peut pas faire ces types
d'optimisations avec des variables volatiles. Un exemple courant de variable volatile
serait qu'une pourrait être modifiée par deux threads d'un programme multithread.
Considérez le code suivant :
1x = 10 ; 2
ans = 20 ;
3 z = x ;
Si x peut être modifié par un autre thread, il est possible que l'autre thread change x
entre les lignes 1 et 3 de sorte que z ne soit pas 10.
Cependant, si x n'a pas été déclaré volatile, le compilateur peut supposer que x est
inchangé et définir z sur 10.
Une autre utilisation de volatile est d'empêcher le compilateur d'utiliser un registre
pour une variable.
Machine Translated by Google
94 CHAPITRE 4. SOUSPROGRAMMES
Machine Translated by Google
Chapitre 5
Tableaux
5.1 Présentation
Un tableau est un bloc contigu de liste de données en mémoire. Chaque élément
de la liste doit être du même type et utiliser exactement le même nombre d'octets de
mémoire pour le stockage. En raison de ces propriétés, les tableaux permettent un
accès efficace aux données par leur position (ou index) dans le tableau. L'adresse de
n'importe quel élément peut être calculée en connaissant trois faits :
• L'adresse du premier élément du tableau.
• Le nombre d'octets dans chaque élément
• L'indice de l'élément
Il est commode de considérer l'indice du premier élément du tableau comme étant
nul (comme en C). Il est possible d'utiliser d'autres valeurs pour le premier indice, mais
cela complique les calculs.
5.1.1 Définition des tableaux
Définir des tableaux dans les segments data et bss
Pour définir un tableau initialisé dans le segment de données, utilisez les directives
normales db, dw, etc. NASM fournit également une directive utile nommée TIMES qui
peut être utilisée pour répéter une instruction plusieurs fois sans avoir à dupliquer les
instructions à la main. La figure 5.1 en montre plusieurs exemples.
Pour définir un tableau non initialisé dans le segment bss, utilisez les directives
resb, resw, etc. N'oubliez pas que ces directives ont un opérande qui spécifie le
nombre d'unités de mémoire à réserver. La figure 5.1 montre également des exemples
de ces types de définitions.
95
Machine Translated by Google
96 CHAPITRE 5. TABLEAUX
1 segment .données
2 ; définir un tableau de 10 mots doubles initialisés à 1,2,..,10
3 a1 jj 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
4 ; définir un tableau de 10 mots initialisé à 0
5 a2 dw 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
6 ; comme avant d'utiliser TIMES
7 a3 fois 10 dw 0
8 ; définir un tableau d'octets avec 200 0, puis 100 1
9 a4 fois 200 db 0
dix fois 100 db 1
11
12 segments .bss
13 ; définir un tableau de 10 mots doubles non initialisés
14 a5 resd 10
15 ; définir un tableau de 100 mots non initialisés
16 a6 resw 100
Figure 5.1 : Définition des tableaux
Définir des tableaux en tant que variables locales sur la pile
Il n'existe aucun moyen direct de définir une variable tableau locale sur la pile.
Comme précédemment, on calcule le nombre total d'octets requis par toutes les variables locales,
y compris les tableaux, et le soustrait d'ESP (soit directement, soit en utilisant le
ENTREE). Par exemple, si une fonction avait besoin d'une variable de caractère,
deux entiers de mots doubles et un tableau de mots de 50 éléments, il faudrait
1 + 2 × 4 + 50 × 2 = 109 octets. Cependant, le nombre soustrait de l'ESP
doit être un multiple de quatre (112 dans ce cas) pour garder ESP sur un mot double
frontière. On pourrait organiser les variables à l'intérieur de ces 109 octets en plusieurs
façons. La figure 5.2 montre deux manières possibles. La partie inutilisée du premier
l'ordre est là pour garder les mots doubles sur les limites des mots doubles pour
accélérer les accès mémoire.
5.1.2 Accéder aux éléments des tableaux
Il n'y a pas d'opérateur [ ] en langage assembleur comme en C. Pour accéder à un
élément d'un tableau, son adresse doit être calculée. Considérer ce qui suit
deux définitions de tableau :
Voici quelques exemples utilisant ces tableaux :
Machine Translated by Google
5.1. INTRODUCTION 97
EBP 1 carboniser
inutilisé
EBP 8 dmot 1
EBP 12 dword 2 mot
déployer
mot
déployer EBP 100
EBP 104 dword 1
EBP 108 dword 2
EBP109 carboniser
EBP112 inutilisé
Figure 5.2 : Dispositions de la pile
1 mouvement
al, [matrice1] al, ; al = tableau1[0]
2 mouvement
[matrice1 + 1] [matrice1 ; al = tableau1[1]
3 mouvement
+ 3], al ax, [matrice2] ax, ; tableau1[3] = al
4 mouvement
[matrice2 + 2] ; hache = array2[0]
5 mouvement
[matrice2 + 6], ax ax, ; ax = array2[1] (PAS array2[2] !)
6 mouvement
[matrice2 + 1] ; tableau2[3] = axe
7 mouvement ; hache = ??
A la ligne 5, l'élément 1 du tableau de mots est référencé, pas l'élément 2. Pourquoi ?
Les mots sont des unités de deux octets, donc pour passer à l'élément suivant d'un tableau de mots,
il faut avancer de deux octets, pas d'un. La ligne 7 lira un octet du
premier élément et un du second. En C, le compilateur regarde le type
d'un pointeur pour déterminer le nombre d'octets à déplacer dans une expression qui
utilise l'arithmétique des pointeurs pour que le programmeur n'ait pas à le faire. Cependant,
en assembleur, c'est au programmeur de prendre la taille des éléments du tableau dans
compte lors du passage d'un élément à l'autre.
La figure 5.3 montre un extrait de code qui ajoute tous les éléments de array1
dans l'exemple de code précédent. À la ligne 7, AX est ajouté à DX. Pourquoi pas
AL? Tout d'abord, les deux opérandes de l'instruction ADD doivent avoir la même taille.
Deuxièmement, il serait facile d'additionner des octets et d'obtenir une somme trop importante
tenir dans un octet. En utilisant DX, des sommes jusqu'à 65 535 sont autorisées. Cependant, il
est important de réaliser que AH est également ajouté. C'est pourquoi AH est défini
à zéro1 à la ligne 3.
Les figures 5.4 et 5.5 montrent deux façons alternatives de calculer la somme. Le
les lignes en italique remplacent les lignes 6 et 7 de la figure 5.3.
1Régler AH à zéro suppose implicitement que AL est un nombre non signé. Si c'est
signé, l'action appropriée serait d'insérer une instruction CBW entre les lignes 6 et 7
Machine Translated by Google
98 CHAPITRE 5. TABLEAUX
1 mouvement
ebx, array1 dx, ; ebx = adresse du tableau1
2 mouvement 0 ah, ; dx tiendra la somme
3 mouvement 0 ecx, ; ?
4 mouvement 5
5 lp :
6 mouvement al, [ebx] dx, ; al = *ebx
7 ajouter ax ebx ; dx += hache (pas al !)
8 inc. ; bx++
9 boucle LP
Figure 5.3 : Sommation des éléments d'un tableau (Version 1)
1 mouvement
ebx, array1 dx, ; ebx = adresse du tableau1
2 mouvement 0 ecx, ; dx tiendra la somme
3 mouvement 5
4 lp :
5 ajouter dl, [ebx] ; dl += *ebx
6 jnc suivant ; si pas de report aller au suivant
7 inc dh ; dh inc
8 suivant :
9 inc. ebx ; bx++
dix boucle LP
Figure 5.4 : Sommation des éléments d'un tableau (Version 2)
5.1.3 Adressage indirect plus avancé
Sans surprise, l'adressage indirect est souvent utilisé avec les tableaux. Le plus
forme générale d'une référence mémoire indirecte est :
[ base reg + facteur *index reg + constante ]
où:
base reg est l'un des registres EAX, EBX, ECX, EDX, EBP, ESP, ESI ou
EDI.
le facteur est soit 1, 2, 4 ou 8. (Si 1, le facteur est omis.)
index reg est l'un des registres EAX, EBX, ECX, EDX, EBP, ESI, EDI.
(Notez que ESP n'est pas dans la liste.)
Machine Translated by Google
5.1. INTRODUCTION 99
1 mouvement
ebx, array1 dx, ; ebx = adresse du tableau1
2 mouvement 0 ecx, 5 ; dx tiendra la somme
3 mouvement
4 lp :
5 ajouter dl, [ebx] dh, 0 ; dl += *ebx
6 adc ; dh += porter le drapeau + 0
7 inc. ebx ; bx++
8 boucle LP
Figure 5.5 : Sommation des éléments d'un tableau (Version 3)
constante est une constante 32 bits. La constante peut être une étiquette (ou une étiquette
expression).
5.1.4 Exemple
Voici un exemple qui utilise un tableau et le transmet à une fonction. Il
utilise le programme array1c.c (listé cidessous) comme pilote, pas le pilote.c
programme.
tableau1.asm
1 %définir ARRAY_SIZE 100
2 %définir NEW_LINE 10
3
Données à 4 segments
5 PremierMsg 6 db "10 premiers éléments du tableau", 0
Invite 7 db "Entrez l'index de l'élément à afficher : ", 0
SecondMsg 8 db "L'élément %d est %d", NEW_LINE, 0
TroisièmeMsg 9 db "Éléments 20 à 29 du tableau", 0
InputFormat db "%d", 0
dix
11 segments .bss
12 tableaux resd ARRAY_SIZE
13
14 segments .texte
15 extern _puts, _printf, _scanf, _dump_line
16 global_asm_main
17 _asm_main :
18 entrez 4,0 ; variable dword locale à EBP 4
19 poussez ebx
20 pousser esi
21
Machine Translated by Google
100 CHAPITRE 5. TABLEAUX
22 ; initialiser le tableau à 100, 99, 98, 97, ...
23
24 mouvement exx, ARRAY_SIZE
25 mouvement
ebx, tableau
26 init_loop :
27 mouvement [ebx], ecx
28 ajouter ebx, 4
29 boucle init_loop
30
33 populaire
exx
34
35 poussez, dword 10
36 poussez tableau dword
37 appel _print_array esp, 8 ; imprimer les 10 premiers éléments du tableau
38 ajouter
39
40 ; demander à l'utilisateur d'indiquer l'index de l'élément
41 Boucle_invite :
42 pousser Invite dword
43 l'appel _printf
44 populaire
exx
45
54 appel _dump_line ; vider le reste de la ligne et recommencer
55 jmp Boucle_invite ; si saisie invalide
56
57 EntréeOK :
58 mouvement
esi, [ebp4]
59 pousser dword [tableau + 4*esi]
60 pousser esi
61 pousser dword SecondMsg ; imprimer la valeur de l'élément
62 appeler _printf
63 ajouter esp, 12
Machine Translated by Google
5.1. INTRODUCTION 101
64
65 pousser dword ThirdMsg _puts ; imprimer les éléments 2029
66 appel
67 populaire
exx
68
69 pousser dword 10
70 poussez le tableau dword + 20*4 ; adresse du tableau[20]
71 appelez _print_array
72 ajouter esp, 8
73
74 populaire
esi
75 populaire
ebx
76 mouvement eax, 0 ; revenir à C
77 partir
78 ret
79
80 ;
81 ; routine _print_array
82 ; Routine appelable en C qui imprime les éléments d'un tableau de mots doubles comme
83 ; entiers signés.
84 ; Prototype C :
85 ; void print_array(const int * a, int n);
86 ; Paramètres:
87 ; a pointeur vers le tableau à imprimer (à ebp+8 sur la pile)
88 ; n nombre d'entiers à imprimer (à ebp+12 sur la pile)
89
Données de 90 segments
91 Format de sortie db "%5d %5d", NEW_LINE, 0
92
93 segments .texte
94 _print_array global
95_print_array :
96 entrez 0,0
97 pousser esi
98 pousser ebx
99
102 CHAPITRE 5. TABLEAUX
116 populaire
ebx
117 pop esi
118 congé
119 ret
tableau1.asm
tableau1c.c
1 #include <stdio.h>
2
3 int asm main( void );
4 ligne de vidage vide ( vide );
5
6 entier principal()
7 {
8 int ret statut ;
9 ret status = asm main();
dix retourner l'état ret ;
11 }
12
13 /
14 ligne de vidage de fonction
15 vide tous les caractères restants dans la ligne actuelle du tampon d'entrée
16 /
17 ligne de vidage vide ()
18 {
19 int ch ;
20
21 tandis que( (ch = getchar()) != EOF && ch != '\n')
22 / corps nul / ;
23 }
tableau1c.c
Machine Translated by Google
5.1. INTRODUCTION 103
La consigne LEA revisitée
L'instruction LEA peut être utilisée à d'autres fins que le simple calcul
adresses. Un assez commun est pour les calculs rapides. Prendre en compte
suivant:
léa ebx, [4*eax + eax]
Cela stocke effectivement la valeur de 5 × EAX dans EBX. Utiliser LEA pour faire ça
est à la fois plus facile et plus rapide que d'utiliser MUL. Cependant, il faut se rendre compte que le
l'expression entre crochets doit être une adresse indirecte légale. Ainsi,
par exemple, cette instruction ne peut pas être utilisée pour multiplier par 6 rapidement.
5.1.5 Tableaux multidimensionnels
Les tableaux multidimensionnels ne sont pas vraiment très différents du simple
tableaux dimensionnels déjà discutés. En fait, ils sont représentés dans la mémoire comme
un simple tableau unidimensionnel.
Tableaux à deux dimensions
Sans surprise, le tableau multidimensionnel le plus simple est un tableau bidimensionnel.
Un tableau à deux dimensions est souvent affiché sous la forme d'une grille d'éléments.
Chaque élément est identifié par une paire d'indices. Par convention, le premier indice
est identifié avec la ligne de l'élément et le second index la colonne.
Considérez un tableau avec trois lignes et deux colonnes définies comme suit :
int a [3][2] ;
Le compilateur C réserverait de la place pour un tableau et une carte d'entiers 6 (= 2 × 3)
les éléments comme suit :
Indice 0 1 2 3 4 5
Élément a[0][0] a[0][1] a[1][0] a[1][1] a[2][0] a[2][1]
Ce que le tableau tente de montrer, c'est que l'élément référencé comme a[0][0]
est stocké au début du tableau unidimensionnel à 6 éléments. Élément
a[0][1] est stocké à la position suivante (index 1) et ainsi de suite. Chaque rangée du
un tableau à deux dimensions est stocké de manière contiguë dans la mémoire. Le dernier élément
d'une ligne est suivi du premier élément de la ligne suivante. Ceci est connu
comme représentation par ligne du tableau et comment un compilateur C/C++
représenterait le tableau.
Comment le compilateur déterminetil où a[i][j] apparaît dans le rowwise
représentation? Une formule simple calculera l'indice à partir de i et j. Le
formule dans ce cas est 2i + j. Il n'est pas trop difficile de voir comment cette formule est
dérivé. Chaque ligne est longue de deux éléments ; donc, le premier élément de la ligne i est
en position 2i. Ensuite, la position de la colonne j est trouvée en ajoutant j à 2i.
Machine Translated by Google
104 CHAPITRE 5. TABLEAUX
1 mouvement
eax, [ebp 44] ; ebp 44 est mon emplacement
2 sel eax, 1 ; multiple de i par 2
3 ajouter eax, [ebp 48] ; ajouter j
4 mouvement
eax, [ebp + 4*eax 40] ; ebp 40 est l'adresse de a[0][0]
5 mouvement
[ebp 52], eax ; stocker le résultat dans x (à ebp 52)
Figure 5.6 : Assemblage pour x = a[i ][ j ]
Cette analyse montre également comment la formule est généralisée à un tableau avec N
colonnes : N ×i+j. Notez que la formule ne dépend pas du nombre
de rangées.
A titre d'exemple, voyons comment gcc compile le code suivant (en utilisant le
tableau a défini cidessus) :
x = a[i ][ j ] ;
La figure 5.6 montre l'assemblage dans lequel ceci est traduit. Ainsi, le compilateur
convertit essentiellement le code en :
et en fait, le programmeur pourrait écrire de cette façon avec le même résultat.
Il n'y a rien de magique dans le choix de la représentation en ligne
du tableau. Une représentation par colonne fonctionnerait tout aussi bien :
Indice 0 1 2 3 4 5
Élément a[0][0] a[1][0] a[2][0] a[0][1] a[1][1] a[2][1]
Dans la représentation par colonne, chaque colonne est stockée de manière contiguë. L'élément
[i][j] est stocké à la position i + 3j. Autres langages (FORTRAN,
par exemple) utilisez la représentation par colonne. Ceci est important lorsque
code d'interface avec plusieurs langues.
Dimensions supérieures à deux
Pour les dimensions supérieures à deux, la même idée de base est appliquée. Envisagez un
tableau tridimensionnel :
int b [4][3][2] ;
Ce tableau serait stocké comme s'il s'agissait de quatre tableaux bidimensionnels chacun de
size [3][2] consécutivement en mémoire. Le tableau cidessous montre comment cela commence
dehors:
Machine Translated by Google
5.1. INTRODUCTION 105
Indice 2 0 1 3 4 5
Élément b[0][0][0] b[0][0][1] b[0][1][0] b[0][1][1] b[0][2][0 ] b[0][2][1]
Indice 6 7 8 9 dix 11
Élément b[1][0][0] b[1][0][1] b[1][1][0] b[1][1][1] b[1][2][0 ] b[1][2][1]
La formule pour calculer la position de b[i][j][k] est 6i + 2j + k. Le
6 est déterminé par la taille des tableaux [3][2]. En général, pour un tableau de dimension a[L]
[M][N] la position de l'élément a[i][j][k] sera
M × N × je + N × j + k. Remarquez à nouveau que la première dimension (L) ne
apparaissent dans la formule.
Pour des dimensions supérieures, le même processus est généralisé. Pour un tableau à
n dimensions de dimensions D1 à Dn, la position de l'élément désignée par la
indices i1 à in est donné par la formule :
D2 × D3 ∙ ∙ ∙ × Dn × i1 + D3 × D4 ∙ ∙ ∙ × Dn × i2 + ∙ ∙ ∙ + Dn × in−1 + in
ou pour le geek uber maths, il peut être écrit plus succinctement comme suit :
n n
Dk ij
j=1 k=j+1
La première dimension, D1, n'apparaît pas dans la formule. C'est là que vous pouvez dire
Pour la représentation en colonnes, la formule générale serait : l'auteur était un physicien
majeur. (Ou la référence à
i1 + D1 ×i2 +∙ ∙ ∙+ D1 × D2 × ∙ ∙ ∙ × Dn−2 ×in−1 + D1 × D2 × ∙ ∙ ∙ × Dn−1 ×in FORTRAN étaitelle
révélatrice ?)
ou en notation ̈uber math geek :
n j1
Dk ij
j=1 k=1
Dans ce cas, c'est la dernière dimension, Dn, qui n'apparaît pas dans la formule.
Passage de tableaux multidimensionnels en tant que paramètres en C
La représentation en ligne des tableaux multidimensionnels a un effet direct
en programmation C. Pour les tableaux unidimensionnels, la taille du tableau n'est pas
nécessaire pour calculer où se trouve un élément spécifique en mémoire. C'est
pas vrai pour les tableaux multidimensionnels. Pour accéder aux éléments de ces tableaux,
le compilateur doit tout connaître sauf la première dimension. Cela devient apparent
lorsque l'on considère le prototype d'une fonction qui prend une dimension multidimensionnelle
tableau en paramètre. Ce qui suit ne compilera pas :
void f ( int a [ ][ ] ); / aucune information dimensionnelle /
Machine Translated by Google
106 CHAPITRE 5. TABLEAUX
Cependant, ce qui suit compile :
void f ( int a [ ][2] );
Tout tableau à deux dimensions avec deux colonnes peut être passé à cette fonction.
La première dimension n'est pas requise2 .
Ne vous laissez pas confondre par une fonction avec ce prototype :
vide f ( int a [ ] );
Cela définit un tableau unidimensionnel de pointeurs d'entiers (qui peut d'ailleurs être utilisé
pour créer un tableau de tableaux qui agit un peu comme un tableau à deux dimensions).
Pour les tableaux de dimension supérieure, toutes les dimensions du tableau sauf la
première doivent être spécifiées pour les paramètres. Par exemple, un paramètre de tableau
à quatre dimensions peut être passé comme :
void f ( int a [ ][4][3][2] );
5.2 Instructions de tableau/chaîne
La famille de processeurs 80x86 fournit plusieurs instructions conçues pour fonctionner
avec des tableaux. Ces instructions sont appelées instructions de chaîne.
Ils utilisent les registres d'index (ESI et EDI) pour effectuer une opération puis pour incrémenter
ou décrémenter automatiquement un ou les deux registres d'index. Le drapeau de direction
(DF) dans le registre FLAGS détermine où les registres d'index sont incrémentés ou
décrémentés. Deux instructions modifient le drapeau de direction :
CLD efface le drapeau de direction. Dans cet état, les registres d'index sont incre
menté.
STD définit le drapeau de direction. Dans cet état, les registres d'index sont décrétés
menté.
Une erreur très courante dans la programmation 80x86 est d'oublier de mettre explicitement
le drapeau de direction dans l'état correct. Cela conduit souvent à un code qui fonctionne la
plupart du temps (lorsque le drapeau de direction se trouve dans l'état souhaité), mais qui ne
fonctionne pas tout le temps.
5.2.1 Lecture et écriture de la mémoire
Les instructions de chaîne les plus simples lisent ou écrivent la mémoire ou les deux.
Ils peuvent lire ou écrire un octet, un mot ou un double mot à la fois. Figure 5.7
2Une taille peut être spécifiée ici, mais elle est ignorée par le compilateur.
Machine Translated by Google
5.2. INSTRUCTIONS DE TABLEAU/CHAÎNE 107
LODSB AL = [DS:ESI] STOSB [ES:EDI] = AL
ESI = ESI ± 1 EDI = EDI ± 1
LODSW AX = [DS:ESI] STOSW [ES:EDI] = AX
ESI = ESI ± 2 EDI = EDI ± 2
LODSD EAX = [DS:ESI] STOSD [ES:EDI] = EAX
ESI = ESI ± 4 EDI = EDI ± 4
Figure 5.7 : Lecture et écriture d'instructions de chaîne
1 segment .data 2
array1 dd 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
3
4 segments .bss 5
array2 resd 10
6
7 segments .text cld
8
; n'oubliez pas ça !
9 mouvement
esi, array1 edi,
dix mouvement
array2 ecx, 10
11 mouvement
12 lp :
13 lodsd
14 stosd
15 boucle LP
Figure 5.8 : Exemple de chargement et de stockage
montre ces instructions avec une courte description en pseudocode de ce qu'elles font. Il
y a plusieurs points à remarquer ici. Premièrement, ESI est utilisé pour la lecture et EDI
pour l'écriture. Il est facile de s'en souvenir si l'on se souvient que SI signifie Source Index
et DI signifie Destination Index. Ensuite, notez que le registre qui contient les données est
fixe (soit AL, AX ou EAX). Enfin, notez que les instructions de stockage utilisent ES pour
déterminer le segment sur lequel écrire, et non DS. Dans la programmation en mode
protégé, ce n'est généralement pas un problème, car il n'y a qu'un seul segment de
données et ES doit être automatiquement initialisé pour le référencer (tout comme DS).
Cependant, dans la programmation en mode réel, il est très important pour le programmeur
d'initialiser ES à la valeur correcte du sélecteur de segment3 . La figure 5.8 montre un
exemple d'utilisation de ces instructions qui
3Une autre complication est qu'on ne peut pas copier la valeur du registre DS dans le registre
ES directement en utilisant une seule instruction MOV. Au lieu de cela, la valeur de DS doit être
copiée dans un registre à usage général (comme AX), puis copiée de ce registre vers ES à l'aide de deux
Machine Translated by Google
108 CHAPITRE 5. TABLEAUX
Octet MOVSB [ES:EDI] = octet [DS:ESI]
ESI = ESI ± 1
EDI = EDI ± 1
MOVSW mot [ES:EDI] = mot [DS:ESI]
ESI = ESI ± 2
EDI = EDI ± 2
MOVSD dword [ES:EDI] = dword [DS:ESI]
ESI = ESI ± 4
EDI = EDI ± 4
Figure 5.9 : Instructions de chaîne de déplacement de mémoire
1 segment .bss 2
tableau resd 10
3
4 segments .text cld
5 ; n'oubliez pas ça !
6 mouvement
edi, tableau
7 mouvement ecx, 10
8 xor eax, eax
9 représentant stosd
Figure 5.10 : Exemple de tableau zéro
copie un tableau dans un autre.
La combinaison d'une instruction LODSx et STOSx (comme dans les lignes 13 et 14
de la Figure 5.8) est très courante. En fait, cette combinaison peut être effectuée par une
seule instruction de chaîne MOVSx. La figure 5.9 décrit les opérations effectuées par ces
instructions. Les lignes 13 et 14 de la figure 5.8 pourraient être remplacées par une seule
instruction MOVSD avec le même effet. La seule différence serait que le registre EAX ne
serait pas du tout utilisé dans la boucle.
5.2.2 Le préfixe de l'instruction REP
La famille 80x86 fournit un préfixe d'instruction spécial4 appelé REP qui peut être utilisé
avec les instructions de chaîne cidessus. Ce préfixe indique au processeur de répéter
l'instruction de chaîne suivante un nombre de fois spécifié. L'ECX
Instructions MOV.
4Un préfixe d'instruction n'est pas une instruction, c'est un octet spécial qui est placé avant une instruction
chaîne qui modifie le comportement des instructions. D'autres préfixes sont également utilisés pour remplacer
les valeurs par défaut des segments d'accès à la mémoire
Machine Translated by Google
5.2. INSTRUCTIONS DE TABLEAU/CHAÎNE 109
CMPSB compare l'octet [DS:ESI] et l'octet [ES:EDI]
ESI = ESI ± 1
EDI = EDI ± 1
CMPSW compare le mot [DS:ESI] et le mot [ES:EDI]
ESI = ESI ± 2
EDI = EDI ± 2
CMPSD compare dword [DS:ESI] et dword [ES:EDI]
ESI = ESI ± 4
EDI = EDI ± 4
SCASB compare AL et [ES:EDI]
EDI ± 1
SCASW compare AX et [ES:EDI]
EDI ± 2
SCASD compare EAX et [ES:EDI]
EDI ± 4
Figure 5.11 : Instructions de chaîne de comparaison
registre est utilisé pour compter les itérations (comme pour l'instruction LOOP).
En utilisant le préfixe REP, la boucle de la Figure 5.8 (lignes 12 à 15) pourrait être remplacée
par une seule ligne :
représentant movsd
La figure 5.10 montre un autre exemple qui met à zéro le contenu d'un tableau.
5.2.3 Instructions de chaîne de comparaison
La figure 5.11 montre plusieurs nouvelles instructions de chaîne qui peuvent être
utilisées pour comparer la mémoire avec une autre mémoire ou un registre. Ils sont utiles
pour comparer ou rechercher des tableaux. Ils définissent le registre FLAGS comme
l'instruction CMP. Les instructions CMPSx comparent les emplacements de mémoire
correspondants et les emplacements de mémoire de balayage SCASx pour une valeur spécifique.
La figure 5.12 montre un extrait de code court qui recherche le nombre 12 dans un
tableau de mots doubles. L'instruction SCASD de la ligne 10 ajoute toujours 4 à EDI,
même si la valeur recherchée est trouvée. Ainsi, si l'on veut trouver l'adresse des 12
trouvés dans le tableau, il faut soustraire 4 à EDI (comme le fait la ligne 16).
5.2.4 Les préfixes des instructions REPx
Il existe plusieurs autres préfixes d'instructions de type REP qui peuvent être utilisés
avec les instructions de chaîne de comparaison. La figure 5.13 montre les deux nouveaux
Machine Translated by Google
110 CHAPITRE 5. TABLEAUX
1 segment .bss
2 tableaux rouge 100
4 segments .text
5 CLD
6 mouvement
edi, tableau ; pointeur vers le début du tableau
7 mouvement ecx, 100 ; nombre d'éléments
8 mouvement eax, 12 ; numéro à scanner
9 livres :
dix scasd
11 je trouvé
12 boucle lp
13 ; code à exécuter si introuvable
14 jmp en avant
15 trouvé :
16 sous édi, 4 ; ; edi pointe maintenant vers 12 dans le tableau
17 code à exécuter si trouvé
18 ans et plus :
Figure 5.12 : Exemple de recherche
REPE, REPZ répète l'instruction pendant que le drapeau Z est défini ou au plus ECX fois
REPNE, REPNZ répète l'instruction pendant que le drapeau Z est effacé ou au plus ECX
fois
Figure 5.13 : Préfixes des instructions REPx
préfixes et décrit leur fonctionnement. REPE et REPZ ne sont que des synonymes
pour le même préfixe (comme le sont REPNE et REPNZ). Si la comparaison répétée
l'instruction de chaîne s'arrête à cause du résultat de la comparaison, l'index
le ou les registres sont toujours incrémentés et ECX décrémenté ; cependant,
le registre FLAGS contient toujours l'état qui a mis fin à la répétition.
Pourquoi ne peuton pas simplement regarder Ainsi, il est possible d'utiliser le drapeau Z pour déterminer si les comparaisons répétées
pour voir si ECX est nul après arrêté en raison d'une comparaison ou ECX devient zéro.
la comparaison répétée?
La figure 5.14 montre un exemple d'extrait de code qui détermine si deux blocs
de mémoire sont égaux. Le JE à la ligne 7 de l'exemple vérifie pour voir le résultat
de la consigne précédente. Si la comparaison répétée s'est arrêtée parce qu'elle
trouvé deux octets inégaux, le drapeau Z sera toujours effacé et aucune branche n'est
fait; cependant, si les comparaisons s'arrêtaient parce que ECX devenait zéro, le
L'indicateur Z sera toujours défini et le code se branche sur l'étiquette égale.
Machine Translated by Google
5.2. INSTRUCTIONS DE TABLEAU/CHAÎNE 111
1 segment .texte
2 CLD
3 mouvement esi, bloc1 ; adresse du premier bloc
4 mouvement edi, bloc2 ; adresse du deuxième bloc
5 mouvement ecx, taille ; taille des blocs en octets
6 rep cmpsb ; répéter pendant que le drapeau Z est défini
7 je égal ; si Z défini, blocs égaux
8 ; code à exécuter si les blocs ne sont pas égaux
9 jmp en avant
10 égal :
11 ; code à exécuter si égal
12 et suivants :
Figure 5.14 : Comparaison des blocs de mémoire
5.2.5 Exemple
Cette section contient un fichier source d'assemblage avec plusieurs fonctions qui
mettre en œuvre des opérations de tableau à l'aide d'instructions de chaîne. De nombreuses fonctions
dupliquer les fonctions familières de la bibliothèque C.
mémoire.asm
1 global _asm_copy, _asm_find, _asm_strlen, _asm_strcpy
2
3 segments .text
4 ; fonction _asm_copy
5 ; copie des blocs de mémoire
6 ; Prototype C
7 ; void asm_copy( void * dest, const void * src, sz non signé);
8 ; paramètres:
9 ; dest pointeur vers le tampon vers lequel copier
dix ; src pointeur vers le tampon à partir duquel copier
11 ; sz nombre d'octets à copier
12
13 ; ensuite, quelques symboles utiles sont définis
14
15 % définir la destination [ebp+8]
16 %définir src [ebp+12]
17 %définir sz [ebp+16]
18 _asm_copy :
19 entrez 0, 0
20 pousser esi
Machine Translated by Google
112 CHAPITRE 5. TABLEAUX
21 pousser édi
22
27 CLD ; drapeau de direction clair
28 représentant
movsb ; exécuter movsb ECX fois
29
30 populaire
édi
31 pop esi
32 congé
33 ret
34
35
36 ; fonction _asm_find
37 ; recherche dans la mémoire un octet donné
38 ; void * asm_find( const void * src, cible char, sz non signé);
39 ; paramètres:
40 ; src pointeur vers le tampon pour rechercher
41 ; cible valeur d'octet à rechercher
42 ; sz nombre d'octets dans le tampon
43 ; valeur de retour :
44 ; si la cible est trouvée, pointeur vers la première occurrence de la cible dans le tampon
45 ; est retourné
46 ; autre
47 ; NULL est retourné
48 ; REMARQUE : la cible est une valeur d'octet, mais est poussée sur la pile en tant que valeur dword.
49 ; La valeur d'octet est stockée dans les 8 bits inférieurs.
50 ;
51 % définir src [ebp+8]
52 %définir la cible [ebp+12]
53 %définir sz [ebp+16]
54
55 _asm_find :
56 entrez 0,0
57 pousser edi
58
59 mouvement
eax, cible edi, src ; al a une valeur à rechercher
60 mouvement
61 mouvement ex, sz
62 CLD
Machine Translated by Google
5.2. INSTRUCTIONS DE TABLEAU/CHAÎNE 113
63
64 croûte de repne ; scanner jusqu'à ECX == 0 ou [ES:EDI] == AL
65
66 je found_it eax, ; si l'indicateur zéro est défini, alors la valeur trouvée
67 mouvement 0 bref ; s'il n'est pas trouvé, renvoie le pointeur NULL
68 jmp quitter
69 trouvé_it :
70 mouvement eax, édi
71 déc eax ; si trouvé retour (DI 1)
72 quitter :
73 pop édi
74 congé
75 ret
76
77
78 ; fonction _asm_strlen
79 ; renvoie la taille d'une chaîne
80 ; asm_strlen( const char * );
81 ; paramètre:
82 ; src pointeur vers la chaîne
83 ; valeur de retour :
84 ; nombre de caractères dans la chaîne (sans compter, se terminant par 0) (en EAX)
85
86 % définir src [ebp + 8]
87_asm_strlen :
88 entrez 0,0
89 pousser edi
90
96 fourreau de repnz ; scan pour terminer 0
97
98 ;
99 ; repnz ira un peu trop loin, donc la longueur est FFFFFFFE ECX,
100 ; pas FFFFFFFF ECX
101 ;
102 mouvement eax,0FFFFFFFEh
103 sous eax, ecx ; longueur = 0FFFFFFFEh ecx
104
Machine Translated by Google
114 CHAPITRE 5. TABLEAUX
109 ; fonction _asm_strcpy
110 ; copie une chaîne
111 ; void asm_strcpy( char * dest, const char * src);
112 ; paramètres:
113 ; dest pointeur vers la chaîne à copier
114 ; src pointeur vers la chaîne à partir de laquelle copier
115 ;
116 % définir la destination [ebp + 8]
117 % définir src [ebp + 12]
118_asm_strcpy :
119 entrez 0,0
120 poussez, esi
121 poussez édi
122
132 populaire
édi
133 pop esi
134 congé
135 ret
mémoire.asm
memex.c
1 #include <stdio.h>
2
3 #define STR TAILLE 30
4 / prototypes /
5
5.2. INSTRUCTIONS DE TABLEAU/CHAÎNE 115
9 asm non signé strlen( const char ) attribut ((cdecl));
10 void asm strcpy( char , const char ) attribut ((cdecl));
11
12 entier principal()
13 {
14 char st1[STR SIZE] = "chaîne de test" ;
15 char st2[TAILLE FORME] ;
16 caractère st ;
17 caractère ch ;
18
22 printf(”Entrez un caractère : ” ); / recherche l'octet dans la chaîne /
23 scanf("%c% [ˆ\n]", &ch);
24 st = asm find(st2 , ch, STR SIZE);
25 si (st)
26 printf ("Trouvé : %s\n", st );
27 autre
28 printf ("Non trouvé\n");
29
30 st1 [0] = 0 ;
31 printf(”Entrez la chaîne :”);
32 scanf("%s", st1 );
33 printf(”len = %u\n”, asm strlen(st1 ));
34
38 renvoie 0 ;
39 }
memex.c
Machine Translated by Google
116 CHAPITRE 5. TABLEAUX
Machine Translated by Google
Chapitre 6
Point flottant
6.1 Représentation en virgule flottante
6.1.1 Nombres binaires non entiers
Lorsque les systèmes numériques ont été discutés dans le premier chapitre, seules les
valeurs entières ont été discutées. Évidemment, il doit être possible de représenter des
nombres non entiers dans d'autres bases que décimales. En décimal, les chiffres à droite
de la virgule ont des puissances négatives associées de dix :
0,123 = 1 × 10−1 + 2 × 10−2 + 3 × 10−3
Sans surprise, les nombres binaires fonctionnent de la même manière :
Cette idée peut être combinée avec les méthodes entières du chapitre 1 pour convertir un
nombre général :
110,0112 = 4 + 2 + 0,25 + 0,125 = 6,375
La conversion du décimal au binaire n'est pas très difficile non plus. En général, divisez
le nombre décimal en deux parties : entier et fraction. Convertissez la partie entière en
binaire en utilisant les méthodes du chapitre 1. La partie fractionnaire est convertie en
utilisant la méthode décrite cidessous.
Considérons une fraction binaire avec les bits étiquetés a, b, c, . . . Le nombre
en binaire ressemble alors à :
0.abcdef . . .
Multipliez le nombre par deux. La représentation binaire du nouveau nombre sera :
a B c d e F . . .
117
Machine Translated by Google
118 CHAPITRE 6. POINT FLOTTANT
0,5625 × 2 = 1,125 premier bit = 1
0,125 × 2 = 0,25 deuxième bit = 0
0,25 × 2 = 0,5 troisième bit = 0
0,5 × 2 = 1,0 quatrième bit = 1
Figure 6.1 : Conversion de 0,5625 en binaire
0,85 × 2 = 1,7
0,7 × 2 = 1,4
0,4 × 2 = 0,8
0,8 × 2 = 1,6
0,6 × 2 = 1,2
0,2 × 2 = 0,4
0,4 × 2 = 0,8
0,8 × 2 = 1,6
Figure 6.2 : Conversion de 0,85 en binaire
Notez que le premier bit est maintenant à sa place. Remplacez le a par 0 pour obtenir :
0.bcdef . . .
et multipliez encore par deux pour obtenir :
b.cdef . . .
Maintenant, le deuxième bit (b) est dans la position un. Cette procédure peut être répétée jusqu'à
ce que le nombre de bits nécessaires soit trouvé. La figure 6.1 montre un exemple réel qui
convertit 0,5625 en binaire. La méthode s'arrête lorsqu'une partie fractionnaire de zéro est
atteinte.
Comme autre exemple, envisagez de convertir 23,85 en binaire. Il est facile de convertir la
partie intégrale (23 = 101112), mais qu'en estil de la partie fractionnaire (0,85) ? La figure 6.2
montre le début de ce calcul. Si l'on regarde
Machine Translated by Google
6.1. REPRÉSENTATION EN POINT FLOTTANT 119
les nombres soigneusement, une boucle infinie est trouvée ! Cela signifie que 0,85 est un binaire répétitif
(par opposition à un décimal répétitif en base 10)1 . Il y a une régularité dans les nombres dans le
calcul. En regardant le modèle, on peut voir que 0,85 = 0,1101102. Ainsi, 23,85 = 10111,1101102.
Une conséquence importante du calcul cidessus est que 23,85 ne peut pas être représenté
exactement en binaire en utilisant un nombre fini de bits. (Juste ne peut pas être représenté en décimal
comme
1 3
avec un nombre fini de chiffres.) Comme le montre ce chapitre, les variables flottantes et doubles
en C sont stockées en binaire.
Ainsi, des valeurs comme 23,85 ne peuvent pas être stockées exactement dans ces variables. Seule
une approximation de 23,85 peut être stockée.
Pour simplifier le matériel, les nombres à virgule flottante sont stockés dans un format cohérent.
Ce format utilise la notation scientifique (mais en binaire, en utilisant des puissances de deux, pas de
dix). Par exemple, 23.85 ou 10111.11011001100110 . . .2 serait stocké sous :
100
1.011111011001100110 . . . × 2
(où l'exposant (100) est en binaire). Un nombre à virgule flottante normalisé
a la forme :
1.sssssssssssss × 2 eeeeee
où 1.ssssssssssss est le signifiant et eeeeeeee est l'exposant.
6.1.2 Représentation en virgule flottante IEEE
L'IEEE (Institute of Electrical and Electronic Engineers) est une organisation internationale qui a
conçu des formats binaires spécifiques pour le stockage des nombres à virgule flottante. Ce format est
utilisé sur la plupart (mais pas tous !) des ordinateurs fabriqués aujourd'hui. Souvent, il est pris en
charge par le matériel de l'ordinateur luimême. Par exemple, les coprocesseurs numériques (ou
mathématiques) d'Intel (qui sont intégrés à tous ses processeurs depuis le Pentium) l'utilisent. L'IEEE
définit deux formats différents avec des précisions différentes : simple et double précision. La simple
précision est utilisée par les variables flottantes en C et la double précision est utilisée par les variables
doubles.
Le coprocesseur mathématique d'Intel utilise également une troisième précision, plus élevée,
appelée précision étendue. En fait, toutes les données du coprocesseur luimême sont dans cette précision.
Lorsqu'il est stocké en mémoire par le coprocesseur, il est automatiquement converti en simple ou
double précision.2 La précision étendue utilise un format général légèrement différent des formats
flottant et double IEEE et ne sera donc pas abordé ici.
1
Il ne devrait pas être si surprenant qu'un nombre puisse se répéter dans une base, mais pas dans
Pensez à 2 1 3 , une autre. il se répète en décimal, mais en ternaire (base 3) ce serait 0,13.
Certains compilateurs (tels que Borland) utilisent cette précision étendue.
Cependant, d'autres compilateurs utilisent la double précision pour le double et le long double. (Ceci est
autorisé par ANSI C.)
Machine Translated by Google
120 CHAPITRE 6. POINT FLOTTANT
31 30 23 22 0
s e F
s bit de signe 0 = positif, 1 = négatif e exposant
biaisé (8 bits) = vrai exposant + 7F (127 décimal). Les valeurs 00 et FF ont une signification
particulière (voir texte). f fraction les 23 premiers bits après le
1. dans le significande.
Figure 6.3 : Simple précision IEEE
Simple précision IEEE
La virgule flottante simple précision utilise 32 bits pour coder le nombre. Il est généralement
précis à 7 chiffres décimaux significatifs. Les nombres à virgule flottante sont stockés dans un
format beaucoup plus compliqué que les nombres entiers. La figure 6.3 montre le format de base
d'un nombre simple précision IEEE. Il y a plusieurs bizarreries au format. Les nombres à virgule
flottante n'utilisent pas la représentation du complément à deux pour les nombres négatifs. Ils
utilisent une représentation de magnitude signée. Le bit 31 détermine le signe du nombre comme
indiqué.
L'exposant binaire n'est pas stocké directement. Au lieu de cela, la somme de l'exposant et
de 7F est stockée des bits 23 à 30. Cet exposant biaisé est toujours non négatif.
La partie fractionnaire suppose un significande normalisé (sous la forme 1.sssssssss).
Comme le premier bit est toujours un un, le premier n'est pas stocké ! Cela permet le stockage
d'un bit supplémentaire à la fin et augmente ainsi légèrement la précision. Cette idée est connue
sous le nom de représentation cachée.
Il faut toujours garder à l'esprit Comment 23,85 seraitil stocké ? Tout d'abord, il est positif donc le bit de signe est 0. Ensuite,
gardez à l'esprit que les octets 41 BE le véritable exposant est 4, donc l'exposant biaisé est 7F + 4 = 8316. Enfin, le CC CD peut
être interprété comme fraction est 01111101100110011001100 (rappelezvous le celui de tête est caché). différentes manières en
fonction de mettre tout cela
ensemble (pour aider à clarifier les différentes sections du flottant sur ce qu'un programme fait pointer
le format, le bit de signe et la
faction ont été soulignés et les bits avec eux! Comme les simples ont été regroupés en 4 bits nibbles) :
nombre à virgule flottante de
précision,
tant qu'entier de mot double, ils ils représentent 23,850000381, mais en
ne sont pas exactement 23,85
(puisqu'il s'agit d'un binaire 0 100 0001 1 011 1110 1100 1100 1100 11002 = 41BECCCC16
répétitif). Si un converti
représente 1 103 023 309 ! cidessus en décimal, on trouve qu'il est d'environ 23,849998474.
Le CPU ne sait pas lequel est
le bon. Ce nombre est très proche de 23,85, mais il n'est pas exact. En fait, en C, interprétation 23.85 ! ne serait pas représenté
exactement comme cidessus. Étant donné que le bit le plus à gauche qui a été tronqué de la représentation exacte est 1, le
dernier bit est arrondi à 1.
Ainsi, 23,85 serait représenté par 41 BE CC CD en hexadécimal en simple précision.
La conversion en décimal donne 23,850000381, ce qui est une approximation légèrement
meilleure de 23,85.
Machine Translated by Google
6.1. REPRÉSENTATION EN POINT FLOTTANT 121
e = 0 et f = 0 désigne le chiffre zéro (qui ne peut pas être normalisé)
Notez qu'il existe un +0 et un 0.
e = 0 et f = 0 désigne un nombre dénormalisé. Ce sont des dis
discuté dans la section suivante.
e = FF et f = 0 désigne l'infini (∞). Il y a les deux positifs
et les infinis négatifs.
e = FF et f = 0 désigne un résultat indéfini, appelé NaN
(Pas un numéro).
Tableau 6.1 : Valeurs spéciales de f et e
63 62 52 51 0
s e F
Figure 6.4 : Double précision IEEE
Comment 23,85 seraitil représenté ? Changez simplement le bit de signe : C1 BE CC
CD. Ne prenez pas le complément à deux !
Certaines combinaisons de e et f ont des significations particulières pour les flottants IEEE.
Le tableau 6.1 décrit ces valeurs spéciales. Un infini est produit par un
débordement ou par division par zéro. Un résultat indéfini est produit par un
opération invalide comme essayer de trouver la racine carrée d'un nombre négatif,
ajouter deux infinis, etc.
Les nombres à simple précision normalisés peuvent varier en magnitude de 1,0 ×
−126
2 (≈ 1,1755 × 10−35) à 1,11111 . . . 127 × 2 (≈ 3,4028 × 1035).
Nombres dénormalisés
Les nombres dénormalisés peuvent être utilisés pour représenter des nombres avec des
grandeurs trop petites pour être normalisées (c'estàdire inférieures à 1,0×2 −126). Par exemple, considérez
−129
le nombre 1.0012×2 (≈ 1,6530×10−39). Sous la forme normalisée donnée,
l'exposant est trop petit. Cependant, il peut être représenté sous la forme non normalisée :
0,010012 × 2 −127. Pour stocker ce nombre, l'exposant biaisé est
mis à 0 (voir Tableau 6.1) et la fraction est le signifiant complet du
nombre écrit sous la forme d'un produit avec 2−127 (c'estàdire que tous les bits sont stockés, y compris
−129
celui à gauche de la virgule). La représentation de 1.001×2
est alors:
0 000 0000 0 001 0010 0000 0000 0000 0000
Machine Translated by Google
122 CHAPITRE 6. POINT FLOTTANT
Double précision IEEE
La double précision IEEE utilise 64 bits pour représenter les nombres et est généralement
précise à environ 15 chiffres décimaux significatifs. Comme le montre la figure 6.4, le format
de base est très similaire à la simple précision. Plus de bits sont utilisés pour l'exposant biaisé
(11) et la fraction (52) que pour la simple précision.
La plage plus large pour l'exposant biaisé a deux conséquences. La première est qu'il est
calculé comme la somme de l'exposant vrai et de 3FF (1023) (et non 7F comme pour la simple
précision). Deuxièmement, une large gamme d'exposants vrais (et donc une plus grande
gamme de grandeurs) est autorisée. Les magnitudes à double précision peuvent aller d'environ
10−308 à 10308 .
C'est le plus grand champ de la fraction qui est responsable de l'augmentation de
le nombre de chiffres significatifs pour les valeurs doubles.
À titre d'exemple, considérons à nouveau 23,85. L'exposant biaisé sera 4 +
3FF = 403 en hexadécimal. Ainsi, la double représentation serait :
0 100 0000 0011 0111 1101 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
ou 40 37 D9 99 99 99 99 9A en hex. Si on reconvertit cela en décimal, on trouve
23,8500000000000014 (il y a 12 zéros !) qui est une bien meilleure approximation de 23,85.
La double précision a les mêmes valeurs spéciales que la simple précision3 .
Les nombres dénormalisés sont également très similaires. La seule différence principale est
que les nombres dénormalisés doubles utilisent 2−1023 au lieu de 2−127 .
6.2 Arithmétique en virgule flottante
L'arithmétique à virgule flottante sur un ordinateur est différente de celle des mathématiques
continues. En mathématiques, tous les nombres peuvent être considérés comme exacts.
Comme indiqué dans la section précédente, sur un ordinateur, de nombreux nombres ne
peuvent pas être représentés exactement avec un nombre fini de bits. Tous les calculs sont
effectués avec une précision limitée. Dans les exemples de cette section, les nombres avec
un significande de 8 bits seront utilisés pour plus de simplicité.
6.2.1 Ajout
Pour additionner deux nombres à virgule flottante, les exposants doivent être égaux. S'ils
ne sont pas déjà égaux, ils doivent être rendus égaux en décalant le signifiant du nombre avec
le plus petit exposant. Par exemple, considérons 10,375 + 6,34375 = 16,71875 ou en binaire :
3 1,0100110 × 2
+ 1,1001011 × 2 2
3La seule différence est que pour les valeurs infinies et indéfinies, l'exposant biaisé est
7FF et non FF.
Machine Translated by Google
6.2. ARITHMÉTIQUE À POINT FLOTTANT 123
Ces deux nombres n'ont pas le même exposant donc décalez le significande pour rendre les
exposants identiques puis ajoutez :
3 1,0100110 × 2
3
+ 0,1100110 × 2 3
10,0001100 × 2
2
Notez que le décalage de 1,1001011 × 2 arrondi dépose le dernier et après
3 3
donne 0,1100110 × 2 (ou 1,00001100 × . Le résultat de l'addition, 10.0001100×2
4
2 la réponse exacte ) est égal à 10000,1102 ou 16,75. Ce n'est pas égal à
(16,71875) ! Il ne s'agit que d'une approximation en raison des erreurs d'arrondi du processus
d'addition.
Il est important de réaliser que l'arithmétique en virgule flottante sur un ordinateur (ou une
calculatrice) est toujours une approximation. Les lois des mathématiques ne fonctionnent pas
toujours avec les nombres à virgule flottante sur un ordinateur. Les mathématiques supposent
une précision infinie qu'aucun ordinateur ne peut égaler. Par exemple, les mathématiques
enseignent que (a + b) − b = a ; cependant, cela peut ne pas être vrai exactement sur un
ordinateur !
6.2.2 Soustraction
La soustraction fonctionne de manière très similaire et pose les mêmes problèmes que l'addition.
Par exemple, considérons 16,75 − 15,9375 = 0,8125 :
1.0000110 × 2 4
3 − 1,1111111 × 2
3 4
Décalage 1,1111111 × 2 donne (arrondi) 1,0000000 × 2
4 1,0000110 × 2
4
− 1,0000000 × 2
0,0000110 × 2 4
4 0,0000110 × 2 = 0,112 = 0,75 ce qui n'est pas tout à fait correct.
6.2.3 Multiplication et division
Pour la multiplication, les significandes sont multipliées et les exposants sont additionnés.
Considérons 10,375 × 2,5 = 25,9375 :
3 1,0100110 × 2
1 × 1,0100000 × 2
10100110
+ 10100110 4
1.10011111000000
× 2
Machine Translated by Google
124 CHAPITRE 6. POINT FLOTTANT
Bien sûr, le résultat réel serait arrondi à 8 bits pour donner :
4 1,1010000 × 2 = 11010.0002 = 26
La division est plus compliquée, mais a des problèmes similaires avec l'arrondi
les erreurs.
6.2.4 Ramifications pour la programmation
Le point principal de cette section est que les calculs en virgule flottante ne sont pas exacts.
Le programmeur doit en être conscient. Une erreur courante que font les programmeurs avec
les nombres à virgule flottante est de les comparer en supposant qu'un calcul est exact. Par
exemple, considérons une fonction nommée f (x) qui effectue un calcul complexe et un
programme essaie de trouver les racines de la fonction4 . On pourrait être tenté d'utiliser
l'instruction suivante pour vérifier si x est une racine :
si ( f (x) == 0.0 )
Mais que se passetil si f (x) renvoie 1 × 10−30 ? Cela signifie très probablement que x est une
très bonne approximation d'une vraie racine ; cependant, l'égalité sera fausse.
Il se peut qu'aucune valeur à virgule flottante IEEE de x ne renvoie exactement zéro, en raison
d'erreurs d'arrondi dans f (x).
Une bien meilleure méthode serait d'utiliser:
si ( fabs ( f (x)) < EPS )
où EPS est une macro définie comme étant une très petite valeur positive (comme 1×10−10).
Ceci est vrai chaque fois que f (x) est très proche de zéro. En général, pour comparer une
valeur à virgule flottante (disons x) à une autre (y), utilisez :
si ( fabs(x − y)/fabs(y) < EPS )
6.3 Le coprocesseur numérique
6.3.1 Matériel
Les premiers processeurs Intel n'avaient pas de support matériel pour les opérations en
virgule flottante. Cela ne signifie pas qu'ils ne pouvaient pas effectuer d'opérations flottantes.
Cela signifie simplement qu'ils devaient être exécutés par des procédures composées de
nombreuses instructions à virgule non flottante. Pour ces premiers systèmes, Intel a fourni une
puce supplémentaire appelée coprocesseur mathématique. Un coprocesseur mathématique a
des instructions machine qui exécutent de nombreuses opérations en virgule flottante beaucoup
plus rapidement qu'en utilisant une procédure logicielle (sur les premiers processeurs, au moins 10 fois
4Une racine d'une fonction est une valeur x telle que f(x) = 0
Machine Translated by Google
6.3. LE COPROCESSEUR NUMERIQUE 125
plus rapide!). Le coprocesseur du 8086/8088 s'appelait le 8087. Pour le 80286, il y avait un
80287 et pour le 80386, un 80387. Le processeur 80486DX intégrait le coprocesseur
mathématique dans le 80486 luimême.5 Depuis le Pentium, toutes les générations de
processeurs 80x86 avoir un coprocesseur mathématique intégré ; cependant, il est toujours
programmé comme s'il s'agissait d'une unité distincte. Même les systèmes antérieurs sans
coprocesseur peuvent installer un logiciel qui émule un coprocesseur mathématique. Ces
packages d'émulation sont automatiquement activés lorsqu'un programme exécute une
instruction du coprocesseur et exécute une procédure logicielle qui produit le même résultat
que le coprocesseur aurait (bien que beaucoup plus lent, bien sûr).
Le coprocesseur numérique possède huit registres à virgule flottante. Chaque registre
contient 80 bits de données. Les nombres à virgule flottante sont toujours stockés sous forme
de nombres à précision étendue de 80 bits dans ces registres. Les registres sont nommés
ST0, ST1, ST2, . . . ST7. Les registres à virgule flottante sont utilisés différemment des registres
d'entiers du CPU principal. Les registres à virgule flottante sont organisés en pile. Rappelez
vous qu'une pile est une liste LastIn FirstOut (LIFO). ST0 fait toujours référence à la valeur
en haut de la pile. Tous les nouveaux numéros sont ajoutés au sommet de la pile. Les numéros
existants sont poussés vers le bas sur la pile pour faire de la place pour le nouveau numéro.
Il existe également un registre d'état dans le coprocesseur numérique. Il a plusieurs
drapeaux. Seuls les 4 drapeaux utilisés pour les comparaisons seront couverts : C0, C1, C2
et C3. L'utilisation de ceuxci est discutée plus loin.
6.3.2 Consignes
Pour faciliter la distinction entre les instructions CPU normales et copro
cessseurs, tous les mnémoniques du coprocesseur commencent par un F.
Chargement et stockage
Plusieurs instructions chargent des données en haut de la pile de registres du
coprocesseur :
Source FLD charge un nombre à virgule flottante de la mémoire sur le dessus de la pile.
La source peut être un nombre à précision simple, double ou étendue ou un
registre de coprocesseur.
La source FILD lit un entier de la mémoire, le convertit en virgule flottante et stocke le résultat en
haut de la pile. La source peut être un mot, un mot double ou un mot
quadruple. stocke un sur le dessus de la pile. stocke
FLD1 un zéro en haut de la pile.
FLDZ
Il existe également plusieurs instructions qui stockent les données de la pile en mémoire.
Certaines de ces instructions apparaissent également (c'estàdire suppriment) le numéro de
5Cependant, le 80486SX n'avait pas de coprocesseur intégré. Il y avait un
puce 80487SX séparée pour ces machines.
Machine Translated by Google
126 CHAPITRE 6. POINT FLOTTANT
la pile au fur et à mesure qu'elle la stocke.
FST destination stocke le haut de la pile (ST0) en mémoire. La destination peut être soit un
nombre simple ou double précision, soit un
registre du coprocesseur.
Destination FSTP stocke le haut de la pile en mémoire tout comme FST ; cependant,
une fois le nombre stocké, sa valeur est extraite de la pile.
La destination peut être soit un numéro à précision simple, double ou
étendue, soit un registre de coprocesseur.
FIST destination stocke la valeur du haut de la pile convertie en entier
en mémoire. La destination peut être un mot ou un double
mot. La pile ellemême est inchangée. Comment la virgule flottante
le nombre est converti en entier dépend de certains bits dans
le mot de contrôle du coprocesseur. Ceci est un spécial (nonflottant
point) registre de mots qui contrôle le fonctionnement du coprocesseur.
Par défaut, le mot de contrôle est initialisé pour qu'il arrondisse
à l'entier le plus proche lorsqu'il est converti en entier. Cependant,
le FSTCW (Store Control Word) et le FLDCW (Load Control
Word) peuvent être utilisées pour modifier ce comportement.
FISTP dest Identique à FIST sauf pour deux choses. Le haut de la pile est
sauté et la destination peut également être un mot quadruple.
Il existe deux autres instructions qui peuvent déplacer ou supprimer des données sur le
s'empiler.
FXCH STn échange les valeurs dans ST0 et STn sur la pile (où n
est le numéro de registre de 1 à 7).
FFREE STn libère un registre sur la pile en marquant le registre comme
inutilisé ou vide.
Addition et soustraction
Chacune des instructions d'addition calcule la somme de ST0 et une autre
opérande. Le résultat est toujours stocké dans un registre du coprocesseur.
FADD source ST0 += source . Le src peut être n'importe quel registre de coprocesseur
ou un nombre simple ou double précision en mémoire.
FADD destination, ST0 destination += ST0. La destination peut être n'importe quel registre de
coprocesseur.
FADDP destination ou dest += ST0 puis pile pop. Le dest peut être n'importe lequel
FADDP destination, STO registre du coprocesseur.
FIADD src ST0 += (flottant) src . Ajoute un entier à ST0. Le
src doit être un mot ou un double mot en mémoire.
Il y a deux fois plus d'instructions de soustraction que d'addition car
l'ordre des opérandes est important pour la soustraction (ie a + b = b + a,
mais a b = b a !). Pour chaque instruction, il en existe une autre qui
soustrait dans l'ordre inverse. Ces instructions inverses se terminent toutes par
Machine Translated by Google
6.3. LE COPROCESSEUR NUMERIQUE 127
1 segment .bss
2 tableaux TAILLE requise
3 somme demande 1
4
5 segments .text
6 mouvement exx, TAILLE
7 mouvement
esi, tableau
8 fldz ; ST0 = 0
9 livres :
dix fadd qword [esi] ajouter esi, ; ST0 += *(esi)
11 8 loop lp ; passer au double suivant
12
13 somme qword fstp ; stocker le résultat dans la somme
Figure 6.5 : Exemple de somme de tableau
R ou RP. La figure 6.5 montre un court extrait de code qui additionne les éléments
d'un tableau de doubles. Aux lignes 10 et 13, il faut préciser la taille de
l'opérande mémoire. Sinon, l'assembleur ne saurait pas si le
l'opérande mémoire était un flottant (dword) ou un double (qword).
Source FSUB ST0 = source . Le src peut être n'importe quel registre de coprocesseur
ou un nombre simple ou double précision en mémoire.
FSUBR src ST0 = source ST0. Le src peut être n'importe quel registre de
coprocesseur ou un nombre simple ou double précision dans
mémoire.
Destination FSUB, ST0 destination = ST0. La destination peut être n'importe quel registre de
coprocesseur.
FSUBR destination, ST0 dest = ST0 dest . La destination peut être n'importe quel registre de
coprocesseur.
FSUBP destination ou dest = ST0 puis pile pop. Le dest peut être n'importe lequel
Destination FSUBP, STO registre du coprocesseur.
Destination FSUBRP ou dest = ST0 dest puis pop pile. Le dest peut
Destination FSUBRP, STO être n'importe quel registre de coprocesseur.
FISUB src ST0 = (flottant) src . Soustrait un entier de
ST0. Le src doit être un mot ou un double mot en mémoire
ory.
FISUBR src ST0 = (flottant) src ST0. Soustrait ST0 d'un
entier. Le src doit être un mot ou un double mot dans
mémoire.
Machine Translated by Google
128 CHAPITRE 6. POINT FLOTTANT
Multiplication et division
Les instructions de multiplication sont complètement analogues à l'addition
instructions.
Source FMUL ST0 *= source . Le src peut être n'importe quel registre de coprocesseur
ou un nombre simple ou double précision en mémoire.
Destination FMUL, ST0 destination *= ST0. La destination peut être n'importe quel registre de
coprocesseur.
FMULP destination ou dest *= ST0 puis pile pop. Le dest peut être n'importe lequel
FMULP destination, STO registre du coprocesseur.
FIMULsrc ST0 *= (flottant) src . Multiplie un entier par ST0.
Le src doit être un mot ou un double mot en mémoire.
Sans surprise, les instructions de division sont analogues aux instructions de soustraction. La
division par zéro donne un infini.
FDIV src ST0 /= source . Le src peut être n'importe quel registre de coprocesseur
ou un nombre simple ou double précision en mémoire.
FDIVR source ST0 = source / ST0. Le src peut être n'importe quel registre de
coprocesseur ou un nombre simple ou double précision dans
mémoire.
FDIV destination, ST0 destination /= ST0. La destination peut être n'importe quel registre de
coprocesseur.
Destination FDIVR, ST0 destination = ST0 / destination . La destination peut être n'importe quel
registre de coprocesseur.
FDIVP dest ou dest /= ST0 puis pop pile. Le dest peut être n'importe lequel
FDIVP dest, STO registre du coprocesseur.
FDIVRP dest ou dest = ST0 / dest puis pop pile. Le dest peut
Destinataire FDIVRP, STO être n'importe quel registre de coprocesseur.
FIDIV src ST0 /= (flottant) src . Divise ST0 par un entier.
Le src doit être un mot ou un double mot en mémoire.
FIDIVR src ST0 = (flottant) src / ST0. Divise un entier par
ST0. Le src doit être un mot ou un double mot en mémoire
ory.
Comparaisons
Le coprocesseur effectue également des comparaisons de nombres à virgule flottante.
La famille d'instructions FCOM effectue cette opération.
Machine Translated by Google
6.3. LE COPROCESSEUR NUMERIQUE 129
1 ; si ( x > y )
2 ;
3 fld qmot [x] ; ST0 = x
4 fcomp qmot [y] ; comparer STO et y
5 hache fstsw ; déplacer les bits C dans FLAGS
6 sahf
7 jna else_part ; si x n'est pas audessus de y, aller à else_part
8 then_part :
9 ; code pour alors partie
dix jmp end_if
11 autre_partie :
12 ; code pour la partie else
13 end_if :
Figure 6.6 : Exemple de comparaison
FCOM src compare ST0 et src . Le src peut être un registre de coprocesseur
ou un flottant ou un double en mémoire.
FCCOMP src , la pile. Le src peut être un
compare ST0 et src puis saute
registre du coprocesseur ou un flottant ou un double en mémoire.
FCOMPP compare ST0 et ST1, puis saute la pile deux fois.
FICOM src compare ST0 et (float) src . Le src peut être un mot ou
dword entier en mémoire.
FICOMP src compare ST0 et (float)src , apparaît alors la pile. Le src
peut être un entier word ou dword en mémoire.
FTST compare ST0 et 0.
Ces instructions modifient les bits C0, C1, C2 et C3 du coprocesseur
registre d'état. Malheureusement, il n'est pas possible pour le CPU d'accéder à ces
bits directement. Les instructions de branchement conditionnel utilisent le registre FLAGS,
pas le registre d'état du coprocesseur. Cependant, il est relativement simple de transférer les bits
du mot d'état dans les bits correspondants des FLAGS.
enregistrezvous en utilisant de nouvelles instructions :
FSTSW dest Stocke le mot d'état du coprocesseur dans un mot en mémoire ou dans le registre
AX.
SAHF Stocke le registre AH dans le registre FLAGS.
LAHF Charge le registre AH avec les bits du registre FLAGS.
La figure 6.6 montre un court exemple d'extrait de code. Transfert lignes 5 et 6
les bits C0, C1, C2 et C3 du mot d'état du coprocesseur dans les FLAGS
enregistrer. Les bits sont transférés de façon à ce qu'ils soient analogues au résultat
d'une comparaison de deux entiers non signés. C'est pourquoi la ligne 7 utilise un JNA
instruction.
Machine Translated by Google
130 CHAPITRE 6. POINT FLOTTANT
Le Pentium Pro (et les processeurs ultérieurs (Pentium II et III)) prennent en charge deux
de nouveaux opérateurs de comparaison qui modifient directement le registre FLAGS du CPU.
FCOMI src compare ST0 et src . Le src doit être un registre de coprocesseur.
FCOMIP src compare ST0 et src , apparaît alors la pile. Le src doit être un
registre du coprocesseur.
La figure 6.7 montre un exemple de sousprogramme qui trouve le maximum de deux doubles
en utilisant l'instruction FCOMIP. Ne confondez pas ces instructions avec
les fonctions de comparaison d'entiers (FICOM et FICOMP).
Consignes diverses
Cette section couvre quelques autres instructions diverses que le co
processeur fournit.
FCHS ST0 = ST0 Change le signe de ST0
FABS ST0 = |ST0| Prend la valeur absolue de ST0
FSQRT ST0 = √ STO Prend la racine carrée de ST0
FSCALE ST0 = ST0×2 ST1
multiplie ST0 par une puissance de 2 rapidement. ST1
n'est pas supprimé de la pile du coprocesseur. La figure 6.8 montre
un exemple d'utilisation de cette instruction.
6.3.3 Exemples
6.3.4 Formule quadratique
Le premier exemple montre comment la formule quadratique peut être encodée en
assemblée. Rappelons que la formule quadratique calcule les solutions de la
équation quadratique:
ax2 + bx + c = 0
La formule ellemême donne deux solutions pour x : x1 et x2.
−b ± √ b 2 − 4ac
x1, x2 = 2a
2
L'expression à l'intérieur de la racine carrée (b − 4ac) est appelé le discriminant.
Sa valeur est utile pour déterminer laquelle des trois possibilités suivantes
sont vraies pour les solutions.
1. Il n'y a qu'une seule vraie solution dégénérée. b 2 4ac = 0
2. Il existe deux vraies solutions. b 2 − 4ac > 0
2 − 4ac < 0
3. Il existe deux solutions complexes. b
Voici un petit programme C qui utilise le sousprogramme d'assemblage :
Machine Translated by Google
6.3. LE COPROCESSEUR NUMERIQUE 131
quadt.c
1 #include <stdio.h>
2
3 int quadratique ( double, double, double, double , double );
4
5 entier principal()
6 {
7 double a,b,c , racine1 , racine2;
8
9 printf(”Entrez a, b, c : ”);
dix scanf(”%lf %lf %lf”, &a, &b, &c);
11 si ( quadratique ( a, b, c, &root1, &root2 ) )
12 printf ("racines : %.10g %.10g\n", racine1, racine2 );
13 autre
14 printf ("Aucune vraie racine\n");
15 renvoie 0 ;
16 }
quadt.c
Voici la routine d'assemblage :
quad.asm
1 ; fonction quadratique
2 ; trouve des solutions à l'équation quadratique :
3 ; a*x^2 + b*x + c = 0
4 ; Prototype C :
5 ; int quadratique( double a, double b, double c,
6 ; double * racine1, double * racine2 )
7 ; Paramètres:
8 a, b, c coefficients des puissances de l'équation quadratique (voir cidessus)
9 ; ; root1 pointeur vers double pour stocker la première racine dans
dix ; root2 pointeur vers double pour stocker la deuxième racine dans
11 ; Valeur de retour :
12 ; renvoie 1 si des racines réelles sont trouvées, sinon 0
13
14 %définir a 15 qword [ebp+8]
%définir b 16 qword [ebp+16]
%définir c 17 qword [ebp+24]
%définir racine1 18 dword [ebp+32]
%définir racine2 19 dword [ebp+36]
%définir disque qword [ebp8]
Machine Translated by Google
132 CHAPITRE 6. POINT FLOTTANT
20 % définissent one_over_2a qword [ebp16]
21
22 segments .données
23 MoinsQuatre dw 4
24
25 segments .text
26 global _quadratic
27 _quadratique :
28 pousser ebp
29 mouvement
ebp, esp
30 sous esp, 16 ; allouer 2 doubles (disque & one_over_2a)
31 pousser ebx ; doit enregistrer l'ebx d'origine
32
33 remplir mot [MinusFour] ; pile 4
34 fld un ; pile : a, 4
35 fld c ; pile : c, a, 4
36 fmulp st1 fmulp ; pile : a*c, 4
37 st1 fld b ; pile : 4*a*c
38
39 fld b ; pile : b, b, 4*a*c
40 fmulp st1 faddp ; pile : b*b, 4*a*c
41 st1 ftst ; pile : b*b 4*a*c
42 ; tester avec 0
43 hache fstsw
44 sahf
45 jb no_real_solutions ; si disque < 0, pas de vraies solutions
46 fsqrt ; pile : sqrt(b*b 4*a*c)
47 fstp disque ; stocker et pop stack
48 fld1 ; pile : 1,0
49 fld un ; pile : a, 1,0
50 fscale ; pile : a * 2^(1.0) = 2*a, 1
51 fdivp st1 fst ; pile : 1/(2*a)
52 one_over_2a ; pile : 1/(2*a)
53 fld b ; pile : b, 1/(2*a)
54 fld disque ; pile : disque, b, 1/(2*a)
55 fsubrp st1 fmulp ; pile : disque b, 1/(2*a)
56 st1 ebx, root1 ; pile : (b + disque)/(2*a)
57 mouvement
6.3. LE COPROCESSEUR NUMERIQUE 133
62 fsubrp st1 fmul ; pile : disque b
63 one_over_2a ebx, root2 ; pile : (b disque)/(2*a)
64 mouvement
69 pas de_réelles_solutions :
70 mouvement eax, 0 ; la valeur de retour est 0
71
72 quitter :
73 populaire
ebx
74 mouvement
esp, ebp
75 populaire ebp
76 ret
quad.asm
6.3.5 Lecture d'un tableau à partir d'un fichier
Dans cet exemple, une routine d'assemblage lit les doubles d'un fichier. Voici
un petit programme de test C :
readt.c
1 /
2 Ce programme teste la procédure d'assemblage en lecture double () 32 bits.
3 Il lit les doubles de stdin . (Utilisez la redirection pour lire à partir du fichier.)
4 /
5 #include <stdio.h>
6 extern int read doubles ( FILE , double , int );
7 #définir MAX 100
8
9 entier principal()
10 {
11 int je ,n;
12 double a[MAX] ;
13
14 n = lire double( stdin , a, MAX);
15
16 pour( je=0; je < n; je++ )
17 printf ("%3d %g\n", je, un[je ]);
18 renvoie 0 ;
19 }
Machine Translated by Google
134 CHAPITRE 6. POINT FLOTTANT
readt.c
Voici la routine de montage
lire.asm
1 segment .données
2 formats de base de données "%lf", 0 ; format pour fscanf()
3
4 segments .text
5 global _read_doubles
6 externe _fscanf
7
8 %définir SIZEOF_DOUBLE 8
9 % définir FP dword [ebp + 8]
10 %define ARRAYP dword [ebp + 12]
11 %define ARRAY_SIZE dword [ebp + 16]
12 %définir TEMP_DOUBLE [ebp 8]
13
14 ;
15 ; fonction _read_doubles
16 ; Prototype C :
17 ; int read_doubles( FILE * fp, double * arrayp, int array_size );
18 ; Cette fonction lit les doubles d'un fichier texte dans un tableau, jusqu'à ce que
19 ; EOF ou tableau est plein.
20 ; Paramètres:
21 ; fp Pointeur FILE à lire (doit être ouvert pour l'entrée)
22 ; tableaup ; pointeur vers un double tableau dans lequel lire
23 array_size nombre d'éléments dans le tableau
24 ; Valeur de retour :
25 ; nombre de doubles stockés dans le tableau (dans EAX)
26
27 _read_doubles :
28 pousser ebp
29 mouvement
ebp, esp
30 sous en particulier, SIZEOF_DOUBLE ; définir un double sur la pile
31
36 boucle_while :
37 cmp edx, ARRAY_SIZE ; estce que edx < ARRAY_SIZE ?
38 jnl arrêt court ; sinon, quittez la boucle
Machine Translated by Google
6.3. LE COPROCESSEUR NUMERIQUE 135
39 ;
40 ; appelez fscanf() pour lire un double dans TEMP_DOUBLE
41 ; fscanf() peut changer edx alors enregistrezle
42 ;
43 pousser edx lea ; enregistrer edx
44 eax, TEMP_DOUBLE
45 eax ; appuyez sur &TEMP_DOUBLE
46 pousser pousser format dword ; pousser et formater
47 pousser FP ; pousser le pointeur de fichier
48 appeler _fscanf
49 ajouter esp, 12
50 populaire
edx ; restaurer edx
51 cmp eax, 1 ; fscanf atil renvoyé 1 ?
52 jne court abandon ; sinon, quittez la boucle
53
54 ;
55 ; copier TEMP_DOUBLE dans ARRAYP[edx]
56 ; (Les 8 octets du double sont copiés par deux copies de 4 octets)
57 ;
58 mouvement
eax, [ebp 8]
59 mouvement [esi + 8*edx], eax eax, [ebp ; première copie les 4 octets les plus bas
60 mouvement
4]
61 mouvement
[esi + 8*edx + 4], eax ; prochaine copie les 4 octets les plus élevés
62
63 inc. edx
64 jmp boucle_while
65
66 quitter :
67 populaire
esi ; restaurer esi
68
71 mouvement
esp, ebp
72 populaire ebp
73 ret
lire.asm
6.3.6 Trouver des nombres premiers
Ce dernier exemple examine à nouveau la recherche de nombres premiers. Cette implémentation
est plus efficace que la précédente. Il stocke les nombres premiers
a trouvé dans un tableau et ne divise que par les nombres premiers précédents qu'il a trouvés
au lieu de chaque nombre impair pour trouver de nouveaux nombres premiers.
Machine Translated by Google
136 CHAPITRE 6. POINT FLOTTANT
Une autre différence est qu'il calcule la racine carrée de la supposition pour
le prochain premier pour déterminer à quel moment il peut arrêter de chercher des facteurs.
Il modifie le mot de contrôle du coprocesseur de sorte que lorsqu'il stocke la racine carrée
en tant qu'entier, il tronque au lieu d'arrondir. Ceci est contrôlé par des bits
10 et 11 du mot de contrôle. Ces bits sont appelés RC (Rounding
Contrôle). S'ils valent tous les deux 0 (valeur par défaut), le coprocesseur arrondit quand
conversion en entier. S'ils valent tous les deux 1, le coprocesseur tronque l'entier
conversions. Notez que la routine prend soin de sauvegarder le contrôle d'origine
mot et restaurezle avant qu'il ne revienne.
Voici le programme du pilote C :
fprime.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 /
4 fonction trouve les nombres premiers
5 trouve le nombre de nombres premiers indiqué
6 Paramètres :
7 a − tableau pour contenir les nombres premiers
8 n − combien de nombres premiers trouver
9 /
10 extern void find nombres premiers ( int a, unsigned n );
11
12 entier principal()
13 {
14 état entier ;
15 i non signé ;
16 maximum non signé ;
17 int a ;
18
19 printf(”Combien de nombres premiers souhaitezvous trouver ? ”);
20 scanf(”%u”, &max);
21
22 a = calloc ( taillede(int ), max);
23
24 si ( une ) {
25
26 trouver les nombres premiers (a,max);
27
28 / affiche les 20 derniers nombres premiers trouvés /
29 pour( je= ( max > 20 ) ? max − 20 : 0; je < max; je++ )
30 printf(”%3d %d\n”, i+1, a[i]);
Machine Translated by Google
6.3. LE COPROCESSEUR NUMERIQUE 137
31
32 libre (un );
33 état = 0 ;
34 }
35 sinon {
36 fprintf ( stderr , ”Impossible de créer un tableau de %u entiers\n”, max);
37 statut = 1;
38 }
39
40 état de retour ;
41 }
fprime.c
Voici la routine d'assemblage :
premier2.asm
1 segment .texte
2 global _find_primes
3 ;
4 ; fonction find_primes
5 ; trouve le nombre de nombres premiers indiqué
6 ; Paramètres:
7 ; tableau tableau pour contenir les nombres premiers
8 ; n_find combien de nombres premiers trouver
9 ; Prototype C :
10 ;extern void find_primes( int * array, unsigned n_find )
11 ;
12 % définir le tableau ebp + 8
13 %define n_find ebp + 12
14 % définir n ebp 4 15 % définir isqrt ebp 8 ; nombre de nombres premiers trouvés jusqu'à présent
16 % définir orig_cntl_wd ebp 10 17 % définir ; étage du sqrt de deviner
new_cntl_wd ebp 12 ; mot de contrôle d'origine
; nouveau mot de contrôle
18
19 _find_primes :
20 entrez 12,0 ; faire de la place aux variables locales
21
22 pousser ebx ; enregistrer les variables de registre possibles
23 pousser esi
24
25 mot fstcw [orig_cntl_wd] hache, ; obtenir le mot de contrôle actuel
26 mouvement
[orig_cntl_wd]
Machine Translated by Google
138 CHAPITRE 6. POINT FLOTTANT
27 ou hache, 0C00h ; définir les bits d'arrondi sur 11 (tronquer)
28 mouvement [new_cntl_wd], hache
29 mot fldcw [new_cntl_wd]
30
31 mouvement
esi, [tableau] ; esi pointe vers le tableau
32 mouvement dword [esi], 2 dword ; tableau[0] = 2
33 mouvement [esi + 4], 3 ebx, 5 dword [n], ; tableau[1] = 3
34 mouvement 2 ; ebx = deviner = 5
35 mouvement ; n = 2
36 ;
37 ; Cette boucle externe trouve un nouveau nombre premier à chaque itération, qu'il ajoute au
38 ; fin du tableau. Contrairement au programme de recherche principal précédent, cette fonction
39 ; ne détermine pas la primitivité en divisant par tous les nombres impairs. C'est seulement
40 ; divise par les nombres premiers qu'il a déjà trouvés. (C'est pourquoi ils
41 ; sont stockés dans le tableau.)
42 ;
43 while_limit :
44 mouvement eax, [n]
45 cmp eax, [n_find] court ; tandis que ( n < n_trouver )
46 jnb quit_limit
47
6.3. LE COPROCESSEUR NUMERIQUE 139
69 jmp short while_factor
70
71 ;
72 ; trouvé un nouveau premier !
73 ;
74 quit_factor_prime :
75 mouvement eax, [n]
76 mouvement dword [esi + 4*eax], ebx ; ajouter une estimation à la fin du tableau
77 inc. eax
80 quit_factor_not_prime :
81 ajouter ebx, 2 ; essayez le nombre impair suivant
82 jmp short while_limit
83
84 quit_limit :
85
86 mot fldcw [orig_cntl_wd] ; restaurer le mot de contrôle
87 populaire
esi ; restaurer les variables de registre
88 populaire
ebx
89
90 partir
91 ret
premier2.asm
Machine Translated by Google
140 CHAPITRE 6. POINT FLOTTANT
1 _dmax global
2
3 segments .text
4 ; fonction _dmax
5 ; renvoie le plus grand de ses deux arguments doubles
6 ; Prototype C
7 ; double dmax( double d1, double d2 )
8 ; Paramètres:
9 ; d1 premier doublé
dix ; d2 deuxième doublé
11 ; Valeur de retour :
12 ; plus grand de d1 et d2 (dans ST0)
13 %définir d1 ebp+8
14 %définir d2 ebp+16
15 _dmax :
16 entrez 0, 0
17
18 fld qmot [d2]
19 fld qword [d1] ; ST0 = d1, ST1 = d2
20 fcomip st1 jna ; ST0 = d2
21 court d2_bigger
22 fcomp st0 qword ; pop d2 de la pile
23 fld [d1] sortie ; ST0 = d1
24 jmp courte
25 d2_bigger : 26 ; si d2 est max, rien à faire
sortie :
27 partir
28 ret
Figure 6.7 : Exemple FCOMIP
Machine Translated by Google
6.3. LE COPROCESSEUR NUMERIQUE 141
1 segment .données
2x _ dq 2,75 dw ; converti en format double
3 cinq 5
4
5 segments .text
6 champ dword [cinq] qword ; ST0 = 5
7 fld [x] ; ST0 = 2,75, ST1 = 5
8 fscale ; ST0 = 2,75 * 32, ST1 = 5
Figure 6.8 : Exemple FSCALE
Machine Translated by Google
142 CHAPITRE 6. POINT FLOTTANT
Machine Translated by Google
Chapitre 7
Structures et C++
7.1 Ouvrages
7.1.1 Présentation
Les structures sont utilisées en C pour regrouper des données liées dans un composite
variable. Cette technique présente plusieurs avantages :
1. Il clarifie le code en montrant que les données définies dans la structure sont intimement
liées.
2. Cela simplifie le passage des données aux fonctions. Au lieu de transmettre plusieurs
variables séparément, elles peuvent être transmises comme une seule unité.
3. Il augmente la localité1 du code.
Du point de vue de l'assemblage, une structure peut être considérée comme un tableau
avec des éléments de taille variable. Les éléments des tableaux réels ont toujours la même taille
et le même type. Cette propriété est ce qui permet de calculer l'adresse de n'importe quel
élément en connaissant l'adresse de départ du tableau, la taille des éléments et l'index de
l'élément souhaité.
Les éléments d'une structure ne doivent pas nécessairement avoir la même taille (et ne le
sont généralement pas). Pour cette raison, chaque élément d'une structure doit être explicitement
spécifié et reçoit une balise (ou un nom) au lieu d'un index numérique.
En assemblage, l'élément d'une structure sera accédé de la même manière qu'un élément
d'un tableau. Pour accéder à un élément, il faut connaître l'adresse de départ de la structure et
le décalage relatif de cet élément depuis le début de la structure. Cependant, contrairement à un
tableau où ce décalage peut être calculé par l'indice de l'élément, l'élément d'une structure se
voit attribuer un décalage par le compilateur.
1Consultez la section sur la gestion de la mémoire virtuelle de tout manuel de système d'exploitation pour
discussion sur ce terme.
143
Machine Translated by Google
144 CHAPITRE 7. STRUCTURES ET C++
Élément de décalage
0 X
2
y
6
Figure 7.1 : Structure S
Élément de décalage
X
0 2 inutilisé 4
y
8
Figure 7.2 : Structure S
Par exemple, considérons la structure suivante :
struct S
{ entier court / Entier de 2 octets / /
x ; int y ; Entier de 4 octets / /
double z ; } ; Flottant de 8 octets /
La figure 7.1 montre à quoi pourrait ressembler une variable de type S dans la mémoire
de l'ordinateur. La norme ANSI C stipule que les éléments d'une structure sont disposés
dans la mémoire dans le même ordre qu'ils sont définis dans la définition de structure. Il
indique également que le premier élément est au tout début de la structure (c'estàdire
décalage zéro). Il définit également une autre macro utile dans le fichier d'entête stddef.h
nommé offsetof(). Cette macro calcule et renvoie le décalage de n'importe quel élément
d'une structure. La macro prend deux paramètres, le premier est le nom du type de la
structure, le second est le nom de l'élément dont il faut trouver le décalage. Ainsi, le résultat
de offsetof(S, y) serait 2 d'après la figure 7.1.
Machine Translated by Google
7.1. STRUCTURES 145
struct S { /
Entier dentier
e 4 sur 2 octets / int court x ; int /
octets / y ; / 8−byte float / double z ; }
attribut ((compressé));
Figure 7.3 : Structure compressée utilisant gcc
7.1.2 Alignement de la mémoire
Si l'on utilise la macro offsetof pour trouver l'offset de y en utilisant le compilateur gcc, on
trouvera qu'il renvoie 4, pas 2 ! Pourquoi? Parce que gcc (et rappelezvous qu'une adresse est sur de nombreux autres compilateurs)
alignent les variables sur les limites de mots doubles par défaut. une limite de mot double si elle est divisible par 4 En mode protégé
mémoire plus rapidement si les données commencent à une limite de mot double. La figure 7.2 32 bits, la CPU lit la
montre à quoi ressemble réellement la structure S avec gcc. Le compilateur insère deux octets
inutilisés dans la structure pour aligner y (et z) sur une limite de mot double. Cela montre pourquoi
c'est une bonne idée d'utiliser offsetof pour calculer les décalages au lieu de les calculer soimême
lors de l'utilisation de structures définies en C.
Bien entendu, si la structure n'est utilisée qu'en assemblage, le programmeur peut déterminer
luimême les décalages. Cependant, si l'on interface C et assembleur, il est très important que le
code assembleur et le code C s'accordent sur les décalages des éléments de la structure ! Une
complication est que différents compilateurs C peuvent donner des décalages différents aux
éléments. Par exemple, comme nous l'avons vu, le compilateur gcc crée une structure S qui
ressemble à la figure 7.2 ; cependant, le compilateur de Borland créerait une structure ressemblant
à la figure 7.1. Les compilateurs C fournissent des moyens de spécifier l'alignement utilisé pour les
données. Cependant, la norme ANSI C ne précise pas comment cela sera fait et, par conséquent,
différents compilateurs le font différemment.
Le compilateur gcc a une méthode flexible et compliquée pour spécifier l'alignement. Le
compilateur permet de spécifier l'alignement de n'importe quel type en utilisant une syntaxe
spéciale. Par exemple, la ligne suivante :
typedef short int attribut int non aligné ((aligned (1)));
définit un nouveau type nommé unaligned int qui est aligné sur les limites d'octets.
(Oui, toutes les parenthèses après l'attribut sont obligatoires !) Le 1 dans le paramètre aligné peut
être remplacé par d'autres puissances de deux pour spécifier d'autres alignements. (2 pour
l'alignement de mots, 4 pour l'alignement de mots doubles, etc.)
Si l'élément y de la structure était changé pour être un type int non aligné, gcc mettrait y à l'offset
2. Cependant, z serait toujours à l'offset 8 puisque les doubles sont également alignés sur les
doubles mots par défaut. La définition du type de z devrait également être modifiée pour qu'il soit
mis à l'offset 6.
Machine Translated by Google
146 CHAPITRE 7. STRUCTURES ET C++
struct S
{ entier court x ; / Entier de 2 octets / /
int y ; double Entier de 4 octets / /
z ; } ; Flottant de 8 octets /
#pragma pack(pop) / restaurer l'alignement d'origine /
Figure 7.4 : Structure compressée utilisant Microsoft ou Borland
Le compilateur gcc permet également de compacter une structure. Cela indique au compilateur
d'utiliser le minimum d'espace possible pour la structure. La figure 7.3 montre comment S pourrait
être réécrit de cette façon. Cette forme de S utiliserait le minimum d'octets possible, 14 octets.
Les compilateurs de Microsoft et de Borland prennent tous deux en charge la même méthode de
spécifiant l'alignement à l'aide d'une directive #pragma.
#pragmapack (1)
La directive cidessus indique au compilateur de regrouper les éléments des structures sur les limites
d'octets (c'estàdire sans remplissage supplémentaire). Le un peut être remplacé par deux, quatre,
huit ou seize pour spécifier l'alignement sur les limites de mot, double mot, quadruple mot et
paragraphe, respectivement. La directive reste en vigueur jusqu'à ce qu'elle soit remplacée par une
autre directive. Cela peut causer des problèmes car ces directives sont souvent utilisées dans les
fichiers d'entête. Si le fichier d'entête est inclus avant d'autres fichiers d'entête avec des structures,
ces structures peuvent être disposées différemment de ce qu'elles seraient par défaut. Cela peut
conduire à une erreur très difficile à trouver. Différents modules d'un programme peuvent disposer
les éléments des structures à différents endroits !
Il existe un moyen d'éviter ce problème. Microsoft et Borland prennent en charge un moyen de
sauvegarder l'état d'alignement actuel et de le restaurer ultérieurement. La figure 7.4 montre comment
cela serait fait.
7.1.3 Champs de bits
Les champs de bits permettent de spécifier les membres d'une structure qui n'utilisent qu'un
nombre spécifié de bits. La taille des bits ne doit pas nécessairement être un multiple de huit. Un
membre de champ de bits est défini comme un membre int ou int non signé auquel sont ajoutés deux
points et une taille de bit. La figure 7.5 montre un exemple. Cela définit une variable 32 bits qui est
décomposée dans les parties suivantes :
Machine Translated by Google
7.1. STRUCTURES 147
structure S {
f1 non signé : 3 ; / champ 3−bits /
f2 non signé : 10 ; / Champ de 10 bits /
f3 non signé : 11 ; / Champ de 11 bits /
fa4 non signé : 8 ; / Champ de 8 bits /
} ;
Figure 7.5 : Exemple de champ de bits
Octet \ Bit 7 6 5 4 3 2 1 0
0 Code opération (08h)
1 Unité logique # msb de LBA
2 milieu de l'adresse de bloc logique
3 lsb de l'adresse de bloc logique
4 Longueur de transfert
5 Contrôle
Figure 7.6 : Format de commande de lecture SCSI
Le premier champ de bits est affecté aux bits les moins significatifs de son double mot.2
Cependant, le format n'est pas si simple si l'on regarde comment les bits sont
effectivement stocké en mémoire. La difficulté survient lorsque les champs de bits s'étendent sur un octet
limites. Parce que les octets sur un petit processeur endian seront inversés
en mémoire. Par exemple, les champs de bits de la structure S ressembleront à ceci en mémoire :
L'étiquette f2l fait référence aux cinq derniers bits (c'estàdire aux cinq bits les moins significatifs)
du champ de bits f2. L'étiquette f2m fait référence aux cinq éléments les plus significatifs de
f2. Les doubles lignes verticales indiquent les limites des octets. Si l'on renverse tout
les octets, les morceaux des champs f2 et f3 seront réunis dans le bon
lieu.
La disposition de la mémoire physique n'est généralement pas importante à moins que les données ne soient
être transféré dans ou hors du programme (ce qui est en fait assez courant
avec des champs de bits). Il est courant que les interfaces des périphériques matériels utilisent des
nombre de bits que les champs de bits pourraient être utiles pour représenter.
2En fait, la norme ANSI/ISO C donne au compilateur une certaine flexibilité quant à la manière
les bits sont disposés. Cependant, les compilateurs C courants (gcc, Microsoft et Borland)
disposer les champs comme ceci.
Machine Translated by Google
148 CHAPITRE 7. STRUCTURES ET C++
1 #define MS OU BORLAND (defined( BORLANDC ) \
2 || défini (MSC VER))
3
4 #si MS OU BORLAND
5 # pragma pack (pousser)
6 # pragma pack(1)
7 #endif
8
9 struct SCSI lire cmd {
dix code opération non signé : 8;
11 lba msb non signé : 5 ;
12 unité logique non signée : 3;
13 non signé lba mi : 8; lba / morceaux du milieu /
14 lsb non signé : 8 ;
15 longueur de transfert non signé : 8 ;
16 contrôle non signé : 8;
17 }
18 #si défini( GNUC )
19 attribut ((compressé))
20 #endif
21 ;
22
23 #si MS OU BORLAND
24 # pragma pack (pop)
25 #endif
Figure 7.7 : Structure du format de commande de lecture SCSI
Un exemple est SCSI3 . Une commande de lecture directe pour un périphérique SCSI est
spécifiée en envoyant un message de six octets au périphérique dans le format spécifié dans
Illustration 7.6. La difficulté de représenter cela en utilisant des champs de bits est le bloc logique
adresse qui s'étend sur 3 octets différents de la commande. À partir de la figure 7.6,
on voit que les données sont stockées au format big endian. La figure 7.7 montre
une définition qui tente de fonctionner avec tous les compilateurs. Les deux premières lignes
définir une macro qui est vraie si le code est compilé avec Microsoft ou
Compilateurs Borland. Les parties potentiellement déroutantes sont les lignes 11 à 14. Première
on peut se demander pourquoi les champs lba mid et lba lsb sont définis séparément
et non comme un seul champ 16 bits ? La raison en est que les données sont en big endian
commande. Un champ de 16 bits serait stocké dans l'ordre little endian par le compilateur.
Ensuite, les champs lba msb et unité logique semblent être inversés ; cependant,
3Small Computer Systems Interface, une norme industrielle pour les disques durs, etc.
Machine Translated by Google
7.1. STRUCTURES 149
8 bits 8 bits 8 bits contrôle
8 bits
longueur de 3 bits 5 bits 8 bits
transfert lba lsb lba mid unité logique lba msb opcode
Figure 7.8 : Mappage des champs cmd de lecture SCSI
1 struct SCSI lire cmd {
2 opcode char non signé ;
3 caractère non signé lba msb : 5 ;
4 unité logique char non signé : 3;
5 char lba moyen non signé ; / morceaux du milieu /
6 caractère non signé lba lsb ;
7 longueur de transfert de caractères non signés ;
8 contrôle des caractères non signés ;
9 }
10 #si défini( GNUC )
11 attribut ((compressé))
12 #endif
13 ;
Figure 7.9 : Autre structure de format de commande de lecture SCSI
ce n'est pas le cas. Il faut les mettre dans cet ordre. La figure 7.8 montre
comment les champs sont mappés en tant qu'entité 48 bits. (Les limites d'octets sont à nouveau
indiqué par les doubles lignes.) Lorsque cela est stocké en mémoire dans Little Endian
ordre, les bits sont disposés dans le format souhaité (Figure 7.6).
Pour compliquer davantage les choses, la définition de la cmd de lecture SCSI ne
ne fonctionne pas tout à fait correctement pour Microsoft C. Si l' expression sizeof(SCSI read
cmd) est évaluée, Microsoft C renverra 8, et non 6 ! C'est parce que le
Le compilateur Microsoft utilise le type de champ de bits pour déterminer comment mapper
les morceaux. Comme tous les champs de bits sont définis comme des types non signés , le compilateur
remplit deux octets à la fin de la structure pour en faire un nombre entier de
mots doubles. Cela peut être résolu en rendant tous les champs non signés courts
plutôt. Maintenant, le compilateur Microsoft n'a pas besoin d'ajouter d'octets de remplissage
puisque six octets est un nombre entier de mots de deux octets.4 Les autres compilateurs
fonctionnent également correctement avec ce changement. La figure 7.9 montre encore une autre
définition qui fonctionne pour les trois compilateurs. Il évite tout sauf deux du bit
champs en utilisant des caractères non signés.
Le lecteur ne doit pas se décourager s'il trouve la discussion précédente
déroutant. C'est confus! L'auteur trouve souvent moins déroutant d'éviter
champs de bits et utiliser des opérations sur les bits pour examiner et modifier les bits
4Le mélange de différents types de champs de bits conduit à un comportement très déroutant ! Le lecteur est
invité à expérimenter.
Machine Translated by Google
150 CHAPITRE 7. STRUCTURES ET C++
manuellement.
7.1.4 Utilisation de structures en assemblage
Comme indiqué cidessus, l'accès à une structure en assemblage ressemble beaucoup à
accéder à un tableau. Pour un exemple simple, considérons comment on écrirait
une routine d'assemblage qui mettrait à zéro l'élément y d'une structure S.
En supposant que le prototype de la routine serait :
nul zéro y ( S sp );
la routine d'assemblage serait:
1 % définissent décalage_y 4
2 _zéro_y :
3 entrez 0,0
4 mouvement
eax, [ebp + 8] dword ; obtenir s_p (pointeur de structure) de la pile
5 mouvement
[eax + y_offset], 0
6 partir
7 ret
C permet de passer une structure par valeur à une fonction ; cependant, cela
est presque toujours une mauvaise idée. Lorsqu'elles sont transmises par valeur, toutes les données du
La structure doit être copiée dans la pile puis récupérée par la routine. Il
est beaucoup plus efficace de passer un pointeur vers une structure à la place.
Le C permet également d'utiliser un type de structure comme valeur de retour d'une fonction.
Evidemment une structure ne peut pas être retournée dans le registre EAX. Différent
les compilateurs gèrent cette situation différemment. Une solution courante que les compilateurs
utilisent est de réécrire en interne la fonction comme une fonction qui prend une structure
pointeur comme paramètre. Le pointeur est utilisé pour mettre la valeur de retour dans un
structure définie en dehors de la routine appelée.
La plupart des assembleurs (y compris NASM) ont un support intégré pour définir
structures dans votre code assembleur. Consultez votre documentation pour plus de détails.
7.2 Assembleur et C++
Le langage de programmation C++ est une extension du langage C.
De nombreuses règles de base de l'interface C et du langage d'assemblage s'appliquent également
à C++. Cependant, certaines règles doivent être modifiées. Aussi, certains des
les extensions de C++ sont plus faciles à comprendre avec une connaissance de l'assemblage
langue. Cette section suppose une connaissance de base de C++.
Machine Translated by Google
7.2. ASSEMBLAGE ET C++ 151
1 #include <stdio.h>
2
3 vide f ( int x ) 4 {
5 printf ("%d\n", x);
6 }
7
8 vide f ( double x ) 9 {
dix printf ("%g\n", x);
11 }
Figure 7.10 : Deux fonctions f()
7.2.1 Surcharge et manipulation de noms
C++ permet de définir différentes fonctions (et fonctions membres de classe)
portant le même nom. Lorsque plusieurs fonctions partagent le même nom, les
fonctions sont dites surchargées. Si deux fonctions sont définies avec le même nom
en C, l'éditeur de liens produira une erreur car il trouvera deux définitions pour le
même symbole dans les fichiers objets qu'il lie. Par exemple, considérons le code de
la Figure 7.10. Le code assembleur équivalent définirait deux étiquettes nommées f
ce qui sera évidemment une erreur.
C++ utilise le même processus de liaison que C, mais évite cette erreur en procédant
à une modification du nom ou en modifiant le symbole utilisé pour étiqueter la fonction.
D'une certaine manière, C utilise également déjà la manipulation de noms. Il ajoute
un trait de soulignement au nom de la fonction C lors de la création de l'étiquette de
la fonction. Cependant, C modifiera le nom des deux fonctions de la figure 7.10 de la
même manière et produira une erreur. C++ utilise un processus de manipulation plus
sophistiqué qui produit deux étiquettes différentes pour les fonctions. Par exemple,
la première fonction de la figure 7.10 se verrait attribuer par DJGPP l'étiquette f Fi et
la seconde fonction, f Fd. Cela évite toute erreur de l'éditeur de liens.
Malheureusement, il n'y a pas de norme sur la façon de gérer les noms en C++ et
différents compilateurs modifient les noms différemment. Par exemple, Borland C++
utiliserait les étiquettes @f$qi et @f$qd pour les deux fonctions de la figure 7.10.
Cependant, les règles ne sont pas totalement arbitraires. Le nom mutilé encode la
signature de la fonction. La signature d'une fonction est définie par l'ordre et le type
de ses paramètres. Remarquez que la fonction qui prend un seul argument int a un i
à la fin de son nom mutilé (pour DJGPP et Borland) et que celle qui prend un argument
double a un ad à la fin de son nom mutilé. S'il y avait une fonction nommée f avec le
prototype :
Machine Translated by Google
152 CHAPITRE 7. STRUCTURES ET C++
DJGPP transformerait son nom en f Fiid et Borland en @f$qiid.
Le type de retour de la fonction ne fait pas partie de la signature d'une fonction et n'est
pas encodé dans son nom mutilé. Ce fait explique une règle de surcharge en C++. Seules les
fonctions dont les signatures sont uniques peuvent être surchargées. Comme on peut le voir,
si deux fonctions avec le même nom et la même signature sont définies en C++, elles produiront
le même nom mutilé et créeront une erreur de l'éditeur de liens. Par défaut, toutes les fonctions
C++ sont mutilées, même celles qui ne sont pas surchargées. Lorsqu'il compile un fichier, le
compilateur n'a aucun moyen de savoir si une fonction particulière est surchargée ou non, il
tronque donc tous les noms. En fait, il modifie également les noms des variables globales en
encodant le type de la variable de la même manière que les signatures de fonction. Ainsi, si
l'on définit une variable globale dans un fichier comme un certain type et que l'on essaie ensuite
de l'utiliser dans un autre fichier comme le mauvais type, une erreur de l'éditeur de liens sera
produite. Cette caractéristique de C++ est connue sous le nom de liaison typesafe. Cela expose
également un autre type d'erreur, les prototypes incohérents. Cela se produit lorsque la
définition d'une fonction dans un module ne correspond pas au prototype utilisé par un autre
module. En C, cela peut être un problème très difficile à déboguer. C n'attrape pas cette erreur.
Le programme compilera et établira un lien, mais aura un comportement indéfini car le code
appelant poussera différents types sur la pile que ce à quoi la fonction s'attend. En C++, cela
produira une erreur de l'éditeur de liens.
Lorsque le compilateur C++ analyse un appel de fonction, il recherche une fonction
correspondante en examinant les types des arguments passés à la fonction5 .
S'il trouve une correspondance, il crée alors un CALL vers la fonction correcte en utilisant les
règles de modification de nom du compilateur.
Étant donné que différents compilateurs utilisent différentes règles de gestion des noms,
le code C++ compilé par différents compilateurs peut ne pas pouvoir être lié. Ce fait est
important lorsque l'on envisage d'utiliser une bibliothèque C++ précompilée ! Si l'on souhaite
écrire une fonction en assembleur qui sera utilisée avec du code C++, il faut connaître les
règles de nommage du compilateur C++ à utiliser (ou utiliser la technique expliquée cidessous).
L'élève astucieux peut se demander si le code de la figure 7.10 fonctionnera comme prévu.
Étant donné que le nom C++ modifie toutes les fonctions, la fonction printf sera modifiée et le
compilateur ne produira pas d'appel à l'étiquette printf. C'est une préoccupation valable! Si le
prototype de printf était simplement placé en haut du fichier, cela se produirait. Le prototype
est :
int printf ( const char , ...);
DJGPP détruirait ceci pour être printf FPCce. (Le F est pour la fonction, P
5La correspondance n'a pas besoin d'être une correspondance exacte, le compilateur prendra en compte
les correspondances faites en castant les arguments. Les règles de ce processus sortent du cadre de ce
livre. Consultez un livre C++ pour plus de détails.
Machine Translated by Google
7.2. ASSEMBLAGE ET C++ 153
pour pointeur, C pour const, c pour char et e pour points de suspension.) Cela
n'appellerait pas la fonction printf de la bibliothèque C normale ! Bien sûr, il doit y avoir
un moyen pour que le code C++ appelle le code C. C'est très important car il y a
beaucoup d'ancien code C utile. En plus de permettre d'appeler du code C hérité, C++
permet également d'appeler du code assembleur en utilisant les conventions de
manipulation C normales.
C++ étend le mot clé extern pour lui permettre de spécifier que la fonction ou la
variable globale qu'il modifie utilise les conventions C normales. Dans la terminologie C+
+, la fonction ou la variable globale utilise la liaison C. Par exemple, pour déclarer printf
avoir une liaison C, utilisez le prototype :
extern ”C” int printf ( const car , ... );
Cela indique au compilateur de ne pas utiliser les règles de manipulation de noms C++
sur cette fonction, mais d'utiliser à la place les règles C. Cependant, en procédant ainsi,
la fonction printf ne peut pas être surchargée. Cela fournit le moyen le plus simple
d'interfacer C++ et l'assemblage, de définir la fonction pour utiliser la liaison C, puis
d'utiliser la convention d'appel C.
Par commodité, C++ permet également de définir l'enchaînement d'un bloc de
fonctions et de variables globales. Le bloc est désigné par les accolades habituelles.
extern ”C” { /
Variables globales de liaison C et prototypes de fonctions / }
Si l'on examine les fichiers d'entête ANSI C fournis avec C/C++ com
aujourd'hui, ils trouveront ce qui suit en haut de chaque fichier d'entête :
#ifdef cplusplus
extern
”C” { #endif
Et une construction similaire vers le bas contenant une accolade fermante.
Les compilateurs C++ définissent la macro cplusplus (avec deux sousscores en tête).
L'extrait cidessus enferme l'intégralité du fichier d'entête dans un bloc "C" externe si le
fichier d'entête est compilé en C++, mais ne fait rien s'il est compilé en C (puisqu'un
compilateur C donnerait une erreur de syntaxe pour "C" externe). Cette même technique
peut être utilisée par n'importe quel programmeur pour créer un fichier d'entête pour les
routines d'assemblage qui peuvent être utilisées avec C ou C++.
7.2.2 Références
Les références sont une autre nouvelle fonctionnalité de C++. Ils permettent de
passer des paramètres aux fonctions sans utiliser explicitement des pointeurs. Par
exemple, considérons le code de la Figure 7.11. En fait, les paramètres de référence sont assez
Machine Translated by Google
154 CHAPITRE 7. STRUCTURES ET C++
1 vide f ( int & x ) 2 { x+ // le & désigne un paramètre de référence
+; }
3
4 int main() 5
{ int
6 y = 5; f (y);
7 printf // la référence à y est passée, notez non & ici !
8 ("%d\n", y); // affiche 6 ! renvoie 0 ;
9
10 }
Figure 7.11 : Exemple de référence
simples, ce ne sont vraiment que des pointeurs. Le compilateur cache simplement cela au
programmeur (tout comme les compilateurs Pascal implémentent les paramètres var en
tant que pointeurs). Lorsque le compilateur génère l'assembly pour l'appel de fonction à la
ligne 7, il passe l'adresse de y. Si on écrivait la fonction f en assembleur, on agirait comme
si le prototype était6 :
vide f ( int xp);
Les références ne sont qu'une commodité particulièrement utile pour la surcharge
des opérateurs. C'est une autre fonctionnalité de C++ qui permet de définir des
significations pour les opérateurs communs sur les types de structure ou de classe. Par
exemple, une utilisation courante consiste à définir l'opérateur plus (+) pour concaténer des objets chaîne.
Ainsi, si a et b étaient des chaînes, a + b renverrait la concaténation des chaînes a et b.
C++ appellerait en fait une fonction pour ce faire (en fait, ces expressions pourraient être
réécrites en notation de fonction comme opérateur +(a,b)).
Pour plus d'efficacité, on aimerait passer l'adresse des objets de chaîne au lieu de les
passer par valeur. Sans références, cela pourrait être fait comme opérateur +(&a,&b),
mais cela nécessiterait d'écrire dans la syntaxe de l'opérateur comme &a + &b. Ce serait
très gênant et déroutant. Cependant, en utilisant des références, on peut l'écrire sous la
forme a + b, ce qui semble très naturel.
7.2.3 Fonctions en ligne
Les fonctions inline sont encore une autre fonctionnalité de C++7 . Les fonctions en ligne sont
destinées à remplacer les macros basées sur le préprocesseur et sujettes aux erreurs qui prennent
des paramètres. Rappelezvous de C, que l'écriture d'une macro qui met au carré un nombre peut
ressembler à :
6Bien sûr, ils pourraient vouloir déclarer la fonction avec une liaison C pour éviter le nom
mangling comme indiqué dans la section 7.2.1 Les
7
compilateurs C prennent souvent en charge cette fonctionnalité en tant qu'extension de ANSI C.
Machine Translated by Google
7.2. ASSEMBLAGE ET C++ 155
1 inline in inline f ( int x )
2 { retourne x x ; }
3
4 entier f ( entier x )
5 { retourne x x ; }
6
7 entier principal()
8 {
9 int y , x = 5;
dix y = f(x );
11 y = en ligne f (x );
12 renvoie 0 ;
13 }
Figure 7.12 : Exemple d'inlining
#define SQR(x) ((x) (x))
Parce que le préprocesseur ne comprend pas C et fait des substitutions simples, les
parenthèses sont nécessaires pour calculer la bonne réponse dans
la plupart des cas. Cependant, même cette version ne donnera pas la bonne réponse pour
CARRÉ(x++).
Les macros sont utilisées car elles éliminent la surcharge liée à l'appel d'une fonction
pour une fonction simple. Comme l'a montré le chapitre sur les sousprogrammes,
effectuer un appel de fonction implique plusieurs étapes. Pour une fonction très simple,
le temps qu'il faut pour faire l'appel de la fonction peut être plus que le temps de
effectuer réellement les opérations dans la fonction ! Les fonctions en ligne sont beaucoup
manière plus conviviale d'écrire du code qui ressemble à une fonction normale, mais qui
n'appelle pas un bloc de code commun. Au lieu de cela, les appels aux fonctions en ligne sont
remplacé par le code qui exécute la fonction. C++ permet à une fonction d'être
faite en ligne en plaçant le motclé inline devant la définition de la fonction. Par exemple,
considérons les fonctions déclarées dans la Figure 7.12. L'appel
à la fonction f sur la ligne 10 fait un appel de fonction normal (en assembleur, en supposant
x est à l'adresse ebp8 et y est à ebp4) :
1 pousser dword [ebp8]
2 appeler _f
3 populaire
exx
4 mouvement
[ebp4], eax
Cependant, l'appel à la fonction inline f sur la ligne 11 ressemblerait à :
Machine Translated by Google
156 CHAPITRE 7. STRUCTURES ET C++
1 mouvement
eax, [ebp8]
2 imul eax, eax
3 mouvement
[ebp4], eax
Dans ce cas, il y a deux avantages à l'inlining. Premièrement, la fonction en ligne est plus
rapide. Aucun paramètre n'est poussé sur la pile, aucun cadre de pile n'est
créé puis détruit, aucune branche n'est créée. Deuxièmement, l'appel de fonction en ligne utilise
moins de code ! Ce dernier point est vrai pour cet exemple, mais ne
pas vrai dans tous les cas.
Le principal inconvénient de l'inlining est que le code inline n'est pas lié et
le code d'une fonction en ligne doit donc être disponible pour tous les fichiers qui l'utilisent.
L'exemple de code d'assemblage précédent le montre. L'appel du noninline
fonction ne nécessite que la connaissance des paramètres, le type de valeur de retour,
convention d'appel et le nom de l'étiquette de la fonction. Tout ça
les informations sont disponibles à partir du prototype de la fonction. Cependant, en utilisant
la fonction en ligne nécessite la connaissance de tout le code de la fonction.
Cela signifie que si une partie d'une fonction en ligne est modifiée, toutes les sources
les fichiers qui utilisent la fonction doivent être recompilés. Rappelons que pour les noninline
fonctions, si le prototype ne change pas, souvent les fichiers qui utilisent le
la fonction n'a pas besoin d'être recompilée. Pour toutes ces raisons, le code de inline
les fonctions sont généralement placées dans des fichiers d'entête. Cette pratique est contraire à la
règle dure et rapide normale en C selon laquelle les instructions de code exécutables ne sont jamais
placés dans les fichiers d'entête.
7.2.4 Cours
Une classe C++ décrit un type d'objet. Un objet a à la fois des membres de données et des
membres de fonction8 . En d'autres termes, c'est une structure avec des données et
fonctions qui lui sont associées. Considérons la classe simple définie dans la Figure 7.13.
Une variable de type Simple ressemblerait à une structure C normale avec un
En fait, C++ utilise le seul membre int. Les fonctions ne sont pas stockées dans la mémoire affectée au
ce mot clé pour accéder au structure. Cependant, les fonctions membres sont différentes des autres fonctions.
pointeur vers l'objet agi Un paramètre caché leur est passé. Ce paramètre est un pointeur vers le
de l'intérieur du membre
objet sur lequel la fonction membre agit.
fonction.
Par exemple, considérons la méthode set data de la classe Simple de la figure 7.13. Si elle
était écrite en C, elle ressemblerait à une fonction
passé explicitement un pointeur vers l'objet sur lequel on agit comme le montre le code de la figure
7.14. Le commutateur S sur le compilateur DJGPP (ainsi que gcc et
les compilateurs Borland également) indique au compilateur de produire un fichier d'assemblage
contenant le langage assembleur équivalent pour le code produit. Pour
DJGPP et gcc le fichier d'assemblage se termine par une extension .s et malheureusement
8Souvent appelées fonctions membres en C++ ou plus généralement méthodes.
Machine Translated by Google
7.2. ASSEMBLAGE ET C++ 157
1 classe Simple {
2 publics :
3 Simple (); // constructeur par défaut
4 simple (); // destructeur
5 int obtenir des données () const ; // fonctions membres
6 données d'ensemble vides ( int ) ;
7 privés :
8 données // données des membres
9 entières ; } ;
dix
11 Simple :: Simple()
12 { données = 0 ; }
13
14 Simple ::˜Simple()
15 { / corps nul / }
16
17 int Simple :: obtenir des données () const
18 { renvoie les données ; }
19
20 void Simple :: set data ( int x )
21 { données = x ; }
Figure 7.13 : Une classe C++ simple
utilise actuellement la syntaxe du langage d'assemblage AT&T qui est assez différente de
Syntaxe NASM et MASM9 . (Les compilateurs Borland et MS génèrent un fichier
avec une extension .asm utilisant la syntaxe MASM.) La figure 7.15 montre la sortie
de DJGPP converti en syntaxe NASM et avec des commentaires ajoutés pour clarifier
le but des déclarations. Sur la toute première ligne, notez que les données définies
la méthode se voit attribuer une étiquette mutilée qui encode le nom de la méthode,
le nom de la classe et les paramètres. Le nom de la classe est codé
car d'autres classes peuvent avoir une méthode nommée set data et les deux
les méthodes doivent recevoir des étiquettes différentes. Les paramètres sont encodés de façon
que la classe peut surcharger la méthode set data pour prendre d'autres paramètres
tout comme les fonctions C++ normales. Cependant, comme auparavant, différents compilateurs
encodera ces informations différemment dans l'étiquette mutilée.
Ensuite, aux lignes 2 et 3, le prologue familier de la fonction apparaît. Sur la ligne 5,
9Le compilateur gcc inclut son propre assembleur appelé gas. L'assembleur de gaz
utilise la syntaxe AT&T et le compilateur génère donc le code au format gaz. Là
sont plusieurs pages sur le Web qui traitent des différences entre les formats INTEL et AT&T.
Il existe aussi un programme gratuit nommé a2i (http://www.multimania.com/placr/a2i.html),
qui convertit le format AT&T au format NASM.
Machine Translated by Google
158 CHAPITRE 7. STRUCTURES ET C++
void set data ( Simple object , int x )
{
objet>données = x ;
}
Figure 7.14 : Version C de Simple::set data()
1 _set_data__6Simple : push ebp ; nom mutilé
2
3 mouvement
ebp, esp
4
5 mouvement
eax, [ebp + 8] ; eax = pointeur vers l'objet (ceci)
6 mouvement
edx, [ebp + 12] ; edx = paramètre entier
7 mouvement [eax], édx ; les données sont à l'offset 0
8
9 partir
dix ret
Figure 7.15 : Sortie du compilateur de Simple::set data( int )
le premier paramètre de la pile est stocké dans EAX. Ce n'est pas le paramètre x ! Au lieu de
cela, c'est le paramètre caché10 qui pointe vers l'objet
mis en oeuvre. La ligne 6 stocke le paramètre x dans EDX et la ligne 7 stocke EDX dans
le double mot vers lequel EAX pointe. Ceci est le membre de données du Simple
l'objet sur lequel on agit, qui étant la seule donnée de la classe, est stocké à
offset 0 dans la structure simple.
Exemple
Cette section utilise les idées du chapitre pour créer une classe C++ qui
représente un entier non signé de taille arbitraire. Comme l'entier peut être
quelle que soit sa taille, il sera stocké dans un tableau d'entiers non signés (mots doubles). Il
peut être fait n'importe quelle taille en utilisant l'allocation dynamique. Les doubles mots sont
stocké dans l'ordre inverse11 (c'estàdire que le mot double le moins significatif est à l'index
0). La figure 7.16 montre la définition de la classe Big int12. La taille d'un
Big int est mesuré par la taille du tableau non signé utilisé pour stocker
10Comme d'habitude, rien n'est caché dans le code assembleur !
11Pourquoi ? Parce que les opérations d'addition commenceront alors toujours le traitement au début
du tableau et aller de l'avant.
12Voir la source de l'exemple de code pour le code complet de cet exemple. Le texte va
ne faire référence qu'à une partie du code.
Machine Translated by Google
7.2. ASSEMBLAGE ET C++ 159
1 classe Grand int {
2 publics :
3 /
4 Paramètres :
5 taille − taille de l'entier exprimée en nombre de
'
6 valeur initiale normale s
7 non signée int valeur initiale de Big int comme un entier non signé normal
8 /
9 explicite Big int (taille t taille ,
dix
valeur initiale non signée = 0);
11 /
12 Paramètres :
13 taille − taille de l'entier exprimée en nombre de
'
14 valeur initiale int s
15 normale non signée valeur initiale de Big int sous forme de chaîne contenant
16 représentation hexadécimale de la valeur .
17 /
18 Big int ( taille t taille ,
19 const char valeur initiale );
20
21 Big int ( const Big int & big int à copier );
22 ˜Grand entier ();
23
24 // renvoie la taille de Big int (en termes d'entiers non signés)
25 taille t taille () const;
26
27 const Big int & operator = ( const Big int & big int à copier );
28 ami Big int opérateur + ( const Big int & op1,
29 const Grand int & op2 );
30 ami Opérateur Big int − ( const Big int & op1,
31 const Big int & op2);
32 ami bool opérateur == ( const Big int & op1,
33 const Grand int & op2 );
34 ami bool opérateur < ( const Big int & op1,
35 const Big int & op2);
36 ami ostream & opérateur << ( ostream & os,
37 const Big int & op );
38 privés :
39 taille t taille ; // taille du tableau non signé
40 nombre non signé ; // pointeur vers un tableau non signé contenant la valeur
41 } ;
Figure 7.16 : Définition de la classe Big int
Machine Translated by Google
160 CHAPITRE 7. STRUCTURES ET C++
1 // prototypes pour les routines d'assemblage
2 "C" externes {
3 int ajouter de grands ints ( Big int résolution ,
4 & const Big int & op1,
5 const Big int & op2);
6 int sub big ints ( Big int & const Big résolution ,
7 int & op1,
8 const Big int & op2);
9 }
dix
11 opérateur Big int en ligne + ( const Big int & op1, const Big int & op2)
12 {
13 Résultat Big int (op1. size ());
14 int res = ajouter de gros entiers (résultat, op1, op2);
15 si ( res == 1)
16 throw Big int :: Débordement ();
17 si ( res == 2)
18 throw Big int :: Erreur de taille();
19 retour résultat ;
20 }
21
22 opérateur Big int en ligne − ( const Big int & op1, const Big int & op2)
23 {
24 Résultat Big int (op1. size ());
25 int res = sub big ints (résultat, op1, op2);
26 si ( res == 1)
27 throw Big int :: Débordement ();
28 si ( res == 2)
29 throw Big int :: Erreur de taille();
30 retour résultat ;
31 }
Figure 7.17 : Code arithmétique de la classe Big int
Machine Translated by Google
7.2. ASSEMBLAGE ET C++ 161
ses données. Le membre de données de taille de la classe se voit attribuer le décalage zéro et le
membre de nombre se voit attribuer le décalage 4.
Pour simplifier ces exemples, seules les instances d'objets de même taille peuvent être
ajoutées ou soustraites les unes des autres.
La classe a trois constructeurs : le premier (ligne 9) initialise l'instance de classe en utilisant
un entier non signé normal ; la seconde (ligne 18) initialise l'instance en utilisant une chaîne
contenant une valeur hexadécimale. Le troisième constructeur (ligne 21) est le constructeur de
copie .
Cette discussion se concentre sur le fonctionnement des opérateurs d'addition et de
soustraction puisque c'est là que le langage d'assemblage est utilisé. La figure 7.17 montre les
parties pertinentes du fichier d'entête pour ces opérateurs. Ils montrent comment les opérateurs
sont configurés pour appeler les routines d'assemblage. Étant donné que différents compilateurs
utilisent des règles de manipulation radicalement différentes pour les fonctions d'opérateur, les
fonctions d'opérateur en ligne sont utilisées pour configurer des appels aux routines
d'assemblage de liaison C. Cela facilite le portage vers différents compilateurs et est tout aussi
rapide que les appels directs. Cette technique élimine également le besoin de lever une
exception depuis l'assemblage !
Pourquoi l'assemblage estil utilisé ici ? Rappelons que pour effectuer une arithmétique
de précision multiple, la retenue doit être déplacée d'un dword pour être ajoutée au prochain
dword significatif. C++ (et C) ne permettent pas au programmeur d'accéder à l'indicateur de
retenue du processeur. L'ajout ne peut être effectué qu'en demandant à C++ de recalculer
indépendamment l'indicateur de report et de l'ajouter conditionnellement au prochain dword. Il
est beaucoup plus efficace d'écrire le code en assembleur où l'indicateur de retenue est
accessible et d'utiliser l'instruction ADC qui ajoute automatiquement l'indicateur de retenue en
a beaucoup de sens.
Par souci de brièveté, seule la routine d'assemblage add big ints sera abordée
ici. Vous trouverez cidessous le code de cette routine (de big math.asm):
grand math.asm
1 segment .text global
2 add_big_ints, sub_big_ints
3 %définir size_offset 0 4 %définir
number_offset 4
5
6 %définir EXIT_OK 0 7
%définir EXIT_OVERFLOW 1 8
%définir EXIT_SIZE_MISMATCH 2
9
dix ; Paramètres pour les routines add et sub
11 %définir res ebp+8 12
%définir op1 ebp+12 13 %définir
op2 ebp+16
14
Machine Translated by Google
162 CHAPITRE 7. STRUCTURES ET C++
15 add_big_ints :
16 pousser ebp
17 mouvement
ebp, esp
18 pousser ebx
19 pousser esi
20 pousser édi
21 ;
22 ; configurez d'abord esi pour qu'il pointe vers op1
23 ; edi pour pointer vers op2
24 ; ebx pour pointer vers res
25 mouvement
esi, [op1]
26 mouvement
édi, [op2]
27 mouvement ebx, [res]
28 ;
29 ; assurezvous que les 3 Big_int ont la même taille
30 ;
31 mouvement eax, [esi + size_offset]
32 cmp eax, [edi + size_offset]
33 jne tailles_not_equal eax, ; op1.size_ != op2.size_
34 cmp [ebx + size_offset]
35 jne tailles_pas_égales ; op1.size_ != res.size_
36
48 cc ; drapeau de port clair
49 xor edx, edx ; edx = 0
50 ;
51 ; boucle d'addition
52 add_loop :
53 mouvement eax, [edi+4*edx]
54 adc eax, [esi+4*edx]
55 mouvement [ebx + 4*edx], eax
56 inc. edx ; ne modifie pas porter le drapeau
Machine Translated by Google
7.2. ASSEMBLAGE ET C++ 163
57 boucle add_loop
58
59 jc débordement
60 ok_done :
61 xor eax, eax ; valeur de retour = EXIT_OK
62 fait
débordement jmp 63 :
64 mouvement eax, EXIT_OVERFLOW
65 jmp fait
66 tailles_pas_égal :
67 mouvement eax, EXIT_SIZE_MISMATCH
68 effectués :
69 populaire
édi
70 populaire
esi
71 populaire
ebx
72 partir
73 ret
grand math.asm
Espérons que la plupart de ce code devrait être simple pour le lecteur en
maintenant. Les lignes 25 à 27 stockent des pointeurs vers les objets Big int passés au
fonction dans les registres. N'oubliez pas que les références ne sont que des pointeurs.
Les lignes 31 à 35 vérifient que les tailles des tableaux des trois objets
sont identiques. (Notez que le décalage de la taille est ajouté au pointeur pour accéder
le membre de données.) Les lignes 44 à 46 ajustent les registres pour pointer vers le tableau
utilisé par les objets respectifs au lieu des objets euxmêmes. (Encore,
le décalage du membre numérique est ajouté au pointeur d'objet.)
La boucle des lignes 52 à 57 additionne les entiers stockés dans les tableaux
en ajoutant d'abord le dword le moins significatif, puis le suivant le moins significatif
dwords, etc. L'addition doit être faite dans cette séquence pour l'arithmétique de précision étendue
(voir Section 2.1.5). La ligne 59 vérifie le débordement, en cas de débordement
le drapeau de retenue sera défini par le dernier ajout du dword le plus significatif.
Étant donné que les dwords du tableau sont stockés dans l'ordre little endian, la boucle commence
au début du tableau et avance vers la fin.
La figure 7.18 montre un court exemple utilisant la classe Big int. Noter que
Les constantes Big int doivent être déclarées explicitement comme à la ligne 16. Ceci est nécessaire
pour deux raisons. Tout d'abord, il n'y a pas de constructeur de conversion qui convertira
un int non signé à un Big int. Deuxièmement, seuls les Big int de même taille peuvent
être ajouté. Cela rend la conversion problématique puisqu'il serait difficile de
savoir quelle taille convertir. Une implémentation plus sophistiquée de la
classe permettrait d'ajouter n'importe quelle taille à n'importe quelle autre taille. L'auteur n'a pas
souhaite trop compliquer cet exemple en l'implémentant ici. (Cependant,
le lecteur est encouragé à le faire.)
Machine Translated by Google
164 CHAPITRE 7. STRUCTURES ET C++
1 #include "gros int.hpp"
2 #include <iostream>
3 en utilisant l'espace de noms std ;
4
5 entier principal()
6 {
7 essayer {
8 Grand entier b(5,"8000000000000a00b");
9 Grand entier a(5,"80000000000010230");
dix Grand entier c = a + b ;
” ” ” = ”
11 cout << a << + << b << << c << finl;
12 for( int i=0; i < 2; i++ ) {
13 c = c + une ;
”
14 cout << ”c = << c << finl;
15 }
”
16 cout << ”c−1 = << c − Big int(5,1) << endl;
17 Grand int d (5, "12345678");
”
18 cout << ”d = << d << endl;
19 cout << ”c == d ” << (c == d) << endl;
20 cout << ”c > d ” << (c > d) << finl ;
21 }
22 catch( const car str ) {
23 cerr << ” Pris: ” << str << endl;
24 }
25 catch( Big int :: Débordement ) {
26 cerr << "Débordement" << endl;
27 }
28 catch( Big int :: Taille inadaptée ) {
29 cerr << "Nonconcordance de taille" << endl;
30 }
31 renvoie 0 ;
32 }
Figure 7.18 : Utilisation simple de Big int
Machine Translated by Google
7.2. ASSEMBLAGE ET C++ 165
1 #include <cstddef>
2 #include <iostream>
3 en utilisant l'espace de noms std ;
4
5 classe A {
6 publics :
7 void cdecl m() { cout << ”A::m()” << endl; }
8 annonce int ;
9 } ;
dix
11 classe B : publique A {
12 publics :
13 void cdecl m() { cout << ”B::m()” << endl; }
14 bd int ;
15 } ;
16
17 vide f ( A p )
18 {
19 p−>ad = 5 ;
20 p−>m();
21 }
22
23 entier principal()
24 {
25 A un;
26 B b ;
”
27 cout << ”Taille de a: << sizeof(a)
28 << " Décalage de l'annonce : " << offsetof(A,ad) << endl;
29 cout << ”Taille de b: ” << sizeof(b)
30 << ” Décalage de l'annonce : ” << offsetof(B,ad)
31 << ” Offset of bd: ” << offsetof(B,bd) << endl;
32 FA);
33 f(&b);
34 renvoie 0 ;
35 }
Figure 7.19 : Héritage simple
Machine Translated by Google
166 CHAPITRE 7. STRUCTURES ET C++
1 _f__FP1A : ; nom de fonction mutilé
2 pousser ebp
3 mouvement
ebp, esp
4 mouvement
eax, [ebp+8] ; eax pointe vers l'objet
5 mouvement dword [eax], 5 eax, ; en utilisant le décalage 0 pour l'annonce
6 mouvement
[ebp+8] pousser ; transmettre l'adresse de l'objet à A::m()
7 eax
8 appeler _m__1A ; nom de méthode mutilé pour A :: m ()
9 ajouter esp, 4
dix partir
11 ret
Figure 7.20 : Code d'assemblage pour l'héritage simple
7.2.5 Héritage et polymorphisme
L'héritage permet à une classe d'hériter des données et des méthodes d'une autre.
Par exemple, considérons le code de la Figure 7.19. Il montre deux classes, A et
B, où la classe B hérite de A. La sortie du programme est :
Taille d'un : 4 Décalage de l'annonce : 0
Taille de b : 8 Décalage de ad : 0 Décalage de bd : 4
Suis()
Suis()
Notez que les membres de données d'annonce des deux classes (B l'hérite de A) sont
au même décalage. Ceci est important puisque la fonction f peut être passée a
pointeur vers un objet A ou tout objet d'un type dérivé (c'estàdire hérité
à partir de) A. La figure 7.20 montre le code asm (modifié) pour la fonction (généré
par gcc).
Notez que dans la sortie, la méthode m de A a été appelée à la fois pour a et
b objets. Depuis l'assembly, on peut voir que l'appel à A::m() est codé en dur dans la fonction.
Pour une véritable programmation orientée objet, la méthode
call doit dépendre du type d'objet passé à la fonction. Ce
est connu sous le nom de polymorphisme. C++ désactive cette fonctionnalité par défaut. On utilise
le motclé virtual pour l'activer. La figure 7.21 montre comment les deux classes
serait changé. Aucun des autres codes ne doit être modifié. Le polymorphisme peut être mis en
œuvre de plusieurs façons. Malheureusement, l'implémentation de gcc
est en transition au moment d'écrire ces lignes et devient de plus en plus
compliquée que sa mise en œuvre initiale. Dans un souci de simplification
Dans cette discussion, l'auteur ne couvrira que l'implémentation du polymorphisme utilisé par les
compilateurs Microsoft et Borland basés sur Windows. Ce
Machine Translated by Google
7.2. ASSEMBLAGE ET C++ 167
1 classe A { 2
public:
3 virtual void cdecl m() { cout << ”A::m()” << endl; } annonce int ; } ;
4
7 classe B : public A { 8
public : vide
9 virtuel cdecl m() { cout << ”B::m()” << endl; } int bd ; } ;
dix
11
Figure 7.21 : Héritage polymorphe
la mise en œuvre n'a pas changé depuis de nombreuses années et ne changera probablement
pas dans un avenir prévisible.
Avec ces modifications, la sortie du programme change :
Taille d'un : 8 Décalage de l'annonce : 4
Taille du b : 12 Décalage de l'annonce : 4 Décalage du bd : 8
Suis()
B::m()
Maintenant, le deuxième appel à f appelle la méthode B::m() car on lui passe un objet B.
Ce n'est cependant pas le seul changement. La taille d'un A est maintenant 8 (et B est 12). De
plus, le décalage de l'annonce est 4, et non 0. Qu'y atil au décalage 0 ? La réponse à ces
questions est liée à la manière dont le polymorphisme est implémenté.
Une classe C++ qui a des méthodes virtuelles reçoit un champ caché supplémentaire qui
est un pointeur vers un tableau de pointeurs de méthode13. Cette table est souvent appelée
vtable. Pour les classes A et B, ce pointeur est stocké à l'offset 0. Les compilateurs Windows
placent toujours ce pointeur au début de la classe en haut de l'arbre d'héritage. En regardant
le code assembleur (Figure 7.22) généré pour la fonction f (de la Figure 7.19) pour la version
méthode virtuelle du programme, on peut voir que l'appel à la méthode m n'est pas à une
étiquette.
La ligne 9 trouve l'adresse de la vtable à partir de l'objet. L'adresse de l'objet est poussée sur
la pile à la ligne 11. La ligne 12 appelle la méthode virtuelle en se branchant à la première
adresse dans la vtable14. Cet appel n'utilise pas d'étiquette, il se branche sur l'adresse de
code pointée par EDX. Ce type d'appel est un
13Pour les classes sans méthodes virtuelles, les compilateurs C++ rendent toujours la classe compatible
avec une structure C normale avec les mêmes membres de données.
14Bien sûr, cette valeur est déjà dans le registre ECX. Il a été mis là dans la ligne 8 et la ligne 10 pourrait être
supprimée et la ligne suivante modifiée pour pousser ECX. Le code n'est pas très efficace car il a été généré sans
que les optimisations du compilateur soient activées.
Machine Translated by Google
168 CHAPITRE 7. STRUCTURES ET C++
1 ?f@@YAXPAVA@@@Z :
2 pousser ebp
3 mouvement
ebp, esp
4
5 mouvement
eax, [ebp+8]
6 mouvement
dmot [eax+4], 5 ; p> ad = 5 ;
7
8 mouvement
ecx, [ebp + 8] edx, ; ecx = p
9 mouvement [ecx] eax, [ebp ; edx = pointeur vers vtable
dix mouvement
+ 8] pousser eax ; eax = p
11 appeler dword ; poussez "ce" pointeur
12 [edx] esp, 4 ; appeler la première fonction dans vtable
13 ajouter ; nettoyer la pile
14
15 populaire ebp
16 ret
Figure 7.22 : Code d'assemblage pour la fonction f()
exemple de reliure tardive. Une liaison tardive retarde le choix de la méthode
à appeler jusqu'à ce que le code s'exécute. Cela permet au code d'appeler le
méthode pour l'objet. Le cas normal (Figure 7.20) code en dur un appel à un
certaine méthode et est appelée liaison précoce (car ici la méthode est liée
tôt, au moment de la compilation).
Le lecteur attentif se demandera pourquoi les méthodes de classe de la figure 7.21 sont
explicitement déclarées pour utiliser la convention d'appel C en utilisant
le mot clé cdecl. Par défaut, Microsoft utilise une convention d'appel différente pour les
méthodes de classe C++ que la convention C standard. Il passe le
pointeur vers l'objet sur lequel agit la méthode dans le registre ECX à la place
d'utiliser la pile. La pile est toujours utilisée pour les autres paramètres explicites
de la méthode. Le modificateur cdecl lui dit d'utiliser l'appel C standard
convention. Borland C++ utilise la convention d'appel C par défaut.
Examinons ensuite un exemple un peu plus compliqué (Figure 7.23).
Dans celuici, les classes A et B ont chacune deux méthodes : m1 et m2. Se souvenir
que puisque la classe B ne définit pas sa propre méthode m2, elle hérite de celle de la classe A
méthode. La figure 7.24 montre comment l'objet b apparaît en mémoire. Illustration 7.25
affiche la sortie du programme. Tout d'abord, regardez l'adresse de la vtable
pour chaque objet. Les adresses des deux objets B sont les mêmes et donc, ils
partager la même vtable. Une vtable est une propriété de la classe et non un objet (comme
un membre de données statique). Ensuite, regardez les adresses dans les vtables. Depuis
en regardant la sortie de l'assemblage, on peut déterminer que le pointeur de méthode m1
Machine Translated by Google
7.2. ASSEMBLAGE ET C++ 169
1 classe A {
2 publics :
3 vide virtuel cdecl m1() { cout << ”A::m1()” << endl; }
4 vide virtuel cdecl m2() { cout << ”A::m2()” << endl; }
5 annonce int ;
6 } ;
7
8 classe B : public A { // B hérite du m2() de A
9 publiques :
dix vide virtuel cdecl m1() { cout << ”B::m1()” << endl; }
11 bd int ;
12 } ;
13 / imprime la vtable de l'objet donné /
14 void print vtable ( A pa )
15 {
16 // p voit pa comme un tableau de dwords
17 non signé p = réinterpréter cast<non signé >(pa);
18 // vt voit vtable comme un tableau de pointeurs
19 void vt = réinterpréter cast<void >(p[0]);
”
20 cout << hex << ”adresse vtable = for( int << vt << endl;
21 i=0; i < 2; i++ )
22 cout << ”dword ” << i << ”: ” << vt[i] << endl;
23
24 // appelle des fonctions virtuelles de manière EXTRÊMEMENT non portable !
25 void ( m1func pointer)(A ); // variable de pointeur de fonction
26 pointeur m1func = réinterpréter cast<void ( )(A )>(vt[0]);
27 pointeur m1func(pa ); // appelle la méthode m1 via le pointeur de fonction
28
34 entier principal()
35 {
36 A a ; B b1; B b2;
37 cout << ”a: ” << << finl; imprimer vtable (&a);
38 cout << ”b1: ” endl; imprime vtable (&b);
39 cout << ”b2: ” << endl; imprime vtable (&b2);
40 renvoie 0 ;
41 }
Figure 7.23 : Exemple plus compliqué
Machine Translated by Google
170 CHAPITRE 7. STRUCTURES ET C++
0 0
vtablep &B::m1()
4 publicité 4 &A::m2()
8 bd vtable
b1
Figure 7.24 : Représentation interne de b1
un:
adresse vtable = 004120E8
dword 0 : 00401320
dword 1 : 00401350
A::m1()
A::m2()
b1 :
adresse vtable = 004120F0
dword 0 : 004013A0
dword 1 : 00401350
B::m1()
A::m2()
b2 :
adresse vtable = 004120F0
dword 0 : 004013A0
dword 1 : 00401350
B::m1()
A::m2()
Figure 7.25 : Sortie du programme de la Figure 7.23
Machine Translated by Google
7.2. ASSEMBLAGE ET C++ 171
est à l'offset 0 (ou dword 0) et m2 est à l'offset 4 (dword 1). Les pointeurs de méthode m2 sont
les mêmes pour les vtables de classe A et B car la classe B hérite de la méthode m2 de la
classe A.
Les lignes 25 à 32 montrent comment on pourrait appeler une fonction virtuelle en lisant
son adresse dans la vtable pour l'objet15. L'adresse de la méthode est stockée dans un
pointeur de fonction de type C avec un pointeur this explicite. D'après la sortie de la figure
7.25, on peut voir que cela fonctionne. Cependant, s'il vous plaît, n'écrivez pas de code comme
celuici! Ceci est uniquement utilisé pour illustrer comment les méthodes virtuelles utilisent la
vtable.
Il y a quelques leçons pratiques à en tirer. Un fait important est qu'il faudrait être très
prudent lors de la lecture et de l'écriture de variables de classe dans un fichier binaire. On ne
peut pas simplement utiliser une lecture ou une écriture binaire sur l'objet entier car cela lirait
ou écrirait le pointeur vtable vers le fichier ! Il s'agit d'un pointeur vers l'endroit où réside la
vtable dans la mémoire du programme et varie d'un programme à l'autre. Ce même problème
peut se produire en C avec des structures, mais en C, les structures ne contiennent des
pointeurs que si le programmeur les met explicitement. Il n'y a pas de pointeurs évidents
définis dans les classes A ou B.
Encore une fois, il est important de réaliser que différents compilateurs implémentent
différemment les méthodes virtuelles. Sous Windows, les objets de classe COM (Component
Object Model) utilisent des vtables pour implémenter des interfaces COM16. Seuls les
compilateurs qui implémentent des vtables de méthodes virtuelles comme le fait Microsoft
peuvent créer des classes COM. C'est pourquoi Borland utilise la même implémentation que
Microsoft et l'une des raisons pour lesquelles gcc ne peut pas être utilisé pour créer des classes COM.
Le code de la méthode virtuelle ressemble exactement à un code non virtuel.
Seul le code qui l'appelle est différent. Si le compilateur peut être absolument sûr de la
méthode virtuelle qui sera appelée, il peut ignorer la vtable et appeler la méthode directement
(par exemple, utiliser la liaison anticipée).
7.2.6 Autres fonctionnalités C++
Le fonctionnement des autres fonctionnalités C++ (par exemple, les informations de type
RunTime, la gestion des exceptions et l'héritage multiple) dépasse le cadre de ce texte. Si le
lecteur souhaite aller plus loin, un bon point de départ est The Annotated C++ Reference
Manual par Ellis et Stroustrup et The Design and Evolution of C++ par Stroustrup.
15N'oubliez pas que ce code ne fonctionne qu'avec les compilateurs MS et Borland, pas avec gcc.
Les classes 16COM utilisent également la convention d'appel stdcall, et non la convention C standard.
Machine Translated by Google
172 CHAPITRE 7. STRUCTURES ET C++
Machine Translated by Google
Annexe A
Consignes 80x86
A.1 Instructions en virgule non flottante
Cette section répertorie et décrit les actions et les formats des non
instructions en virgule flottante de la famille de processeurs Intel 80x86.
Les formats utilisent les abréviations suivantes :
R registre général
Registre R8 8 bits
Registre R16 16 bits
Registre R32 32 bits
Registre des segments SR
Mémoire M
Octet M8
Mot M16
M32 mot double
je valeur immédiate
Cellesci peuvent être combinées pour les instructions à opérandes multiples. Par
exemple, le format R, R signifie que l'instruction prend deux opérandes de registre.
La plupart des instructions à deux opérandes autorisent les mêmes opérandes.
L'abréviation O2 est utilisée pour représenter ces opérandes : R,RR,MR,IM,RM,I. Si un
registre ou une mémoire 8 bits peut être utilisé pour un opérande, l'abréviation R/M8
est utilisée.
Le tableau montre également comment les différents bits du registre FLAGS sont
affectés par chaque instruction. Si la colonne est vide, le bit correspondant n'est pas du
tout affecté. Si le bit est toujours remplacé par une valeur particulière, un 1 ou un 0
s'affiche dans la colonne. Si le bit est remplacé par une valeur qui dépend des
opérandes de l'instruction, un C est placé dans la colonne. Enfin, si le bit est modifié
d'une manière indéfinie a ? apparaît dans la colonne. Parce que le
173
Machine Translated by Google
174 ANNEXE A. INSTRUCTIONS 80X86
seules les instructions qui changent le drapeau de direction sont CLD et STD, ce n'est pas
répertoriés dans les colonnes DRAPEAUX.
Drapeaux
Nom Description Formats OSZAPC
ADC Ajouter avec transporter O2 CCCCCC
AJOUTER
Ajouter des entiers O2 CCCCCC
ET ET au niveau du bit O2 0 CC ? C 0
BSWAP Échange d'octets R32
APPEL Routine d'appel IRM
CBW Convertir un octet en mot
CDQ Convertir Dword en
Qmot
CLC Transport clair 0
CLD Effacer le drapeau de direction
CMC Complémentaire C
CMP Comparer des entiers O2 CCCCCC
CMPSB Comparer les octets CCCCCC
CMPSW Comparer des mots CCCCCC
CMPSD Comparez Dwords CCCCCC
MDC Convertir Word en
Dword en DX:AX
CWDE Convertir Word en
Dword dans EAX
DÉC Décrémenter un entier RM CCCCC
DIV Division non signée RM ? ? ? ? ? ?
ENTRER Créer un cadre de pile Je,0
IDIV Diviser signé RM ? ? ? ? ? ?
IMUL Multiplier signé RM C ? ? ? ? C
R16,R/M16
R32,R/M32
R16,je
R32,je
R16,R/M16,I
R32,R/M32,I
Inc Incrémenter l'entier RM CCCCC
INT Générer une interruption je
JA Sauter audessus je
JAE Sauter audessus ou égal I
JB Sauter cidessous je
JBE Sauter en dessous ou égal I
JC Sauter Porter je
Machine Translated by Google
A.1. INSTRUCTIONS EN POINT NON FLOTTANT 175
Drapeaux
Nom Description Formats OSZAPC
JCXZ Sauter si CX = 0 je
JE Sauter égal je
JG Sauter plus grand je
JGE Sauter plus grand ou je
Égal
JL Sauter moins je
JLE Sauter moins ou égal je
JMP Saut inconditionnel RMI
JNA Sauter pas audessus je
JNAE Sauter pas audessus ou je
Égal
JNB Sauter pas en dessous je
JNBE Sauter pas en dessous ou je
Égal
JNC Sauter sans porter je
JNE Sauter pas égal je
JNG Sauter pas plus grand je
JNGE Sauter pas plus grand ou je
Égal
JNL Sauter pas moins je
JNLE Sauter pas moins ou je
Égal
JNO Sauter sans débordement je
JNS Sauter sans signe je
JNZ Sauter pas zéro je
JO Débordement de saut je
JPE Sauter la parité paire je
JEA Sauter la parité impaire je
JS Signe de saut je
JZ Sauter zéro je
LAHF Charger DRAPEAUX dans AH
LÉA Charger l'adresse effective R32,M
PARTIR Quitter le cadre de la pile
LODSB Charger l'octet
LODSW Charger le mot
LODSD Charger Dword
BOUCLE Boucle je
BOUCLE/LOOPZ Boucle si égal je
Boucle LOOPNE/LOOPNZ si non égale je
Machine Translated by Google
176 ANNEXE A. INSTRUCTIONS 80X86
Drapeaux
Nom Description Formats OSZAPC
MOV Déplacer des données O2
SR,R/M16
R/M16,SR
MOVSB Déplacer l'octet
MOVSW Déplacer le mot
MOVSD Déplacer Dword
MOVSX Déplacement signé R16,R/M8
R32,R/M8
R32,R/M16
MOVZX Déplacer sans signature R16,R/M8
R32,R/M8
R32,R/M16
MUL Multiplier RM non signé C ? ? ? ? C
NEG Nier RM CCCCCC
NON Pas d'opération
PAS Complément à 1 RM
OU OU au niveau du bit O2 0 CC ? C 0
POPULAIRE
Pop de la pile R/M16
R/M32
POPA Pop tout
POPF Drapeaux Pop CCCCCC
POUSSER Pousser pour empiler R/M16
R/M32 I
POUSSA Tout pousser
PUSHF Drapeaux à pousser
RLC Rotation à gauche avec Carry R/M,I C C
R/M,CL
RCR Rotation à droite avec R/M, je C C
Porter R/M,CL
REPRÉSENTANT
Répéter
REPE/REPZ Répéter si égal
REPNE/REPNZ Répéter si non égal
RET Retour
ROL Tourne à gauche R/M, je C C
R/M,CL
ROR Tourner à droite R/M, je C C
R/M,CL
SAHF Copie AH dans CCCCC
DRAPEAUX
Machine Translated by Google
A.1. INSTRUCTIONS EN POINT NON FLOTTANT 177
Drapeaux
Nom Description Formats OSZAPC
SEL Se déplace vers la gauche R/M, je C
R/M, CL
CFF Soustraire avec Emprunter O2 CCCCCC
SCASB Rechercher un octet CCCCCC
SCASW Rechercher Word CCCCCC
SCASD Rechercher Dword CCCCCC
SETA Définir audessus R/M8
SETAE Définir audessus ou égal à R/M8
SETB Définir cidessous R/M8
SETBE Définir en dessous ou égal à R/M8
CSET Set Carry R/M8
SÈTE Définir égal R/M8
SETG Définir plus grand R/M8
SETGE Définir supérieur ou égal à R/M8
SETL Définir moins R/M8
RÉGLER Définir inférieur ou égal R/M8
SETNA Définir pas audessus R/M8
SETNAE Définir pas audessus ou R/M8
Égal
SETNB Définir pas en dessous R/M8
SETNBE Définir pas en dessous ou R/M8
Égal
SETNC Ne pas transporter R/M8
SETNE Définir non égal R/M8
REGLAGE Définir non supérieur R/M8
RÉGLAGE Définir non supérieur ou R/M8
Égal
SETNL Définir pas moins R/M8
RÉGLER Set Not Less ou Equal R/M8
SETNO Définir aucun débordement R/M8
RÉGLAGES Définir aucun signe R/M8
SETNZ Définir pas zéro R/M8
SÉTO Définir le débordement R/M8
SETPE Définir la parité paire R/M8
SETPO Définir la parité impaire R/M8
ENSEMBLES
Définir le signe R/M8
SETZ Définir zéro R/M8
DAS Décalage arithmétique vers R/M, je C
Droite R/M, CL
Machine Translated by Google
178 ANNEXE A. INSTRUCTIONS 80X86
Drapeaux
Nom Description Formats OSZAPC
SHR Décalage logique vers la droite R/M,I C
R/M, CL
SHL Décalage logique vers la gauche R/M,I C
R/M, CL
SC Set Carry 1
MST Définir le drapeau de direction
STOSB Magasin Btye
STOSW Stocker le mot
STOSD Store Dword
SOUS Soustraire O2 CCCCCC
TEST Comparaison logique R/M,R 0 CC ? C 0
R/M, je
XCHG Échange R/M,R
R,R/M
XOR XOR au niveau du bit O2 0 CC ? C 0
Machine Translated by Google
A.2. INSTRUCTIONS EN POINT FLOTTANT 179
A.2 Instructions en virgule flottante
Dans cette section, de nombreuses instructions du coprocesseur mathématique 80x86 sont
décrit. La section de description décrit brièvement le fonctionnement du
instruction. Pour économiser de l'espace, des informations indiquant si l'instruction apparaît
la pile n'est pas donnée dans la description.
La colonne de format montre quel type d'opérandes peut être utilisé avec chaque
instruction. Les abréviations suivantes sont utilisées :
Registre du coprocesseur STn A
F Nombre simple précision en mémoire
D Nombre double précision en mémoire
E Nombre de précision étendue en mémoire
I16 Mot entier en mémoire
I32 Double mot entier en mémoire
I64 Mot quadruple entier en mémoire
Les instructions nécessitant un Pentium Pro ou supérieur sont marquées d'un astérisque
( ).
180 ANNEXE A. INSTRUCTIONS 80X86
FSQRT ST0 = √ STO
FST destination Stocker ST0 STn DF
Destination FSTP Stocker ST0 STn FDE
Destination FSTCW Mémoriser le registre de mot de contrôle I16
FSTSW destination Mémoriser le mot d'état Registre I16 AX
Source FSUB ST0 = source STn DF
Destination FSUB, ST0 destination = STO STn
FSUBP destination [,ST0] destination = ST0 STn
FSUBR src ST0 = source ST0 STn DF
FSUBR destination, ST0 dest = ST0dest STn
FSUBP destination [,ST0] dest = ST0dest STn
FTST Comparer ST0 avec 0.0
Destination FXCH Échangez ST0 et dest STn
Machine Translated by Google
Indice
ADC, 37, 54 C++, 150–171
ADD, 13, 36 Exemple Big int, 158–163 classes,
AND, 50 156–171 constructeur
array1.asm, 99–102 de copie, 161 liaison précoce,
tableaux, 95–115 168 extern ”C”, 153
accès, 96–102 définition, héritage, 166–171
95–96 variable fonctions inline, 154–156
locale, 96 statique, 95 liaison tardive, 168 fonctions
membres, voir meth
multidimensionnel, 103–106 ods name mangling, 151–153
paramètres, 105 –106
bidimensionnel, 103–104 polymorphisme, 166–171
assembleur, 11 références, 153–154 liaison
langage assembleur, 11–12 typesafe, 152 virtual, 166
vtable, 167–171
binaire, addition
1–2, opérations
2 bits
APPEL, 69–70
ET, 50
convention d'appel, 65, 70–76, 83– 84 cdecl,
assemblage, 52–53
84
C, 5657
stdcall, 84
PAS, 51
OU, 50
C, 21, 71, 80–84
décalages, 47–
étiquettes,
50 décalages arithmétiques,
82 paramètres, 82
48 décalages logiques,
registres, 81
47–48 rotations, 49
valeurs de retour, 83
XOR, 51
Pascal,
prédiction de branche, 53
segment bss, 21 registre 71,
BSWAP, 59 ans appel standard 84,
OCTET, 16 appel standard 84, 72, 84, 171
octets, 4 CBW, 31 ans
CDQ, 31
Chauffeur C, 19 ans CTC, 37
181
Machine Translated by Google
182 INDICE
CLD, 106 DIV, 34, 48
horloge, 5 faire en boucle, 43
PCM, 37–38 DWORD, 16
CMPSB, 109
CMPSD, 109 endianess, 24–25, 57–60
CMPSW, invert endian, 59
segment de code 109, 21
FABS, 130
COM, 171
FADD, 126
commentaire,
FADDP, 126
12 compilateur, 5, 11
FCHS, 130
Borland, 22, 23 ans
FCOM, 129
DJGPP, 22, 23
FCOMI, 130
gcc, 22
attribut FCOMIP, 130, 140
, 84, 145, 148,
149 FCOMP, 129
FCOMPP, 129
Microsoft, pack
FDIV, 128
de 22 pragmas, 146, 148, 149
FDIVP, 128
Watcom, 83
FDIVR, 128
branche conditionnelle, 39–41
FDIVRP, 128
bits de comptage, 60–64
GRATUIT, 126
méthode un, 60–61
FIADD, 126
méthode trois, 62–64
FICOM, 129
méthode deux, 61–62
FICOMP, 129
Unité centrale,
FIDIV, 128
5–7 80x86, 6
FIDIVR, 128
CWD, 31 ans
FILD, 125
CDE, 31 ans
POING, 126
segment de données, FISUB, 127
21 débogage, 16–18 FISUBR, 127
DEC, FDL, 125
13 décimal, FLD1, 125
1 directif, 13–15 % FLDCW, 126
de définition, 14 FLDZ, 125
DX, 14, 95 virgule flottante, 117–139
données, 14–15 arithmétique, 122–124
JJ, 15 représentation, 117–122
DQ, 15 dénormalisée, 121
equ, 13 double précision, 122
externe, 77 cachée, 120
global, 21, 78, 80 IEEE, 119–122
RESX, 14, 95 simple précision, 120–121
FOIS, 15, 95 coprocesseur à virgule flottante, 124–139
Machine Translated by Google
INDICE 183
addition et soustraction, 126– 127 adressage indirect, 65–66 tableaux,
98–102 nombre
comparaisons, 128–130 entier, 27–38
matériel, 124–125 comparaisons, 37–38
chargement et stockage de données, 125– division, 34
126 précision étendue, 36–37
multiplication et division, 128. multiplication, 33–34
FMUL, 128 représentation, 27–33
FMULP, 128 complément à un, 28 magnitude
FSCALE, 130, 141 signée, 27 complément à
FSQRT, 130 deux, 28–30 bit de signe, 27,
TSF, 126 extension à 30
FSTCW, 126 signes, 30–33 signé, 27–30,
FSTP, 126 38 non signé, 27, 38
FSTSW, 129 interfaçage avec C, 80–
FSUB, 127 89 interruption, 10
FSUBP, 127
FSUBR, 127
JC, 39 ans
FSUBRP, 127
J. E., 41 ans
FTST, 129
JG, 41 ans
FXCH, 126
JGE, 41 ans
gaz, 157 JL, 41 ans
JLE, 41 ans
hexadécimal, 3–4 JMP, 38–39
JNC, 39 ans
I/O, 16–18 JNE, 41 ans
bibliothèque asm io, 16–18 JNG, 41 ans
dump math, 18 dump JNGE, 41 ans
mem, 17 dump regs, JNL, 41 ans
17 dump stack, 17 JNLE, 41 ans
print char, 17 print int, JNO, 39 ans
17 print nl, 17 print JNP, 39 ans
string, 17 read JNS, 39 ans
char, 17 lire JNZ, 39 ans
entier, 17 JO, 39 ans
JP, 39 ans
JS, 39 ans
IDIV, 34 si JZ, 39 ans
déclaration, 42
immédiat, 12 étiquette, 14–16
IMUL, 33–34 LAHF, 129
Inc, 13 LÉA, 83, 103
Machine Translated by Google
184 INDICE
lien, 23 fichier 32 bits, 10
de liste, 23–24 localité,
143 quad.asm, 130–133
LODSB, 107 QWORD, 16
LODDS, 107
RCL, 49
LODSW, 107
RCR, 49
BOUCLE, 41
read.asm, 133–135
BOUCLE, 41
mode réel, 8–9
LOOPNE, 41
récursivité, 89–90
LOOPNZ, 41 ans
LOOPZ, 41 ans registre, 5, 7–8 32
bits, 8
langage machine, 5, 11 MASM, pointeur de base, 7, 8
12 math.asm, EDI, 107
35–36 mémoire, 4–5 EDX:EAX, 31 , 34, 37, 83 EFLAGS,
pages, 10 8 EIP, 8 ESI,
segments, 107
9, 10 virtuel, 9, 10 FLAGS, 7,
mémoire.asm, 37–38 CF, 38 DF, 106
111–115 mémoire:segments, OF, 38
9 méthodes, 156 PF, 39 SF,
mnémonique , 11 38 ZF,
MOV, 12 MOVSB, 38 index,
108 MOVSD, 7 IP, 7
108 MOVSW,
108 MOVSX, 31 segments,
MOVZX, 31 MUL, 7, 8,
33–34, 48, 103 107 pointeur de pile, 7,
programmes 8 REP, 108–109 REPE,
multimodules, 77–80 110 REPNE, 110
REPNZ, voir
REPNE REPZ,
MSNA, 12
voir REPE RET, 69–70, 72
NEG, 34, 55
ROL, 49 ROR, 49
grignoter, 4
PAS, 52
code opération, 11
OU, 51
SAHF, 129
prime.asm, 43–45 Sal, 48 ans
prime2.asm, 135–139 mode RSA, 48
protégé 16 bits, 9 CFF, 37 ans
SCASB, 109
Machine Translated by Google
INDICE 185
SCASD, 109 MOT, 16 mot,
SCASW, 109 8
SCSI, 147–149
SETxx, 54 XCHG, 60 ans
OU exclusif, 51
SETG, 55
SHL, 47 ans
SHR, 47
fichier squelette,
25 exécution spéculative, 53
pile, 68–76
variables locales, 75–76, 82–83
paramètres, 70–73
code de démarrage, 23
STD, 106
types de
stockage
automatique,
91 global, 91
registre, 93
statique, 91 volatile, 93
CSTOSB, 107
STOSD, 107
instructions de chaîne, 106–115
structures, 143–150
alignement, 145–146
champs de bits, 146–
150 offsetof(), 144
SUB, 13, 36
sousprogramme, 66–
93 appelant, 69–
76 réentrant, 89
sousprogramme, voir sousprogramme
TASM, 12
TCP/IP, 59
TEST, 51
segments de texte, voir complément à
deux du segment de code, 28–
30 arithmétique, 33–37
DEUXIÈME, 16
UNICODE, 59
boucle while, 43