Vous êtes sur la page 1sur 195

Machine Translated by Google

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  lui­mê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

1.5  Fichier  squelette . . . . . . . . . . . . . . . . . . . . . . . . . . .  25

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  Sous­programmes   65
4.1  Adressage  indirect . . . . . . . . . . . . . . . . . . . . . . . .  65
4.2  Exemple  de  sous­programme  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  multi­modules . . . . . . . . . . . . . . . . . . . . .  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  Sous­programmes  réentrants  et  récursifs . . . . . . . . . . . . .  89
4.8.1  Sous­programmes  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é  ci­dessus,  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  lui­mê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  sous­jacent  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 :  pacman128@gmail.com  
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  (0­9).  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  0­9  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  kilo­octets  ( 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 =

1 073 741 824 octets). Adresse  0  1  5  6  7 2 3  4


Mémoire  2A  45  B8  20  8F  CD  12  2E

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  lui­mê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  
celui­ci).  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.  Ceux­ci  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  ci­dessous),  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  passe­t­il  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 :  sont­elles  actuellement  en  mémoire ;  si  en  mémoire,  où  
est­il ;  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,  CD­ROM  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  ci­dessus  
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  elle­mê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  elle­même.
Ils  sont  stockés  dans  l'instruction  elle­mê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  sous­entendu.

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  points­virgules  commencent  un  commentaire) :
Machine Translated by Google

1.3.  LANGAGE  D'ASSEMBLAGE 13

mouvement eax,  3   ;  stocker  3  dans  le  registre  EAX  (3  est  l'opérande  immédiat)


mouvement
bx,  hache ;  stocker  la  valeur  de  AX  dans  le  registre  BX

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.

inc. exx ;  ECX++


déc dl ;  dl­­

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  ci­dessus  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.  Ci­dessous  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:

1 mouvement al,  [L1]  eax,   ;  copier  l'octet  à  L1  dans  AL


2 mouvement L1 ;  EAX  =  adresse  de  l'octet  à  L1
3 mouvement [L1],  ah  eax,   ;  copier  AH  dans  l'octet  à  L1
4 mouvement [L6]  eax,  [L6] ;  copier  le  mot  double  en  L6  dans  EAX
5 ajouter ;  EAX  =  EAX  +  mot  double  en  L6
6 ajouter [L6],  eax  al,   ;  mot  double  sur  L6  +=  EAX
7 mouvement [L6] ;  copier  le  premier  octet  du  mot  double  à  L6  dans  AL

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 :

mouvement [L6],  1 ;  stocker  un  1  en  L6

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 :

mouvement dmot  [L6],  1 ;  stocker  un  1  en  L6

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  ci­dessous  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  sous­programme.
Les  macros  sont  définies  dans  le  fichier  asm  io.inc  décrit  ci­dessus.  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  au­dessus  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  plates­formes.  
En  fait,  il  est  rare  d'utiliser  l'assemblage  du  tout.
Alors,  pourquoi  quelqu'un  devrait­il  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  ceux­ci  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

45 appel read_int   ;  lire  un  entier


46 mouvement
[entrée1],  eax ;  stocker  dans  input1
47

48 mouvement
eax,  invite2  print_string ;  imprimer  l'invite
49 appel
50

51 appel read_int   ;  lire  un  entier


52 mouvement
[entrée2],  eax ;  stocker  dans  input2
53

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

72 appel print_string  eax,   ;  imprimer  le  troisième  message


73 mouvement ebx
74 appel print_int   ;  imprimer  la  somme  (ebx)
75 appel print_nl ;  imprimer  une  nouvelle  ligne
76

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  ci­dessus  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  ci­dessus).  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  format­objet  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ù  object­format  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  ci­dessous,  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  ci­dessus,  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  listing­file  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

48  00000000  456E7465722061206E­ prompt1  db "Entrez  un  nombre :  ",  0


49  00000009  756D6265723A2000
50  00000011  456E74657220616E6F­ prompt2  db "Entrez  un  autre  numéro :  ",  0
51  0000001A  74686572206E756D62­
52  00000023  65723A2000

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'op­code  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  serait­il  représenté  en  un  octet  dans  la  mémoire  de  l'ordinateur.  
Comment  le  signe  moins  serait­il  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  ci­dessus.

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 :

mouvement hache,  0034h   ;  ax  =  52  (stocké  sur  16  bits) ;  cl  =  8  bits  inférieurs  


mouvement cl,  al de  ax

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  ci­dessus  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.  (Rappelez­vous  
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  renvoie­t­elle  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  faut­il  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  lui­mê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

26 imul eax ;  edx:eax  =  eax  *  eax


27 mouvement ebx,  eax ;  enregistrer  la  réponse  dans  ebx
28 mouvement
eax,  square_msg
29 appel print_string
30 mouvement eax,  ebx
31 appel print_int
32 appel print_nl
33

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

42 imul ecx,  ebx,  25eax,   ;  ecx  =  ebx*25


43 mouvement
cube25_msg
44 appel print_string
45 mouvement eax,  ecx
46 appel print_int
47 appel print_nl
48

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

65 négatif edx ;  nier  le  reste


66 mouvement
eax,  neg_msg
67 appel print_string
68 mouvement eax,  edx
69 appel print_int
70 appel print_nl
71

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é  ci­dessus,  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  sont­ils  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 :

1 ajouter eax,  ecx ;  ajouter  32  bits  inférieurs


2 adc edx,  ebx ;  ajouter  les  32  bits  supérieurs  et  reporter  de  la  somme  précédente

La  soustraction  est  très  similaire.  Le  code  suivant  soustrait  EBX:ECX  de
EDX : EAX :

1 sous eax,  ecx   ;  soustraire  les  32  bits  inférieurs


2 cff edx,  ebx ;  soustraire  les  32 bits  supérieurs  et  emprunter

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  mot­clé  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  mot­clé  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  pseudo­code  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 :

1 cmp   eax,  0   ;  définir  des  drapeaux  (ZF  défini  si  eax  ­  0  =  0)


2 jz puis  bloquer ;  si  ZF  est  défini  branche  sur  thenblock
3 mouvement ebx,  2 ;  ELSE  fait  partie  de  SI
4 jmp suivant ;  sauter  par­dessus  ALORS  une  partie  de  SI
5  puis  bloquer :
6 mouvement ebx,  1 ;  ALORS  une  partie  de  SI
7  suivant :

D'autres  comparaisons  ne  sont  pas  si  faciles  en  utilisant  les  branches  conditionnelles  dans
Tableau  2.3.  Pour  illustrer,  considérons  le  pseudo­code  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  ci­dessus  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  pseudo­code  ci­dessus  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
pseudo­code  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 :

1 mouvement eax,  0   ;  eax  est  la  somme


2 mouvement ecx,  10 ;  ecx  c'est  moi
3  loop_start :
4 ajouter eax,  ecx
5 boucle  loop_start

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  pseudo­code  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  eux­mê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

20 mouvement eax,  2   ;  printf("2\n");


21 appel print_int
22 appel print_nl
23 mouvement eax,  3   ;  printf("3\n");
24 appel print_int
25 appel print_nl
26

27 mouvement dword  [Deviner],  5 ;  Devinez  =  5 ;


28  while_limit : ;  tandis  que  ( Supposition  <=  Limite )
29 mouvement eax,  [devinez]
30 cmp   eax,  [Limite]
31 jnbe end_while_limit ;  utiliser  jnbe  car  les  nombres  ne  sont  pas  signés
32

33 mouvement ebx,  3 ;  ebx  est  le  facteur  =  3 ;


34  while_factor :
35 mouvement eax,  ebx
36 mul eax ;  edx:eax  =  eax*eax
37 Jo end_while_factor  eax,   ;  si  la  réponse  ne  rentre  pas  dans  eax  seul
38 cmp   [Conjecture]
39 jnb end_while_factor  eax, ;  si !(facteur*facteur  <  deviner)
40 mouvement [Conjecture]
41 mouvement edx,0
42 div ebx ;  edx  =  edx:eax  %  ebx
43 cmp   edx,  0
44 je end_while_factor ;  si !(deviner  %  facteur !=  0)
45

46 ajouter ebx,2   ;  facteur  +=  2 ;


47 jmp   while_factor
48  end_while_factor :
49 je end_if  eax, ;  si !(deviner  %  facteur !=  0)
50 mouvement [Devinez]  print_int ;  printf("%u\n")
51 appel

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

6 shl hache,   ;  décalage  de  2  bits  vers  la  gauche,  ax  =  048CH,  CF  =  1


7 mouvement 2  cl,  3
8 shr hache,  cl ;  décalage  de  3  bits  vers  la  droite,  ax  =  0091H,  CF  =  1

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.

1 mouvement bl,  0   ;  bl  contiendra  le  nombre  de  bits  ON


2 mouvement exx,  32 ;  ecx  est  le  compteur  de  boucle
3  count_loop :
4 shl eax,  1   ;  décaler  le  bit  dans  le  drapeau  de  report
5 Jnc skip_inc ;  si  CF  ==  0,  aller  à  skip_inc
6 inc. bl
7  sauter_inc :
8 boucle  count_loop
Machine Translated by Google

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  ci­dessus  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.
Ci­dessous  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.  Ci­dessous  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.  Ci­dessous  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.  Ci­dessous  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  ci­dessous  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.

1 mouvement eax,  100   ;  100  =  64H


2 mouvement ebx,  0000000FH ;  masque  =  16  ­  1  =  15  ou  F
3 et ebx,  eax ;  ebx  =  reste  =  4

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

1 mouvement bl,  0   ;  bl  contiendra  le  nombre  de  bits  ON


2 mouvement exx,  32 ;  ecx  est  le  compteur  de  boucle
3  count_loop :
4 shl eax,  1   ;  décaler  le  bit  dans  le  drapeau  de  report
5 adc bl,  0   ;  ajoutez  juste  le  drapeau  de  portage  à  bl
6 boucle  count_loop

Figure  3.3 :  Comptage  de  bits  avec  ADC

1 mouvement cl,  bh   ;  construisez  d'abord  le  nombre  en  OU  avec


2 mouvement ebx,  1
3 shl ebx,  cl   ;  décalage  à  gauche  cl  fois
4 ou eax,  ebx ;  allumer  un  peu

Éteindre  un  peu  est  juste  un  peu  plus  difficile.

1 mouvement cl,  bh   ;  construisez  d'abord  le  nombre  en  ET  avec


2 mouvement ebx,  1
3 shl ebx,  cl  ebx ;  décalage  à  gauche  cl  fois
4 pas ;  inverser  les  bits
5 et eax,  ebx ;  éteindre  peu

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:

xor eax,  eax ;  eax  =  0

Un  nombre  XOR  avec  lui­mê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
ci­dessous  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

28 xor ebx,  ebx  eax,   ;  ebx  =  0


29 cmp   [entrée1]  bl ;  comparer  le  deuxième  et  le  premier  nombre
30 setg   ;  ebx  =  (entrée2  >  entrée1) ?  dix
31 neg ebx ;  ebx  =  (entrée2  >  entrée1) ?  0xFFFFFFFF :  0
32 mouvement ecx,  ebx ;  ecx  =  (entrée2  >  entrée1) ?  0xFFFFFFFF :  0
33 et ecx,  eax  ebx ;  ecx  =  (entrée2  >  entrée1) ?  entrée2 :  0
34 pas ;  ebx  =  (entrée2  >  entrée1) ?  0 :  0xFFFFFFFF
35 et ebx,  [entrée1]  ecx,   ;  ebx  =  (entrée2  >  entrée1) ? ;  ecx  =  (entrée2   0 :  entrée1
36 ou ebx >  entrée1) ? entrée2 :  entrée1
37

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  ci­dessus,  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é.  Ci­dessous  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  ci­dessous  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.

1  fichier  stat  struct  stats ; /   structure  utilisée  par  stat()   /  2  stat  ("foo",  &  file  


stats ); /   lire  les  informations  sur  le  fichier .  fichier  stats .st  mode  
3 contient  des  bits  d'autorisation   /
4  chmod(”foo”,  (statistiques  du  fichier .st  mode  &  ̃S  IWOTH)  |  S  IRUSR);

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  multi­octets  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

mot  court  non  signé  =  0x1234 ; /   suppose  que  sizeof  ( short)  ==  2   /  unsigned  char  


  p  =  (unsigned  char   )  &word;

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  étaient­ils  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  celui­ci  (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  multi­octets,  comme
Tous  les  en­tê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:

xchg ah,  al ;  échanger  les  octets  de  ax

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  marche­t­il?  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 ;

dix tandis  que( data !=  0 )  { /   méthode  un   /


11 données  =  données  &  (données  −  1);
12 cnt++;
13 }
14 nombre  de  bits  d'octets  [i]  =  cnt ;
15 }
16 }
17

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  stockez­le  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'est­ce  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  ci­dessus  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  ci­dessus  (rappelez­vous  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  ci­dessus  (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

Sous­programmes

Ce  chapitre  examine  l'utilisation  de  sous­programmes  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  sous­programmes.
Le  code  qui  appelle  un  sous­programme  et  le  sous­programme  lui­mê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
sous­programmes  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  sous­programme  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:

1 mouvement ax,  [Données]   ;  adressage  mémoire  direct  normal  d'un  mot


2 mouvement ebx,  Données   ;  ebx  =  &  Données
3 mouvement ax,  [ebx] ;  hache  =  *ebx

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.  SOUS­PROGRAMMES

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  sous­programme  simple
Un  sous­programme  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  sous­programme  est  comme  une  fonction
en  C.  Un  saut  peut  être  utilisé  pour  invoquer  le  sous­programme,  mais  le  retour  de  cadeaux
un  problème.  Si  le  sous­programme  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  sous­programme  ne  peut  pas  être  codé  en  dur  sur  une  étiquette.  Le  code  ci­dessous
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  sous­programme.

sous1.asm
1 ;  fichier :  sub1.asm
2 ;  Exemple  de  programme  de  sous­programme
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  ceux­ci  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  SOUS­PROGRAMME  SIMPLE 67

26 mouvement ecx,  ret1  court   ;  stocker  l'adresse  de  retour  dans  ecx


27 jmp get_int ;  lire  un  entier
28  ret1 :

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

51 appel print_int  print_nl ;  imprimer  la  somme  (ebx)


52 appel ;  imprimer  une  nouvelle  ligne
53

54 papa
55 mouvement eax,  0 ;  revenir  à  C
56 partir

57 ret

58 ;  sous­programme  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.  SOUS­PROGRAMMES

Le  sous­programme  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  sous­programme.  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  sous­programmes.  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  Last­In
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  ci­dessous
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  sous­programmes,  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  sous­programmes  d'appel  
rapides  et  faciles.  L'instruction  CALL  fait  un  saut  inconditionnel  vers  un  sous­programme  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  sous­programme  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  sous­programmes.  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.  SOUS­PROGRAMMES

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  sous­programme  est  appelé,  le  code  appelant  et  le  sous­programme  (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  sous­programme  réentrant  peut  être  appelé  en  tout  point  d'un  
programme  en  toute  sécurité  (même  à  l'intérieur  du  sous­programme  lui­même).

4.5.1  Passer  des  paramètres  sur  la  pile
Les  paramètres  d'un  sous­programme  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  
sous­programme,  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  sous­programme,  mais  ils  sont  
accessibles  à  partir  de  la  pile  elle­mê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  sous­programme  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  sous­programme  ESP

Figure  4.2 :

sur  la  pile  conserve  une  copie  des  données  en  mémoire  accessible
à  tout  moment  du  sous­programme.

Considérez  un  sous­programme  qui  reçoit  un  seul  paramètre  sur  la  pile.  Lors  de  l'utilisation  d'un  habillage  indirect,  le  
Lorsque  le  sous­programme  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  sous­programme  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  sous­programme  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  sous­programme,  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  sous­programme.)  Figure  4.3 même.

montre  la  forme  générale  d'un  sous­programme  qui  suit  ces  conventions.
Les  lignes  2  et  3  de  la  figure  4.3  constituent  le  prologue  général  d'un  sous­programme.
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  sous­programme  sans  se  soucier  de  ce
else  a  été  poussé  sur  la  pile  par  le  sous­programme.
Une  fois  le  sous­programme  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  sous­programme  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.  SOUS­PROGRAMMES

1  sous­programme_étiquette :
2 pousser  ebp   ;  enregistrer  la  valeur  EBP  d'origine  sur  la  pile
3 mouvement
ebp,  esp ;   ;  nouveau  EBP  =  ESP
4 code  de  sous­programme
5 pop ebp   ;  restaurer  la  valeur  EBP  d'origine
6 ret

Figure  4.3 :  Formulaire  de  sous­programme  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  sous­programme  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  sous­programmes  qui
utilisez  les  conventions  d'appel  C  décrites  ci­dessus.  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  sous­programme

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  sous­programme  utilise  à  proximité  du  code  du
sous­programme.

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  pseudo­code
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

24 mouvement edx,  1 ;  edx  est  'i'  en  pseudo­code


25  boucle_while :
26 pousser  edx   ;  enregistrez­moi  sur  la  pile
27 pousser  dword  entrée  appeler   ;  pousser  l'adresse  sur  l'entrée  sur  la  pile
28 get_int
29 ajouter esp,  8 ;  supprimer  i  et  &input  de  la  pile
Machine Translated by Google

74 CHAPITRE  4.  SOUS­PROGRAMMES

30

31 mouvement
eax,  [entrée]
32 cmp   eax,  0
33 je end_while
34

35 ajouter [somme],  eax ;  somme  +=  entrée


36

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 ;  sous­programme  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 ;  sous­programme  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  sous­programmes  soient  réentrants.
Un  sous­programme  réentrant  fonctionnera  s'il  est  appelé  à  n'importe  quel  endroit,  y  compris  le
sous­programme  lui­même.  En  d'autres  termes,  les  sous­programmes  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  sous­programme  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.  SOUS­PROGRAMMES

1  sous­programme_é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  sous­programme
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  sous­programme  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  sous­programme.  La  figure  4.6  montre  le  nouveau  sous­programme
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  sous­programme  é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  sous­programme  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  MULTI­MODULES 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   ;  est­ce  que  je  <=  n ?
dix end_for
11

12 ajouter [ebp­4],  ebx  ebx ;  somme  +=  je


13 inc.
14 jmp court  for_loop
15

16  fin_pour :
17 mouvement
ebx,  [ebp+12]  eax,   ;  ebx  =  puisard
18 mouvement
[ebp­4]  [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  multi­modules
Un  programme  multi­module  est  un  programme  composé  de  plus  d'un  fichier  objet.
Tous  les  programmes  présentés  ici  sont  des  programmes  multi­modules.  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).  Rappelez­vous  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.  SOUS­PROGRAMMES

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  sous­programme_étiquette :
2 entrez  LOCAL_BYTES,  0 ;  code   ;  =  #  octets  nécessaires  aux  locaux
3 de  sous­programme
4 partir
5 ret

Figure  4.10 :  Formulaire  général  de  sous­programme  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  sous­programmes  (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  MULTI­MODULES 79

15

16 mouvement edx,  1 ;  edx  est  'i'  en  pseudo­code


17  boucle_while :
18 pousser  edx   ;  enregistrez­moi  sur  la  pile
19 pousser  dword  entrée  appeler   ;  pousser  l'adresse  sur  l'entrée  sur  la  pile
20 get_int
21 ajouter esp,  8 ;  supprimer  i  et  &input  de  la  pile
22

23 mouvement
eax,  [entrée]
24 cmp   eax,  0
25 je end_while
26

27 ajouter [somme],  eax ;  somme  +=  entrée


28

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.  SOUS­PROGRAMMES

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  sous­programmes  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  sous­programme  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  sous­programme  conserve  les  valeurs  des  éléments  suivants  Le  mot­clé  register  peut
être  utilisé  dans  une  déclaration  
registres :  EBX,  ESI,  EDI,  EBP,  CS,  DS,  SS,  ES.  Cela  ne  veut  pas  dire  que
le  sous­programme  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  sous­programme  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 Celles­ci  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.  SOUS­PROGRAMMES

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'en­tê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  sous­programmes.  Considérons  
le  cas  de  la  transmission  de  l'adresse  d'une  variable  (appelons­la  x)  à  une  fonction
Machine Translated by Google

4.7.  ENSEMBLE  D'INTERFACE  AVEC  C 83

(appelons­le  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  ci­dessus  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.  SOUS­PROGRAMMES

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  ci­dessus  
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  sous­programme  
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  ci­dessus  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  sous­programme.  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 ;  sous­programme  _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 ;  pseudo­code  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.  SOUS­PROGRAMMES

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  à  [ebp­4]
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  [ebp­4],0 ;  somme  =  0
25 dump_stack  1,  2,  4 ;  imprimer  la  pile  de  ebp­8  à  ebp+16
26 mouvement exx,  1 ;  ecx  est­ce  que  je  suis  en  pseudo­code
27  for_loop :
28 cmp   ecx,  [ebp+8]   ;  cmp  je  et  n
29 jnle end_for ;  sinon  i  <=  n,  quitter
30

31 ajouter [ebp­4],  exx ;  somme  +=  je


32 inc. exx
33 jmp court  for_loop
34

35  fin_pour :
36 mouvement
ebx,  [ebp+12]  eax,   ;  ebx  =  puisard
37 mouvement
[ebp­4]  [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  est­elle  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  au­dessus  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é.  Ci­dessous  le  code  
d'assemblage  modifié :

sous6.asm
1 ;  sous­programme  _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 ;  pseudo­code  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  à  [ebp­4]
20  _calc_sum :
21 entrez  4,0 ;  faire  de  la  place  pour  la  somme  sur  la  pile
Machine Translated by Google

88 CHAPITRE  4.  SOUS­PROGRAMMES

1  segment .données
2  formats db  "%d",  0
3

4  segments .text
5 ...
6 léa eax,  [ebp­16]
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  [ebp­4],0 ;  somme  =  0
24 mouvement ex,  1 ;  ecx  est­ce  que  je  suis  en  pseudo­code
25  for_loop :
26 cmp   ecx,  [ebp+8]   ;  cmp  je  et  n
27 jnle end_for ;  sinon  i  <=  n,  quitter
28

29 ajouter [ebp­4],  exx ;  somme  +=  je


30 inc. exx
31 jmp court  for_loop
32

33  fin_pour :
34 mouvement
eax,  [ebp­4] ;  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.  SOUS­PROGRAMMES  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  Sous­programmes  réentrants  et  récursifs
Un  sous­programme  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  ci­dessus  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  ci­dessous).

•  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  sous­programme  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  sous­programmes  réentrants  fonctionnent  beaucoup  mieux  en  multi­thread 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  Sous­programmes  récursifs
Ces  types  de  sous­programmes  s'appellent  eux­mêmes.  La  récursivité  peut  être  directe  ou  
indirecte.  La  récursivité  directe  se  produit  lorsqu'un  sous­programme,  disons  foo,  s'appelle  lui­même  
à  l'intérieur  du  corps  de  foo.  La  récursivité  indirecte  se  produit  lorsqu'un  sous­programme  n'est  pas  
appelé  directement  par  lui­même,  mais  par  un  autre  sous­programme  qu'il  appelle.  Par  exemple,  le  
sous­programme  foo  pourrait  appeler  bar  et  bar  pourrait  appeler  foo.

5Un  programme  multithread  a  plusieurs  threads  d'exécution.  c'est­à­dire  le  programme
elle­même  est  multitâche.
Machine Translated by Google

90 CHAPITRE  4.  SOUS­PROGRAMMES

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(n­1)
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  sous­programmes  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.  SOUS­PROGRAMMES  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  mot­clé  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.  SOUS­PROGRAMMES

1  %define  i  ebp­4
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,   ;  est­ce  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

26 inc. dword  [i]   ;  je++


27 jmp court  lp
28  quitter :
29 partir

30 ret

Figure  4.18 :  Autre  exemple  (version  assemblage)
Machine Translated by Google

4.8.  SOUS­PROGRAMMES  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  multi­thread.  
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.  SOUS­PROGRAMMES
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 :

tableau1   db 5,  4,  3,  2,  1  5,  4,   ;  tableau  d'octets


tableau2 dw 3,  2,  1 ;  tableau  de  mots

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
EBP­109 carboniser

EBP­112 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é  ci­dessous)  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

31 pousser   dword  FirstMsg  _puts ;  imprimer  FirstMsg


32 l'appel

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

46 léa eax,  [ebp­4] ;  eax  =  adresse  du  dword  local


47 pousser   eax
48 pousser   format  d'entrée  dword
49 appeler _scanf
50 ajouter esp,  8
51 cmp   eax,  1 ;  eax  =  valeur  de  retour  de  scanf
52 je EntréeOK
53

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,  [ebp­4]
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  20­29
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

100 xor esi,  esi  ecx,   ;  esi  =  0


101 mouvement
[ebp+12]  ebx,   ;  cex  =  n
102 mouvement
[ebp+8] ;  ebx  =  adresse  du  tableau
103  print_loop :
104 pousser exx ;  printf  peut  changer  ecx !
105
Machine Translated by Google

102 CHAPITRE  5.  TABLEAUX

106 pousser   dword  [ebx  +  4*esi] ;  tableau  push[esi]


107 pousser   esi
108 pousser   format  de  sortie  dword
109 appeler _printf
110 ajouter esp,  12 ;  supprimer  les  paramètres  (laisser  ecx !)
111

112 inc. esi


113 exx
114 boucle  pop print_loop
115

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étermine­t­il  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  ci­dessus) :

x  =  a[i ][ j ] ;

La  figure  5.6  montre  l'assemblage  dans  lequel  ceci  est  traduit.  Ainsi,  le  compilateur
convertit  essentiellement  le  code  en :

x  =   (&a[0][0]  +  2 i  +  j );

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  ci­dessous  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  était­elle  
révélatrice ?)
ou  en  notation  ̈uber  math  geek :

n j­1
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  pseudo­code  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  ci­dessus.  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  peut­on  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

23 mouvement esi,  src  edi,   ;  esi  =  adresse  du  tampon  à  partir  duquel  copier


24 mouvement destination ;  edi  =  adresse  du  tampon  vers  lequel  copier
25 mouvement ex,  sz ;  ecx  =  nombre  d'octets  à  copier
26

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

91 mouvement edi,  src  ecx,   ;  edi  =  pointeur  vers  la  chaîne


92 mouvement
0FFFFFFFFh ;  utiliser  le  plus  grand  ECX  possible
93 xor al,  al ;  al  =  0
94 CLD
95

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

105 pop   édi


106 congé
107 ret
108

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

123 mouvement edi,  destination


124 mouvement esi,  src
125 CLD
126  copie_boucle :
127 lodsb ;  charge  AL  &  inc  si
128 stosb ;  magasin  AL  &  inc  di
129 ou al,  al   ;  définir  des  indicateurs  de  condition
130 jnz cpy_loop ;  s'il  n'a  pas  dépassé  le  0  de  fin,  continuer
131

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

6  void  asm  copy( void   ,  const  void   ,  unsigned )  attribut  ((cdecl));


7  vide     asm  trouver( const  vide   ,
8 char  target ,  unsigned )  attribut  ((cdecl));
Machine Translated by Google

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

19  copie  asm(st2,  st1 , TAILLE  STR ); /   copie  les  30  caractères  de  la  chaîne   /


20 printf(”%s\n”,  st2);
21

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

35  asm  strcpy( st2 , st1 );   /   copie  les  données  significatives  dans  la  chaîne   /


36 printf(”%s\n”,  st2 );
37

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 :

−1 +  0  ×  2 −2 +  1  ×  2 −3 =  0,625


0,1012  =  1  ×  2

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  ci­dessous.
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  est­il  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  ci­dessus  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  lui­mê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  lui­mê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  serait­il  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  (rappelez­vous  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 !  ci­dessus  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  ci­dessus.  É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  serait­il  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  passe­t­il  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  lui­mê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  Last­In  First­Out  (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  ceux­ci  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  elle­mê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  (non­flottant
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.
FIMUL­src 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  au­dessus  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.
enregistrez­vous  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  sous­programme  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  elle­mê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  sous­programme  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  ci­dessus)
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  [ebp­8]
Machine Translated by Google

132 CHAPITRE  6.  POINT  FLOTTANT

20 %  définissent  one_over_2a qword  [ebp­16]
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

58 fld   qword  [ebx]  b ;  stocker  dans  *  root1


59 fstp ;  pile :  b
60 fld disque ;  pile :  disque,  b
61 fchs ;  pile :  ­disque,  b
Machine Translated by Google

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

65 fstp qword  [ebx]  eax,   ;  stocker  dans  *  root2


66 mouvement 1  court   ;  la  valeur  de  retour  est  1
67 jmp quit
68

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

32 pousser esi ;  sauver  esi


33 mouvement esi,  ARRAYP   ;  esi  =  TABLEAUP
34 xor edx,  edx ;  edx  =  index  de  tableau  (initialement  0)
35

36  boucle_while :
37 cmp   edx,  ARRAY_SIZE   ;  est­ce  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  enregistrez­le
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  a­t­il  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

69 mouvement eax,  edx ;  stocker  la  valeur  de  retour  dans  eax


70

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  restaurez­le  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  souhaitez­vous  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

48 mouvement ex,  1 ;  ecx  est  utilisé  comme  index  de  tableau


49 pousser   ebx ;  stocker  deviner  sur  la  pile
50 le  champ dword  [esp] ;  charger  la  supposition  sur  la  pile  du  coprocesseur
51 pop   ebx ;  obtenir  deviner  hors  pile
52 fsqrt   ;  trouver  sqrt  (deviner)
53 poing  dword  [isqrt] ;  isqrt  =  étage(sqrt(quess))
54 ;
55 ;  Cette  boucle  interne  divise  l'estimation  (ebx)  par  les  nombres  premiers  calculés  précédemment
56 ;  jusqu'à  ce  qu'il  trouve  un  facteur  premier  de  conjecture  (ce  qui  signifie  que  la  conjecture  n'est  pas  premier)
57 ;  ou  jusqu'à  ce  que  le  nombre  premier  à  diviser  soit  supérieur  à  floor(sqrt(guess))
58 ;
59  while_factor :
60 mouvement eax,  dword  [esi  +  4*ecx]  eax,  [isqrt]   ;  eax  =  tableau[ecx]
61 cmp   court   ;  tandis  que  ( isqrt  <  tableau[ecx]
62 jnbe quit_factor_prime
63 mouvement eax,  ebx
64 xor edx,  edx
65 div dword  [esi  +  4*ecx]
66 ou edx,  edx   ;  &&  devine  %  array[ecx] !=  0 )
67 jz   court  quit_factor_not_prime
68 inc exx ;  essayez  le  prochain  premier
Machine Translated by Google

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

78 mouvement [n],  ex ;  inc  n


79

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'en­tê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  rappelez­vous  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  soi­mê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  
lui­mê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++

#pragma  pack(push) /   enregistrer  l'état  d'alignement   /  #pragma  


pack(1) /   définir  l'alignement  des  octets   /

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  ci­dessus  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'en­tête.  Si  le  fichier  d'en­tête  est  inclus  avant  d'autres  fichiers  d'en­tê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

8  bits   11  bits   10  bits   3  bits


f4 f3 f2 f1

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 :

5  bits  3  bits  3  bits  5  bits 8  bits   8  bits


f2l f1 f3l  f2m f3m f4

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é  ci­dessus,  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++

vide  f  ( int  x , int  y , double  z);  

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  ci­dessous).

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'en­tête  ANSI  C  fournis  avec  C/C++  com
aujourd'hui,  ils  trouveront  ce  qui  suit  en  haut  de  chaque  fichier  d'en­tê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  sous­scores  en  tête).  
L'extrait  ci­dessus  enferme  l'intégralité  du  fichier  d'en­tête  dans  un  bloc  "C"  externe  si  le  
fichier  d'en­tê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'en­tê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.  Rappelez­vous  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  sous­programmes,
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  mot­clé  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  ebp­8  et  y  est  à  ebp­4) :

1 pousser  dword  [ebp­8]
2 appeler  _f
3 populaire
exx
4 mouvement
[ebp­4],  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,  [ebp­8]
2 imul  eax,  eax
3 mouvement
[ebp­4],  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  non­inline
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  non­inline
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'en­tê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'en­tê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'en­tê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  est­il  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  ci­dessous  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 ;  assurez­vous  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

37 mouvement ecx,  eax ;  ecx  =  taille  de  Big_int


38 ;
39 ;  maintenant,  définissez  les  registres  pour  qu'ils  pointent  vers  leurs  tableaux  respectifs
40 ; esi  =  op1.number_
41 ; edi  =  op2.number_
42 ; ebx  =  res.number_
43 ;
44 mouvement ebx,  [ebx  +  nombre_décalage]
45 mouvement esi,  [esi  +  nombre_décalage]
46 mouvement edi,  [edi  +  nombre_décalage]
47

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  eux­mê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  <<  "Non­concordance  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  mot­clé  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  a­t­il  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  celui­ci,  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

29  vide  ( m2func  pointeur)(A   ); //  variable  de  pointeur  de  fonction


30  pointeur  m2func  =  réinterpréter  cast<void  ( )(A )>(vt[1]);
31  pointeur  m2func(pa ); //  appel  de  la  méthode  m2  via  le  pointeur  de  fonction
32 }
33

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  
celui­ci!  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

Celles­ci  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  au­dessus je

JAE Sauter  au­dessus  ou  égal  I
JB Sauter  ci­dessous 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  au­dessus je

JNAE Sauter  pas  au­dessus  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  au­dessus R/M8
SETAE Définir  au­dessus  ou  égal  à  R/M8
SETB Définir  ci­dessous 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  au­dessus R/M8
SETNAE Définir  pas  au­dessus  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  
(  ).

Instruction Description Format


FABS ST0  =  |ST0|
FADD  source ST0  +=  source STn  DF
FADD  destination,  ST0 destination  +=  STO STn
FADDP  destination  [,ST0] destination  +=  ST0 STn
FCHS ST0  =  −ST0
FCOM  src Comparer  ST0  et  src STn  DF
FCCOMP  src Comparer  ST0  et  src STn  DF
FCOMPP  src Compare  ST0  et  ST1
FCOMI   src Compare  en  DRAPEAUX STn
FCOMIP   src Compare  en  DRAPEAUX STn
FDIV  src ST0 /=  src   STn  DF
FDIV  destination,  ST0 dest /=  STO   STn
FDIVP  destination  [,ST0] dest /=  ST0 STn
FDIVR  source ST0  =  src /ST0  dest   STn  DF
Destination  FDIVR,  ST0 =  ST0/dest STn
FDIVRP  dest  [,ST0]  dest  =  ST0/dest STn
DESTINATION  GRATUITE Marque  comme  vide STn
FIADD  src ST0  +=  source I16  I32
FICOM  src Comparer  ST0  et  src I16  I32
FICOMP  source Comparer  ST0  et  src I16  I32
FIDIV  src STO /=  source I16  I32
FIDIVR  src STO  =  source /ST0 I16  I32
Machine Translated by Google

180 ANNEXE  A.  INSTRUCTIONS  80X86

Instruction Description Format


FILD  src Pousser  src  sur  la  pile I16  I32  I64
FIMUL­src ST0  *=  source I16  I32
FINIT Initialiser  le  coprocesseur
FIST  destination Stocker  ST0 I16  I32
Destination  FISTP Stocker  ST0 I16  I32  I64
FISUB  src ST0  ­=  source I16  I32
FISUBR  src ST0  =  source  ­  ST0 I16  I32
Source  FLD Pousser  src  sur  la  pile STn  FDE
FLD1 Pousser  1.0  sur  la  pile
FLDCW  src Charger  le  registre  de  mot  de  contrôle  I16
FLDPI Pousser  π  sur  la  pile
FLDZ Pousser  0.0  sur  la  pile
Source  FMUL ST0  *=  source STn  DF
Destination  FMUL,  ST0 destination  *=  STO STn
FMULP  destination  [,ST0] destination  *=  ST0 STn
FRNDINT Tour  ST0
ÉCHELLEF ST0  =  ST0  ×  2 ST1

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  =  ST0­dest STn
FSUBP  destination  [,ST0] dest  =  ST0­dest 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,  56­57
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  
sous­programme,  66–
93  appelant,  69–
76  réentrant,  89  
sous­programme,  voir  sous­programme

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

Vous aimerez peut-être aussi