l'assembleur  [livre #4602]

par  pierre maurette




Annexes

12.1 Annexe A - La numération

Introduire le binaire, donc les bases de la numération, à l'aide de quelques belles formules d'arithmétique serait rapide. Ce serait malheureusement inutile, puisque si ces notions mathématiques vous sont familières, alors vous n'avez aucun besoin d'explications sur les systèmes de numération.

D'un autre coté, le temps passé avec les bits et autres symboles hexadécimaux en programmant, spécialement en assembleur, justifie un effort de clarification. Le binaire et l'hexadécimal sont de ces notions qui, si elles restent floues, vont polluer le reste de l'apprentissage. De plus, s'il existe des calculatrices et des programmes qui se chargent des conversions, il faut bien appréhender le sujet pour éviter des erreurs grossières.

Nous avons donc choisi une approche pragmatique, dans laquelle le système décimal est tout d'abord présenté en profondeur. Ensuite seulement, l'accent sera mis sur ce qui nous intéresse, le micro-ordinateur, et ses bases binaire et hexadécimale, et sur la représentation particulière dans ces bases des nombres négatifs.

12.1.1 Décimal

Analysons notre façon habituelle de compter 243 moutons dans un enclos, par exemple. Pas de nombres négatifs pour l'instant. Et uniquement des entiers, puisque nous comptons des moutons et non des gigots ou des côtelettes.

Le mot français que nous prononçons 243 représente le nombre de moutons. Nous écrivons ce nombre à l'aide des chiffres 2, 4 et 3, c'est-à-dire des signes typographiques. Nous disposons de dix chiffres, qui constituent un sous-alphabet : 0, 1, 2, 3, 4, 5, 6, 7, 8, 9. L'ordre dans cette liste est important, ainsi que le fait qu'elle débute par 0 et progresse par incréments de 1.

Il y a dix chiffres. Pour écrire nos nombres nous utilisons donc un système décimal , ou à base 10 . Écrire 243 revient à additionner 2 centaines , 4 dizaines et 3 unités , donc à effectuer l’opération : (2 x 100) + (4 x 10) + (3 x 1).

Remarque

Puissances d'un entier

En arithmétique, nous disons qu'un nombre entier est élevé à la puissance N quand il est multiplié N-1 fois par lui-même. 5 à la puissance 3, noté 5 3 et dit cinq au cube vaut donc cinq multiplié deux fois par lui-même, donc 5 x 5 x 5, donc 125. La puissance 2, c’est-à-dire multiplier un nombre par lui-même, est nommée le carré, comme la puissance 3 est le cube . L'analogie géométrique est claire. Par convention et logiquement, N  0 vaut toujours 1 et N  1 toujours N. Tout ceci n'est que définitions. Pour info, extraire la racine carrée de l'entier P n'a rien à voir avec l'orthodontie : il s'agit de trouver N dont le carré est P, soit de résoudre l'équation en N : P = N x N.

Les quantités 1, 10, 100 sont les puissances successives de la base, chacune d'elles étant obtenue en multipliant la précédente par la base. 10 à la puissance n est donc égal à (10 x 10 x ... x 10), 10 apparaissant n fois, et se note 10 n . Donc, 10 1 vaut effectivement 10.

Pour que tout cela soit plus joli, et pour d'autres bonnes raisons qui leur sont propres, les mathématiciens ont posé que 10 0 = 1, et même que (n'importe quoi) 0  = 1. Munis de ce savoir, revenons à nos moutons. Il y en a :

(2 x 10 2 ) + (4 x 10 1 ) + (3 x 10 0 ) = 243. Les braves bêtes sont toutes là.

Dans l'écriture d'un nombre, le chiffre le plus à droite est celui des unités. Nous disons que c'est le chiffre le moins significatif .

Remarque

Notation explicite de la base utilisée

Il n'est pas nécessaire habituellement, quand la base 10 est utilisée, de le préciser explicitement. Il en sera autrement avec les autres bases. La façon générale de noter qu'un nombre est exprimé dans la base b est (nombre) b , comme dans (243) 10 . Ceci est une convention mathématique que nous appliquerons rarement en informatique : nous utiliserons le plus souvent la notation du langage utilisé pour exprimer un nombre dans une base autre que la base par défaut, généralement 10 : 4521h exprime en assembleur MASM de l'hexadécimal.

Depuis l'école maternelle, nous savons tous compter, et additionner à l'aide de tables d'addition et en faisant des retenues . Une table d'addition est un ensemble de phrases, par exemple :

2 plus 2 font 4 , ou 9 plus 7 font 16, je pose 6 et je retiens 1 .

Un beau jour, nous avons cessé de dire je sais compter jusqu'à... , en nous apercevant que nous pouvions compter jusqu'au suivant, etc.

Si nous avions disposé d'un compteur de moutons à quatre chiffres, de type kilométrique, il aurait affiché, en fin de comptage, 0243.

Avec ce compteur limité à quatre chiffres, nous ne pouvons pas compter plus de 9999 moutons. Ce qui fait, en n'omettant pas le 0000, 10000 nombres différents. Remarquons que 10000 est égal à 10 x 10 x 10 x 10, soit 10 4 ou 10 à la puissance 4. La base à la puissance du nombre de chiffres.

À chaque fois que nous ajouterons un chiffre au compteur, nous multiplions le nombre de valeurs représentables par 10, la base. Tout simplement parce que le compteur prend toutes ses anciennes valeurs pour chacune des 10 valeurs du nouveau chiffre.

À part ces cas particuliers du compteur ou de l'afficheur, le nombre maximal de valeurs représentables est une question rarement cruciale dans nos numérations ordinaires. Et nous évitons de rajouter ou de conserver des 0 en tête des nombres écrits.

Dans la vie courante, le nombre 0 n'est pas absolument nécessaire : c'est déjà de l'abstraction mathématique. Mais le chiffre 0 est indispensable : dans l'écriture d'un nombre, il garde la place. Sans lui, comment écrire 1204 moutons ?

Voyons, avant d'en terminer avec les décimaux, quelques règles arithmétiques que nous employons machinalement :

  Ajouter un 0 à la droite d'un nombre (un décalage vers la gauche avec remplissage par un 0) revient à le multiplier par 10, la base : 92 -> 920.

  Retirer un chiffre à la droite d'un nombre (un décalage vers la droite) revient à une division entière par 10, la base : 975 -> 97. Le reste de la division, 5, est perdu.

  Le chiffre le moins significatif est le reste de la division par 10, la base. Corollaire : un nombre terminé par un 0 est divisible par 10, la base.

Ces trois règles ont un sens général, en cela qu'elles vont s'appliquer à d'autres bases que 10. Ce qui n'est pas le cas pour d'autres règles, liées à la base 10, comme les règles de divisibilité par 3 et 5.

Pourquoi 10 chiffres ? Il est logique de penser au nombre des doigts des deux mains. Ils représentent surtout un bon compromis entre le nombre de signes différents à mémoriser et la taille en chiffres du nombre écrit.

Une fois choisi le nombre de signes (les dix chiffres, la base) utilisables, si nous souhaitons de plus que chaque nombre ait une représentation, et que celle-ci soit unique, c'est la meilleure solution, sur le plan de l'efficacité et du bon sens, que nous venons de décrire.

Mais le choix de la base reste libre.

12.1.2 Binaire

Nous avons défini la signification en entiers naturels, c'est à dire positifs ou nuls, d'un nombre écrit en décimal, en base 10. Notre objectif est maintenant d'introduire d'autres bases, et essentiellement le binaire , ou base 2 , en procédant par analogie et opposition avec le décimal.

Notre seule ambition est l'application aux microprocesseurs, pas la généralisation mathématique. Les deux chiffres de le base 2 seront donc les deux états 0 et 1 des fils ou des cases mémoires de nos machines, dans leur interprétation numérique la plus logique. Nous considérons donc des fils qui, groupés par 4, 8, 16, 32 ou 64 bits, correspondent à une valeur entière.

Nous voyons déjà, de par ces groupes de largeur fixe, deux différences entre le décimal et le binaire : D’une part, le nombre maximal de valeurs représentables, dans un nombre de bits donné, a de l'importance. Par ailleurs, il est bon de conserver les chiffres, ou bits, à 0 à gauche du nombre. Ainsi, 00000101, 5 écrit en binaire sur 8 bits, ne sera pas tout à fait équivalent à 0000000000000101, 5 en binaire sur 16 bits.

Ce que nous venons d'affirmer est mathématiquement incohérent ; il n'y a pas de différence de ce type entre décimal et binaire. En fait, la limitation est due à l'ordinateur, et non au système binaire. Mais, peu importe, puisque nous allons souvent nous trouver réellement confrontés à un problème de dépassement de capacité. Compter 1204 moutons sur un ordinateur 8 bits est un vrai problème.

Pour le reste, l'analogie est totale. Les mots unités, dizaines et centaines disparaissent, mais les puissances successives de la base demeurent, et 1, 10, 100, etc., deviennent 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, etc. Le 1 joue le rôle du 9, dans un comptage par exemple, en ce sens qu'après 1 nous repassons à 0 avec un report. La table d'addition est plus courte, mais sera utilisée de la même façon :

Addition binaire
figure a1.01 Addition binaire [the .swf]

La seconde addition représente un cas de dépassement de capacité. Dans l'ordinateur, le positionnement à 1 d'un bit particulier, CF, nous permettra de traiter cet événement. Sinon, le résultat serait 147 + 114 = 5.

Nous devons ici signaler une règle, par ailleurs facile à prouver, et valable pour tous les systèmes de numérations, base 10 et base 16 comprises : quand l'addition de deux chiffres génère une retenue, celle-ci ne peut être que de 1 au maximum. Mieux, quand deux chiffres et une retenue éventuelle de 1 sont additionnés, cela reste vrai. Pas de démonstration, mais un exemple en décimal :

9 + 9 + 1 [retenue antérieure] donne 19, donc 9 et 1 de retenue.

Idem en binaire :

1 + 1 + 1 [retenue antérieure] donne 11, donc 1 et 1 de retenue.

Donc, 1 bit, le CF, suffit pour des additions sur 8, 16, 32... bits, y compris l'addition ADC, Add with Carry.

Les nombres pairs (= divisibles par la base) seront ceux terminés par le bit 0. Le dernier bit à droite, le moins significatif ou LSB (Least Significant Bit) est donc le reste de la division par deux. De la même façon, le reste de la division par 4 est le nombre représenté par les deux bits de droite, etc. Analogie : les décimaux terminés par 0 sont divisibles par 10. Les deux derniers chiffres d'un nombre décimal sont le reste de sa division par 100.

Un décalage d'un bit vers la gauche en complétant par un 0 est équivalent à une multiplication par 2. De 2 bits, par 4, etc. Analogie : en ajoutant un 0 à droite d'un décimal, il est multiplié par 10, par 100 en ajoutant deux 0, etc.

De même, un décalage vers la droite donne les valeurs divisées par 2, puis 4 puis 8, etc., avec perte du reste. Analogie : décaler vers la droite, c’est-à-dire faire sauter le chiffre le plus à droite d'un décimal, revient à le diviser par 10 avec perte du reste.

Il existe en assembleur, ou même en langage de haut niveau, des instructions agissant directement sur les bits, comme les instructions de décalages qui viennent d'être évoquées ou les instructions logiques bit à bit. Ces instructions se comprennent mieux avec à l'esprit la représentation binaire de l'opérande, mais celui-ci pourra être exprimé dans la base de son choix. Par exemple, les lignes :

mov eax, 12445
shl eax, 4 

représentent une façon plus efficace de coder une multiplication par 16 du registre EAX, qui contient la valeur 12445, ici exprimée en décimal, si le dépassement n'est pas à craindre.

Il nous arrive d'entendre que le choix de la base de numération n'est que purement conventionnel, que l'ordinateur n'est pas plus binaire que décimal ou hexadécimal. C'est faux, le microprocesseur que nous connaissons est plus binaire que décimal. Sur 8 bits par exemple, nous pourrons représenter 255 valeurs différentes. Ce nombre est une réalité qui n'a rien de conventionnel. Ces limites appartiennent bien au monde du binaire, et non du décimal. Certaines machines à calculer mécaniques sont par essence décimales, et les nombres 255 et 256 n'y ont rien de particulier.

12.1.3 Hexadécimal, octal

Plus une base sera grande (10 par rapport à 2), plus l'écriture des nombres sera compacte. La base 2 est par essence bien adaptée à décrire le fonctionnement de l'ordinateur, mais la taille des nombres la rend souvent inexploitable. Il a donc fallu se tourner vers une représentation dans une base plus grande. Pourquoi autre chose que la base 10, notre base maternelle ? C'est ce que nous allons voir maintenant.

Pour construire un système de numération à base 100, la principale difficulté serait la définition et la mémorisation de 100 symboles différents. Imaginons que nous ayons accompli ce travail, à l'aide d'un bon CD-Rom de cliparts par exemple, et affirmons que les symboles se notent Sy[n] , avec n allant de 0 à 99. Sy[0] est 0, par exemple, et nécessairement. Sy[79] pourrait aussi bien représenter un ibis, un peuplier ou la croix du Languedoc.

Convertir un nombre, (124578) 10 par exemple, de base 10 en base 100 se fera très simplement, deux chiffres par deux chiffres. Dans notre cas, par la suite des trois symboles Sy[12] , Sy[45] et Sy[78] , ce qui pourrait donner Vautour Tamanoir Autobus en hiéroglyphes modernes. Le passage inverse est tout aussi simple.

Le passage d’une base à une autre est simple quand la base la plus grande est elle-même une puissance de la base la plus petite. La base la plus grande pourrait être 100, 1000, 10000, etc., dans le cas de la base 10. Exprimé différemment, chaque symbole dans la base la plus grande recouvrira exactement 2, 3 puis 4 symboles de la base la plus petite.

Les bases puissances de 10 sont 100, 1000, etc. Nous avions écrit qu'ajouter un 0 à droite d'un nombre était équivalent à le multiplier par la base. Nous pouvons en déduire que les bases puissances de 2, ou (10) 2 , sont (100) 2 soit 4, (1000) 2 soit 8, (10000) 2 soit 16, etc. Cette règle, disant que les bases puissances s'écrivent 100, 1000, 10000,... dans la base de départ est généralisable à toutes les bases.

Les bases 10 et 2 n'ont pas ce type de rapport. Comparez 133 à son expression binaire 10000101. Il n’y a pas de moyen simple de passer de l’une à l’autre, et réciproquement. Pire, il est impossible de déterminer exactement à quels chiffres d’une base correspondent quels chiffres dans l’autre.

Nous avons vu que les premières bases puissances de la base 2 seront 4, 8 et 16 (100, 1000 et 10000, si écrit en binaire). La base 10, 1010 en binaire, n'en fait pas partie.

La base 4 n’existe pas réellement. Elle admettrait les 4 chiffres 0, 1, 2 et 3. La traduction de la base 2 vers la base 4 se fait simplement par paquets de 2 bits. 10000101 en binaire deviendrait 2011 en base 4. Dans cette hypothétique base 4, chaque chiffre serait représenté par deux fils.

La base 8 existe ; elle a une certaine importance historique et s’appelle l’ octal . Elle utilise les 8 chiffres 0, 1, 2, 3, 4, 5, 6 et 7. 10000101 en binaire devient 205 en octal. Le chiffre octal de base est représenté par 3 fils. C’est bien là le problème de l’octal, qui explique que ce système soit aujourd’hui obsolète. Ce trio de fils ne correspond à rien de nos jours.

Il en va tout autrement de la base 16, ou hexadécimal . Ce système admet les 16 chiffres 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, et F. 10000101 en binaire devient 85 en hexadécimal. De binaire à hexadécimal ou octal et réciproquement, la conversion se fera par paquets de 3 ou 4 bits, très simplement :

Conversion Binaire/Hexadécimal/Octal chiffre par chiffre

Valeur

Chiffre hexa

Quartet binaire

Chiffre Octal

Trio binaire

0

0

0000

0

000

1

1

0001

1

001

2

2

0010

2

010

3

3

0011

3

011

4

4

0100

4

100

5

5

0101

5

101

6

6

0110

6

110

7

7

0111

7

111

8

8

1000

(10)

 

9

9

1001

(11)

 

10

A

1010

(12)

 

11

B

1011

(13)

 

12

C

1100

(14)

 

13

D

1101

(15)

 

14

E

1110

(16)

 

15

F

1111

(17)

 

S'il vous arrivait d'avoir besoin d'effectuer des conversions binaire/hexadécimal de façon répétitive, il serait utile de crayonner une partie de ce tableau sur un coin de feuille.

Tout est dit, mais reprenons nos analogies décimales, pour nous familiariser un peu avec l'arithmétique hexadécimale.

C'est le F qui joue en hexadécimal le rôle du 9 en décimal. La valeur du F est 15 en décimal, et s'écrit 1111 en binaire. Plus généralement, un nombre composé uniquement de F en hexadécimal correspondra à un nombre binaire dont tous les bits sont à 1.

Le chiffre hexadécimal correspond à 4 bits en binaire, donc à 4 fils sur les bus. Nous pouvons donc dire que, pour nous informaticiens, l’hexadécimal est une façon pratique et compacte de noter la réalité binaire. Cela est d’autant plus vrai que les largeurs usuelles des bus de micro-ordinateurs et des microprocesseurs associés sont multiples de 4, puisque ce sont des puissances de 2 : 8, 16, 32 sont des largeurs connues sur les micro-ordinateurs. À ce sujet, pensez qu'il existe des bus internes plus larges, et que justement ils font 64, 128, 256 bits de largeur.

En revanche, comme le binaire, l’hexadécimal a peu de rapports avec le décimal. Pas de conversion simple, en d’autres termes pas de rapport entre un chiffre ou un groupe de chiffres entre une base et l’autre. En synthèse, nous pouvons dire :

  Nous sommes décimaux dans notre expression et visualisation des chiffres.

  Nos ordinateurs sont binaires.

  Parler d’hexadécimal est une autre façon de parler de binaire.

Ce que le binaire peut faire mieux que l'hexadécimal, c'est bien entendu traduire la valeur d'un ou plusieurs bits particuliers dans un mot.

L'hexadécimal est très utilisé pour représenter des adresses. La correspondance entre un chiffre et 4 fils est très pratique pour mettre en valeur, dans une adresse, la notion de page.

Pour terminer voici un résumé du nombre de valeurs possibles et des limites dans les trois largeurs de mots classiques : 

Quelques valeurs à retenir...

Base

8 bits

16 bits

32 bits

Valeurs possibles

256 = 2 8

65536 = 2 16

4294967296 = 2 32

Décimal

0 à 255

0 à 65535

0 à 4294967295

Décimal si signé

-128 à +127

-32768 à +32767

-2147483648 à +2147483647

Hexadécimal

00 à FF

0000 à FFFF

00000000 à FFFFFFFF

Binaire

0. [8]..0 à 1. [8].1

0. [16].0 à 1. [16].1

0. [32].0 à 1.[32].1

Vous y voyez apparaître une représentation de nombres négatifs. C'est justement l'objet de la section suivante, pas nécessairement la plus simple.

12.1.4 Les entiers négatifs

Nous avons vu la représentation naturelle des entiers positifs ou nuls à partir de bus de 8, 16, 32 ou 64 bits de largeur : représentation binaire, celle de l’ordinateur. Nous avons également vu que la représentation hexadécimale se déduisait bit à bit de la représentation binaire.

Nous savons qu’en décimal, ou plus exactement en langage courant, nous exprimons qu’un nombre est négatif en le faisant précéder du signe -, le signe +, par défaut, étant plus rarement indiqué. Indiquer un signe + est souvent une façon d'exprimer que ce nombre est signé, que nous nous situons dans l'ensemble des entiers relatifs.

En binaire, tout doit tenir dans les bits dont nous disposons. Remarquons que le signe, + ou -, s'exprime complètement sur un seul bit. Complètement signifie sans gaspillage d'informations. Vous avez vu ou verrez que les entiers BCD de la FPU codent le signe sur un octet complet : il y a là gaspillage d'informations.

Dans le microprocesseur sont implémentées des instructions d'addition, incrémentation et décrémentation ( ADD , INC , DEC ). Leur comportement aux passages de la limite de capacité est celui du compteur kilométrique, passage de la valeur maximale à zéro en montant, l'inverse en descendant. Ce comportement est ce qu'il y a de moins mauvais, puisque la gestion du dépassement par retenue, à l'aide de l'indicateur CF (et de l'instruction ADC ), est facile.

Ce fonctionnement en non signé est donc très cohérent, et les concepteurs de microprocesseurs désiraient le conserver, mais souhaitaient ajouter une possibilité de traiter facilement des entiers signés. Les raisons sont nombreuses et indiscutables de conserver le choix entre deux modes, alors qu'une représentation signée pourrait couvrir tous les cas de calcul.

Attention

Important !

En fait de traitement d'entiers signés, le microprocesseur va continuer à travailler sur ses mots binaires comme s'ils étaient exclusivement positifs ; et nous allons au besoin interpréter différemment ces mots pour faire du calcul signé avec un minimum d'ajustements. C'est un point crucial dans la compréhension et nous y revenons dans le fil de l'ouvrage dès qu'un exemple nous en donne l'occasion. Que les initiés nous en excusent.

Rappelons que la valeur absolue d'un nombre est tout simplement sa valeur sans le signe, donc un nombre positif. La valeur absolue de x se note |x|. On écrit donc par exemple : |-13|=|13|=13. Un entier signé est, de la façon la plus générale, décrit par son signe + ou - et par sa valeur absolue.

Il a donc été recherché une représentation des nombres signés, ou plus exactement une interprétation signée des nombres binaires que nous connaissons : le nombre binaire 10000101 vaut 133 en binaire non signé, que nous avons appelé la représentation naturelle. Que devra-t-il valoir dans la représentation signée ? Jouons aux savants pendant quelques lignes. Pourquoi ? Parce que la solution qui a été retenue semble inutilement compliquée, par rapport à une solution qui nous vient immédiatement à l'esprit, et que nous buttons souvent sur ce flou. Le développement est fait sur 8 bits dans un premier temps. Qu'attend-on d'une bonne représentation en entiers négatifs ?

Nous voulons pouvoir représenter tous les nombres. Or, il semble bien qu'il y ait "autant" de nombres positifs que de nombres négatifs. Donc, sur 8 bits, nous ne devrions plus représenter que 128 nombres positifs à peu près, pour en représenter à peu près autant de négatifs.

Nous aimerions bien que les nombres positifs représentés en signé le soient de la même façon qu'en non signé. De plus, nous voudrions pouvoir facilement déduire la valeur sur 16 bits de celle sur 8 bits, etc.

Il serait pratique que l'on puisse immédiatement déterminer le signe d'un nombre par la valeur d'un bit et, si ce bit était le MSB, le plus à gauche, nos habitudes seraient respectées.

Il faut que les opérations ADD, INC et DEC donnent le résultat attendu sur ces nombres, sans autre modification, autant que faire se peut. Ce point demande par exemple qu'incrémenter un négatif décrémente sa valeur absolue.

Le mathématicien qui se réveille nous souffle qu'il serait préférable que, dans la plage représentée, chaque valeur ait une et une seule représentation. Donc, une seule représentation du zéro.

Il serait enfin souhaitable que le fonctionnement en compteur kilométrique soit conservé, sans rajouter de rupture supplémentaire.

Commençons par tester la représentation intuitivement la plus évidente : réserver le MSB (bit de poids le plus fort) à la représentation du signe, 0 pour + et 1 pour -, en lui donnant le nom de bit de signe, les bits restants exprimant la valeur absolue en binaire naturel. Testons rapidement cette solution :

12 + (-12) va donner -24, décrémenter 0 donne -127, incrémenter -21 donne -22, le reste à l'avenant. De plus, nous voyons qu'il y a un +0 et un -0, donc deux valeurs pour 0. Nous sommes donc corrects sur les points 1, 2 et 3, et calamiteux sur les autres.

Cherchons donc autre chose, avec un semblant de méthode. Sur 8 bits, si pN est un nombre, nommons nN = -1 x pN son opposé, et cherchons au minimum à respecter le critère pN + nN = 0. En respectant cela, nous additionnons un nombre et la représentation de son inverse, avec l'opérateur d'addition normal, qui ne connaît pas la notion de nombre négatif. C'est donc en réalité 256, qui vaut 0 sur 8 bits, que nous allons atteindre.

Donc : pN + nN = 256 = 255 + 1. Donc, pN + (nN - 1) = 255. 255, c'est tous les bits à 1. Or, que faut-il ajouter à un bit pour obtenir 1 ? Son inverse, puisque 1 + 0 = 1 et 0 + 1 = 1. Il faut donc que (nN - 1) soit l'inverse bit à bit de pN, NOT pN pour parler assembleur. Donc nN = (NOT pN) + 1.

Vous lirez parfois l'expression complément à 1  pour l'inverse bit à bit NOT pN. La dénomination complément à 2  pour l'inverse nN = - pN est universellement utilisée pour désigner ce codage des entiers négatifs, qui n'est pas le seul mais le plus courant. Résumons :

  Le complément à 1 d'un nombre exprimé en binaire s'obtient en inversant chacun de ses bits.

  Le complément à 2 d'un nombre exprimé en binaire s'obtient en ajoutant 1 à son complément à 1.

Testons la définition suivante :

Entiers signés – Calcul
figure a1.02 Entiers signés – Calcul [the .swf]

L'inverse de l'inverse d'un nombre est ce nombre lui-même. L'inverse de 0 est 0. Le 0 est unique. Le MSB joue le rôle de bit de signe. Nous vérifions que tous les critères semblent respectés. Rappelons que nous ne prouvons rien, au sens mathématique ; nous testons sur huit bits une solution qui fonctionne. Sinon, ça se saurait.

Cette définition, pour beaucoup d'entre nous, n'apparaît pas naturelle. Avant de l'admettre définitivement, étudions-la graphiquement :

Entiers signés – Répartition
figure a1.03 Entiers signés – Répartition [the .swf]

Les deux premières colonnes représentent le contenu d'une mémoire 8 bits, en binaire puis en hexadécimal, quand elle est incrémentée à partir de l'état tous les bits à 0. C'est le contenu physique de la mémoire. La troisième colonne représente la traduction naturelle en décimal non signé. Les traits horizontaux un peu plus épais représentent une rupture dans la progression naturelle du comptage. Pour l'instant, la seule rupture était attendue, elle marque le passage de la valeur maximale, 255 sur 8 bits, vers le 0.

La quatrième colonne montre la mauvaise représentation en entiers signés. Les défauts apparaissent clairement : plusieurs 0, ruptures deux fois plus nombreuses, les instructions INC et DEC fonctionnent parfois à l'envers...

La cinquième colonne représente la traduction en complément à 2. C'est là que nous nous apercevons que c'est la bonne méthode, et la seule. La sixième colonne est simplement un décalage de la cinquième, pour peut-être une lecture plus facile. Les valeurs s'incrémentent naturellement, sauf lors d'une seule rupture indispensable.

Les microprocesseurs possèdent un indicateur, CF (pour Carry Flag), qui est positionné lorsqu'une opération, addition par exemple, fait passer au-delà de la rupture 0 vers 255. Il est ensuite facile d'en tenir compte pour réagir. Pour traiter les nombres négatifs, il a suffi d’ajouter un autre indicateur, OF (pour Overflow Flag). Celui-ci est positionné au passage de la rupture 127 vers -128, qui est en binaire naturel la rupture 127 vers 128. OF n'est pas le bit ou flag de signe SF qui, lui, recopie simplement le MSB. Ces flags CF et OF sont toujours positionnés. C'est au programmeur d'en tenir compte ou non. En ce sens, répétons-le encore, il n'y a pas, ou peu, d'arithmétique signée dans le microprocesseur. Il y a simplement une circuiterie qui va grandement faciliter le travail du programmeur.

Deux petits défauts de cette représentation signée :

Obtenir l'inverse d'un nombre n'est pas immédiat. En assembleur x86, c'est NEG qui donnera l'inverse d'un nombre au sens arithmétique, c'est le complément à 2. NOT inverse un mot bit à bit, c'est le complément à 1.

De même, la valeur absolue d'un nombre n'est pas de façon simple dans ses bits. Pour info, voici une méthode élégante bien connue pour extraire cette valeur absolue, sur 8 et 16 bits :

cwd
xor  ax, dx
sub  ax, dx

et :

cbw
xor al, ah
sub al, ah

Une méthode plus "normale" serait :  

or   ax, ax
jge   suite
neg  ax
suite :

 

Voici un petit listing (sous Delphi), dont le rôle est d'afficher les interprétations signées et non signées en parallèle, et de mettre en évidence le comportement de OF et CF :

var
  i, CFlag, OFlag : Integer;
  b: Byte;     // 8 bits non signés
  s: Shortint; // 8 bits signés
begin
  b := 0;
  repeat
    OFlag := 0;
    CFlag := 0;
    asm
      add b, 1
      jno @suite1
      mov OFlag,1
      @suite1:
      jnc @fin
      mov CFlag, 1
      @fin:
      mov al, b
      mov s, al
  end; //asm
    MemoSortie.Lines.Add(IntToStr(CFlag) + 
                   '  ' + IntToStr(OFlag) + 
                   '  ' + IntToStr(b)+ 
                   '  ' + IntToStr(s));
  until b = 0;

b est un entier non signé, s un entier signé. L'assembleur est utilisé pour "transporter" la valeur brute, les bits, depuis b vers s. C'est Delphi qui va effectuer l'interprétation. Donc, nous ne prouvons rien, nous vérifions. Le source et l'exécutable sont sur le CD-Rom.

Il reste à voir comment tout cela se comporte sur des mots de 8, 16 et 32 bits. Le problème est d'étendre un mot sur un plus grand nombre de bits. Les valeurs positives, en signé et non signé, se complètent tout simplement par des 0. Les valeurs négatives se complètent par des 1, donc des F en hexadécimal. Cela est appelé la propagation du bit de signe , et c'est un (tout petit) problème de programmation courant. L'explication découle de la définition du complément à 2. Quelques exemples suffiront pour se fixer les idées :

Extension de format en non signé

Interprétation décimale

Binaire 8 bits

Binaire 16 bits

Hexa 8 bits

Hexa 32 bits

18

0001 0010

0000 0000 0001 0010

12

00000012

215

1101 0111

0000 0000 1101 0111

D7

000000D7

 

 

Extension de format en signé - Propagation  du signe

Interprétation décimale

Binaire 8 bits

Binaire 16 bits

Hexa 8 bits

Hexa 32 bits

18

0001 0010

0000 0000 0001 0010

12

00000012

-41

1101 0111

1111 1111 1101 0111

D7

FFFFFFD7

 

12.1.5 La saisie et l'expression orale

Nous parlons de saisie avant d’aborder la conversion, parce qu'une saisie bien menée permet très souvent d'éviter la conversion.

Dire ou écrire qu'il est possible de représenter 256 valeurs, de 0 à 255, avec 8 bits en binaire, n'est pas ambigu. Le système décimal est le seul système d'échange usuel entre nous. Évitons également les rébus du style : "Six s'écrit cent dix en binaire", au profit de "Six s'écrit (zéro) un, un, zéro en binaire". Un nombre se prononce en décimal. Dans une autre base, il s'écrit ou il s'épelle.

Le langage machine est binaire. En assembleur ou en langage de haut niveau, une étape préalable d'analyse du source, assimilable à un préprocesseur, permet la saisie dans différents systèmes de numérations, y compris par exemple un caractère à la place de son code ASCII.

Il faut bien choisir la base en fonction du type de la donnée :

  Les 243 moutons n'ont aucune raison de devenir (F3) 16 , pas plus que (11110011) 2 .

  Les masques pourront être saisis en binaire : and al, 00000100b suggère une extraction du bit 2.

  Les adresses et d'autres quantités à connotation informatique se saisiront en hexadécimal.

  Un bon format de saisie peut remplacer un commentaire dans le source :

mov al, ‘S’
and al, 00000100b

est mieux que :

mov al, 83  ; lettre S dans al
and al, 4   ; extraction du bit 2  

Dans le cas d'une saisie en binaire ou hexadécimal, pensez à laisser les 0 nécessaires à gauche.

Il est généralement impossible d’insérer des espaces séparateurs entre les groupes de 4 chiffres, ce qui serait pourtant pratique. Vous pouvez toujours saisir ou vérifier en insérant ces espaces, puis les ôter avant compilation.

La saisie de constantes binaires, masques par exemple, sur 32 ou 64 bits sera toujours fastidieuse et génératrice de risques d'erreur. Vous pouvez résoudre le problème une fois pour toutes en créant des listes de macros qui pourraient être nommées par exemple bit32_00 à bit32_31.

Syntaxes courantes

La syntaxe de saisie des constantes numériques varie selon les compilateurs ou assembleurs. Il existe néanmoins des… constantes :

Sous MASM, il est possible de définir une base par défaut, à l'aide de la directive .RADIX  suivie d'un nombre entre 2 et 16. Cette étendue de choix dépasse largement nos besoins.

Le décimal est souvent le système par défaut, qui sera saisi tel quel, mais qui pourra éventuellement être suffixé d’un d ou D, voire d'un t ou T : dans mov ax, 1245d , la constante est bien (1245) 10 et non (1245D) 16 . Piège...

L’hexadécimal peut, selon les cas, être préfixé d’un $, ou d’un 0x, ou encore suffixé d’un h ou H :

$1DF3, 0x1DF3, 1DF3h, 1DF3H.

L'octal demande une lettre o ou O, q ou Q, parfois en suffixe. Parfois, dans le cas de compilateurs C, il suffit qu'une constante commence par un 0 (zéro) pour être interprétée comme de l'octal. Comme dans 012, qui interprété en octal vaut 10. Piège...

Le binaire, quand sa saisie directe est autorisée, demande un b ou B, y ou Y en suffixe.

Dans le cas d'un suffixe, si une constante ne doit pas commencer par une lettre, sous peine d’être interprétée comme un nom de variable, il faut lui ajouter un 0 initial :

mov ax, ABCDh ne se compile pas, mais mov ax, 0ABCDh fonctionne.

L'archaïque utilitaire DEBUG est assez dangereux sur ce plan. Il n'admet que des constantes hexadécimales, sans préfixe ni suffixe. Si vous ajoutez par erreur un d, un D, un b ou un B en suffixe, avec l’intention de saisir une constante en décimal ou en binaire, il sera interprété comme un chiffre hexadécimal. Si cela ne provoque pas de dépassement de la taille maximale, aucune erreur ne sera signalée. Mais peut-être n'utilisez-vous pas DEBUG de façon habituelle...

12.1.6 Les conversions

Malgré les diverses possibilités de saisie et les quelques outils présentés plus loin, vous devrez parfois convertir à la main, ou écrire une routine de conversion.

Un changement de base ne modifie pas vraiment un nombre vers un autre nombre, mais plutôt une chaîne de caractères vers une autre. Certaines chaînes sont considérées comme des nombres, par exemple le contenu d'une variable pour le microprocesseur, ou la représentation décimale pour nous.

Les conversions Binaire-Hexa, dans les deux sens, se traitent très facilement, puisqu'il y a correspondance directe entre paquets de chiffres, ou de bits, comme nous l'avons déjà vu. Nous pouvons rajouter l'octal à cette catégorie.

Pour les autres conversions papier-crayon, nous partons toujours de la base 10 ou nous y arrivons. Si, exceptionnellement, nous devions passer par exemple de la base 5 à la base 7, nous passerions par la base 10. C'est a priori la seule dans laquelle nous savons travailler à la main. En programmation, il en irait autrement.

Base 10 vers autre base

À partir de la base 10 : nous procédons par divisions successives du nombre par la base cible. Les restes successifs représentent le résultat. Deux exemples suffiront  :

3731 à convertir en hexadécimal :

3731 / 16 = 233 reste  
3
 233 / 16 =  14 reste  
9
  14 / 16 =   0 reste 
14

14 correspond au chiffre E en hexa. Le résultat est donc E93h (soit 1110 1001 0011 en binaire, à l'aide du tableau).

Le même, en binaire :

3731 / 2 = 1865 reste 
1
1865 / 2 =  932 reste 
1
 932 / 2 =  466 reste 
0
 466 / 2 =  233 reste 
0
 
 233 / 2 =  116 reste 
1
 116 / 2 =   58 reste 
0
  58 / 2 =   29 reste 
0
  29 / 2 =   14 reste 
1
 
  14 / 2 =    7 reste 
0
   7 / 2 =    3 reste 
1
   3 / 2 =    1 reste 
1
   1 / 2 =    0 reste 
1

Le résultat est donc bien 1110 1001 0011, soit E93h en hexadécimal, à l'aide toujours du tableau.

Base quelconque vers base 10

Pour aller vers la base 10 à partir d'un nombre xn .... x2 x1 x0 (dans le cas de l'hexadécimal, les xi vont de 0 à F, F valant 15) en base X, nous appliquons simplement la définition d'une base :

Résultat = (xn x X n ) + ... +  (x2 x X 2 ) + (x1 x X) + x0.

Soit E93h à convertir en base 10 :

E donne 14. Le résultat est donc (14 x 16 x 16) + (9 x 16) + 3 = 3731.

Pour convertir un binaire en décimal, le procédé est le même. Mais avec un peu d'habitude, il est possible d'aller plus rapidement :

Une méthode simplifiée
figure a1.04 Une méthode simplifiée [the .swf]

Dans ce premier exemple, nous passons tout d'abord à l'hexadécimal. Puis il nous suffit de calculer mentalement 10 + (7 x 16), puisque Ah vaut 10 et 7h vaut 7.

Une autre méthode simplifiée
figure a1.05 Une autre méthode simplifiée [the .swf]

Méthode intéressante, surtout si peu de bits sont allumés. Il suffit d'énumérer les puissances de 2, à partir de 1 pour le LSB, et de ne conserver que celles qui sont en face d'un bit à 1.

12.1.7 Les outils

Nous avons vu qu'il n'est pas très courant d'avoir à convertir entre bases. C'est peut-être en phase d'apprentissage que vous aurez le plus besoin de pratiquer. Vous avez malgré tout besoin d'un utilitaire de conversion.

Vous trouverez sur Internet quelques bons freewares. Attention, rares sont ceux réellement plus efficaces que la calculatrice de Windows.

C'est peut-être le bon moment pour écrire votre propre utilitaire. Pensez alors à suffisamment modulariser et paramétrer vos fonctions, pour pouvoir à l'occasion les réutiliser. Cette démarche présente un intérêt pédagogique certain.

La Calculatrice de Windows, en particulier dans ses dernières versions pour XP ou 2000, est suffisante pour un usage occasionnel. Elle propose, outre le décimal, le binaire, l'hexadécimal et même l'octal, mais ne permet de 0 à gauche dans aucune base. Elle a parfois des comportements étranges :

La calculatrice de Windows, version XP
figure a1.06 La calculatrice de Windows, version XP

Le signe - devant le nombre binaire est surtout gênant parce que la calculatrice utilise par ailleurs le complément à 2. Un nombre décimal négatif est converti correctement en binaire et en hexadécimal, La touche +/- de changement de signe fonctionne, dans la largeur de bus sélectionnée. Mais il n'est pas possible de saisir en binaire un nombre considéré comme négatif pour, par exemple, connaître sa valeur. Il est alors nécessaire de ruser : soit le nombre 10010100b, qui en arithmétique signée vaut -108. Saisissons-le en binaire. Si nous passons en décimal, c’est 148, son interprétation en non signé, qui va s’afficher. En revanche si, avant le passage en décimal, nous inversons le signe du nombre binaire par la touche +/-, le résultat en décimal sera 108. Une bonne calculatrice multibase peut difficilement faire l'économie d'une touche signé/non signé.

Excel dispose, dans la macro complémentaire Utilitaires d'analyse , d’une série de fonctions (HEXDEC, DECHEX, BINHEX, OCTHEX, etc.) de conversion. Un dépannage, tout au plus.

Si vous possédez une calculette scientifique, même bon marché, qui gère les bases, ne cherchez pas plus loin ; avec un crayon et une feuille de papier, c’est idéal. Vérifiez toutefois son comportement sur des entiers signés et son ergonomie : la saisie de caractères hexadécimaux alphabétiques est parfois d'une complexité rédhibitoire.

Ce que vous pouvez retenir, c'est qu'un utilitaire ou une calculatrice ne peut pas deviner si vous travaillez en mode signé ou non signé : un bon produit devra donc proposer ce choix et permettre de le modifier à tout instant. Sinon, ce ne sera qu'un produit approximatif.

12.2 Annexe B - Notions de logique

La logique intervient à trois niveaux au moins, dans notre apprentissage :

Dans la conception du micro-ordinateur, au chapitre Structure d'un micro-ordinateur . Il s'agit alors d'électronique, de logique câblée.

Dans la présentation du jeu d'instructions, plus particulièrement des instructions logiques bit à bit que sont NOT , OR , AND et XOR . Il s'agit de logique ou algèbre de Boole, et en programmation, sa maîtrise est un préalable à celle des masques.

Dans la conception des programmes. Vous aurez à réagir à certaines conditions ou combinaisons de conditions. C'est encore de la logique booléenne ou tout simplement de la logique de tous les jours.

Les objets concernés dans ces trois domaines sont respectivement des niveaux électriques, des bits et des assertions. Les raisonnements et les résultats sont interchangeables dans une large mesure. En fait, quelle que soit la nature du problème, il y a plusieurs (essentiellement trois) façons de l'aborder. La solution d'un problème de la vie courante peut fort bien être facilitée par une table de vérité ; un peu de bon sens suffit souvent en électronique logique.

Nous utilisons un mélange de termes anglais et français pour désigner les fonctions logiques. Ce choix est discutable. Nous avons toutefois décidé, comme dans l’ensemble de l’ouvrage, de privilégier à chaque fois que cela est possible le rapport avec le reste de la documentation et les mnémoniques de l’assembleur.

À notre décharge, constatons que l’utilisation du OR à la place du OU lève toute ambiguïté : il s’agit du OR de l’ordinateur, donc d’un OU INCLUSIF.

Explicitons en langage courant les quatre fonctions logiques donnant lieu à une instruction bit à bit dans le jeu d'instructions de la famille de processeurs x86.

Ces fonctions sont en première approche des boîtes acceptant un ou deux objets logiques à l'entrée et prenant, en fonction de la valeur de ces objets, une valeur logique résultante en sortie. Chaque mot est important :

  AND  : la sortie n'est vraie que si les deux entrées sont vraies. Pour louer une automobile, vous devez posséder le permis de conduire depuis plus d'un an ET être âgé de plus de 21 ans.

  OR  : la sortie est vraie si au moins une des entrées est vraie. Pour avoir une réduction sur les transports urbains, vous devez avoir plus de 60 ans OU des revenus faibles. À 72 ans et sans revenus, elle vous sera bien sûr également accordée.

  XOR  : la sortie est vraie si une des entrées est vraie, mais pas les deux. Fromage ou dessert serait un mauvais exemple, puisque le restaurateur acceptera que vous ne preniez ni l'un ni l'autre. Disons qu'une porte doit être ouverte OU fermée.

  NOT  : la sortie n'est vraie que si l'entrée est fausse. (NOT ouvert) = fermé, pour la porte, celle de l'exemple précédent qui ne peut pas être entrebâillée.

(A OR (NOT A)) est toujours vrai ; du moins dans la logique que nous utilisons, puisqu'il existe une logique aristotélicienne comme il existe une géométrie euclidienne. Nos ordinateurs sont euclidiens et aristotéliciens.

Qu'exprime le restaurateur quand il inscrit sur la carte "Fromage ou Dessert" ? Que vous devez obligatoirement prendre du fromage ou un dessert ? Non, ou alors c'est votre maman. Ce qu'il exige de vous, c'est que vous ne preniez pas les deux. Ce qui s'écrirait : NOT(Fromage AND Dessert), c'est-à-dire qu'il veut au moins que vous ne preniez pas de fromage ou que vous ne preniez pas de dessert, c'est-à-dire : (NOT Fromage) OR (NOT Dessert). Nous venons de redécouvrir en partie le théorème de De Morgan :

NOT(A AND B) = (NOT A) OR (NOT B) et NOT(A OR B) = (NOT A) AND (NOT B)

Si cela n'est pas suffisant, vous pouvez tenter la représentation par des ensembles, souvent très parlante. Le patatoïde A est en réalité l'ensemble des éléments qui vérifient A, et ainsi de suite. La zone grise correspond alors à l'ensemble des éléments qui vérifient la relation. La relation d'implication B=>A est un peu différente, puisqu'elle décrit plus une propriété géométrique avérée ou non qu'une zone, mais se comprend parfaitement.

Vue graphique de fonctions logiques
figure a2.01 Vue graphique de fonctions logiques [the .swf]

Vous serez peut-être un jour confronté à des tests psychotechniques, dans lesquels il vous est précisé, parmi d'autres détails croustillants, que tous les chauves aiment la couleur bleue et où l'on finit par vous demander si la voiture est rouge, si elle ne l'est pas ou enfin si vous ne pouvez fichtrement rien en dire. Un conseil : si votre instinct est fatigué, n'hésitez pas à crayonner une table de vérité, un De Morgan ou quelques patatoïdes. C'est redoutablement efficace. Le tout est de bien modéliser, en particulier l'implication. Tous les chauves aiment la couleur bleue se traduit par :

  IL est chauve => IL aime la couleur bleue.

  L'ensemble des chauves est entièrement contenu dans l'ensemble de ceux qui aiment la couleur bleue.

  IL aime la couleur bleue n'entraîne rien de particulier sur la calvitie de IL.

  IL n'aime pas la couleur bleue => IL n'est pas chauve.

Une fonction logique combinatoire   est une boîte possédant une ou plusieurs entrées logiques, et donnant un état logique en sortie qui ne dépend que de la combinaison des entrées, ce qui définit la logique combinatoire. Si cet état dépend également de l'état antérieur, il s'agira de logique séquentielle . Rappelons que entrées et sorties peuvent être ici des signaux électriques, des bits, voire des assertions, la boîte un circuit logique, une instruction, un groupe d'instructions par exemple. S'il y a plusieurs sorties à une boîte, nous définirons une fonction pour chacune d'entre elles.

L'état de la sortie ne dépendant que des entrées ; pour définir la fonction, il suffira de recenser toutes les combinaisons possibles des entrées et de renseigner l'état de la sortie correspondant. C'est ce tableau qui est appelé une table de vérité . Cette définition est l'inverse d'une définition intuitive. Voyons tout de suite un exemple :

Table de vérité du AND
figure a2.02 Table de vérité du AND [the .swf]

La sortie ne sera à 1 que si les deux entrées sont à 1 ; c'est ce que nous attendions d'une fonction AND. Dans la représentation V et F, nous voyons que V correspond à 1 et F à 0. Les deux tables sont représentées dans un ordre inverse. Généralement, les compilateurs considèrent le 0 comme false et toute autre valeur comme true. Les booléens n’étant pas toujours reconnus par le C, il faut les déclarer en macros ou enum dans certains compilateurs (anciens). Il est possible d’utiliser :

#define false 0
#define true !false

Avec 2 entrées, le nombre de cas sera de 4. Il est de 2 avec une seule entrée. Il est facile de passer à 3, le nombre de cas sera de 8. Construisons cette table :

Table à 3 entrées avec calcul
figure a2.03 Table à 3 entrées avec calcul [the .swf]

Pour passer à 4 entrées/16 lignes, nous voyons qu'il faut prendre le bloc 8 x 3, le dupliquer et lui ajouter une colonne composée de huit 0 puis de huit 1. Vous remarquez que si vous lisez les colonnes des entrées ligne par ligne, comme des nombres binaires, vous comptez de 0 à 7. Tout cela est cohérent. Si notre système comprend 12 entrées, le nombre de lignes de la table de vérité sera de 4096. Bien avant d'atteindre ce nombre, il faudra penser à utiliser d'autres méthodes.

Nous en avons profité pour mettre en évidence le rôle de la table de vérité dans le calcul. L'expression S à calculer est décomposée en sous-expressions, dont le résultat est connu. La dernière opération est le XOR entre les deux résultats intermédiaires. Seules les vraies entrées, indépendantes, déterminent le nombre de cas différents à étudier.

Une fonction à deux entrées est entièrement définie par sa table de vérité, par la valeur des 4 bits de la sortie. Chaque combinaison des X de la sortie définira une fonction différente. Or, il y a 16 combinaisons possibles de 4 bits, donc 16 fonctions. Cette systématique est une bonne habitude en logique et en assembleur. C'est une approche de mathématicien, mais cet art n'est nullement indispensable. De la même façon, nous recensons 2 fonctions à une seule entrée. Les fonctions à 1 et 2 entrées suffisent à comprendre et à construire les autres.

Notre démarche va maintenant être de lister ces fonctions, en construisant leur table de vérité, et de voir ensuite si elles ressemblent à quelque chose de connu.

Les fonctions à 1 entrée
figure a2.04 Les fonctions à 1 entrée [the .swf]

Seule NOT a une véritable signification, elle correspond en électronique à un inverseur, et à l'instruction bit à bit NOT. L'identité recopie l’entrée sur la sortie. C’est en électronique un buffer. Les fonctions Nulle et Unité ne veulent pas dire grand-chose pour nous. Elles intéressent le formalisme mathématique, qui ne dira pas qu’un traitement ne fait rien, mais qu’il est égal à la fonction identité.

Les fonctions à 2 entrées
figure a2.05 Les fonctions à 2 entrées [the .swf]

Nulle et Unité ont déjà été commentées. Nous trouvons ensuite A et B, qui recopient en sortie une de leurs entrées. Ce sont les fonctions Identité vues au-dessus. Les mêmes surmontées d’une barre de négation se disent NOT A et NOT B et leur interprétation est évidente.

Les trois fonctions AND, OR et XOR ont déjà été commentées ; ce sont les seules à avoir une correspondance directe dans le jeu d’instructions. Nous allons revenir sur XOR.

Aussi importantes sont NAND et NOR, respectivement un AND et un OR suivies d’un inverseur. Elles sont surtout connues des électroniciens. Le NAND est la porte logique la plus simple à réaliser technologiquement. Elles possèdent toutes deux la propriété de permettre de construire par assemblage toutes les autres fonctions. Dans le langage courant, NOR est le NI, c’est-à-dire ni l’un ni l’autre. NAND se dirait : pas tous les deux. Elles ont donc une vraie signification.

Sur le A sans B et le B sans A, rien à dire de particulier. Il devrait en être de même pour A=>B et B=>A, qui se disent A implique B, et l’inverse. Nous aurions pu, dans la foulée, les appeler pas A sans b et pas B sans A, et cela aurait peut-être mieux valu. Le rapport entre la notion intuitive d’implication est clair, mais alors, que représente la sortie S ? Une sortie à 0 voudrait dire que le couple en entrée n’est pas POSSIBLE. Il s’agit d’un débat philosophique, certainement passionnant mais qui aujourd’hui ne peut que nous embrouiller. De l’implication, retenons la représentation par des ensembles et le fait que si A entraîne B, alors B faux entraîne que A est faux. Et rien d’autre.

Reste la table intitulée EGAUX. Nous constatons effectivement que la sortie est à 1 si et seulement si les deux entrées sont égales. Nous remarquons de même que cette fonction est la fonction XOR inversée, qu’elle est à XOR ce que NAND est à AND. La sortie de XOR est à 1 quand et seulement quand ses deux entrées sont différentes.

Le XOR (voyez également cette instruction, dans la présentation du jeu d’instructions) est bardé de propriétés intéressantes. Nous voyons au chapitre 1 comment il peut être considéré comme un inverseur programmable : il inverse un bit ou le laisse inchangé, selon l'état de l'autre entrée considérée alors comme entrée de commande. Dans le même chapitre, nous voyons qu'il est parfois nommé demi-additionneur, puisqu'il fournit en sortie la somme de ses deux entrées, la retenue devant être obtenue par un AND. Enfin, si nous XORons deux fois, bit à bit, un mot de n bits, avec le même mot, dit la clé, nous retrouvons le mot initial. Le premier XOR est un codage, le second un décodage. Quel que soit X, (X XOR X) = 0. Il est courant de mettre à zéro une mémoire ou un registre de cette façon, en assembleur :

xor eax, eax 

Les AND et OR sont très utilisés dans les opérations de masquage. Il existe plusieurs types de masquages :

  Les forçages à 1, où un ou plusieurs bits d'un mot sont positionnés à 1, les autres bits restant inchangés.

  Les forçages à 0, où un ou plusieurs bits d'un mot sont positionnés à 0, les autres bits restant inchangés.

  Les extractions pour test de nullité, où un (ou plusieurs, plus rarement) sont préservés, les autres étant positionnés à 0.

  Les extractions où un (ou plusieurs, plus rarement) sont préservés, les autres étant positionnés à 1, d'usage moins fréquent.

Sur un seul bit, les règles suivantes, évidentes, se déduisent des tables de vérité :

  (X AND 0) = (0 AND X) = 0 ;

  (X AND 1) = (1 AND X) = X ;

  (X OR 0) = (0 OR X) = X ;

  (X OR 1) = (1 OR X) = 1 ;

  (X AND X) = (X OR X) = X.

Des quatre premières règles se déduit le tableau suivant, concernant les effets de masquage sur un seul bit :

Comportement des OR et AND dans un masquage

Instruction

Valeur du bit

Effet

OR

1

Forçage à 1

OR

0

Préserve

AND

1

Préserve

AND

0

Forçage à 0

Nous en déduisons, au niveau des mots :

  Forçage de bits particuliers à 0 : AND avec un masque composé de 1, sauf les bits cibles à 0.

  Forçage de bits particuliers à 1 : OR avec un masque composé de 0, sauf les bits cibles à 1.

  Test de la nullité d'un bit : AND avec un masque composé de 0, sauf le bit cible à 1.

Ce sont les cas les plus fréquents, à appliquer sans avoir à trop réfléchir.

OR avec un masque composé de 1, sauf les bits cibles à 0 conduira à un résultat égal à -1 (FFFFh sur 16 bits par exemple) si le, ou tous les, bits cibles sont à 1.

Exceptionnellement, il est possible de tester la nullité ou la non-nullité simultanée des bits cibles.

Enfin, signalons que certaines opérations panachées ne sont pas possibles : forçage des bits 0, 1 et 2 à 010, par exemple.

 

12.3 Annexe C – Little Endian, Big Endian, implantation des données en mémoire

Nous abordons ici les notions little-endian  et big-endian , byte-order  et byte-sexual , le problème NUXI , c'est-à-dire l'implantation des données en mémoire. C'est un sujet qui, très souvent, perturbe le programmeur : ces détails embrument notre cerveau, alors qu'ils peuvent être négligés dans presque tous les cas. Ce problème est de même nature que celui de la mémoire qui décroît quand la pile croît.

Il semble que, chez la plupart des humains, les problèmes haut/bas et droite/gauche (latéralisation) soient particulièrement mal vécus par la cervelle. C'est donc de telles nuisances mentales qu'il faut tenter d’enrayer une fois pour toutes.

Le mot français indien , qu'il soit océan, asiatique ou américain, se traduit par l'anglais indian .

La règle

Sur un microprocesseur très primitif, tous les registres ont une largeur imposée, le mot. Le bus de données a la largeur d’un mot ; la mémoire est accessible mot par mot, les opérations ne jouent que sur des opérandes mots et le résultat est un mot. Dans cette configuration, chaque mot est en mémoire à son adresse, un point c'est tout. Quand à la question de l'ordre des bits dans le mot, elle n'a pas de sens. Il est même possible de croiser les fils du bus de données d'un plan mémoire sans conséquences.

Imaginons maintenant une génération postérieure : une mémoire accessible octet par octet et des registres de largeur 32 bits, ou 4 octets, des dwords. Cette architecture a certainement de bonnes raisons d'être dans certain cas, mais elle est extrêmement pernicieuse. Le fait que les dwords puissent se chevaucher n'est pas une bonne chose.

La question de l'endianité, au cœur de ce chapitre, est celle de l'ordre de stockage en mémoire des octets constituant le dword. Cette notion prend un sens, par rapport au cas élémentaire, puisqu'il est possible d'accéder à des morceaux d'un dword d'adresse A, une adresse pouvant pointer en son sein, aux adresses A+1, A+2 et A+3. Voyons les deux solutions de stockage qui se présentent :

L'alternative endian
figure a3.01 L'alternative endian [the .swf]

La mémoire est représentée de façon intuitive, croissant vers le haut et vers la droite. La valeur à placer en mémoire est 12345678h. Dans les schémas de ce chapitre, le terme msb désigne plutôt un octet, le most significant byte . De même pour lsb .

Décrivons la règle choisie par Intel. C'est le choix de gauche. Si la mémoire est écrite octet par octet, en ordre croissant, alors l'octet 78h, de poids faible, est écrit en premier, et les autres en suivant jusqu'à l'octet de poids fort. Cet octet est le petit bout de la donnée. Nous avons donc écrit la donnée petit bout en premier, little-end-first en anglais. D'où little-endian. Nous vous ferons grâce des explications absolument parallèles aboutissant à big-endian.

Rappel : dans l'architecture IA, jusqu'à ses plus récentes versions, la mémoire est accessible par octet. Nous pouvons dire que l'octet est l' atome du transfert mémoire. Réaffirmons donc clairement que, en assembleur, il est possible d'écrire une donnée 32 bits, le contenu de EAX par exemple, à une adresse quelconque, à l'octet près, en mémoire. Cela est vrai même s'il n'est pas souhaitable d'écrire n'importe où. La notion d'alignement n'est ici qu'une règle d'optimisation, certes importante.

Nous retiendrons qu'en little-endian/Intel, l'adresse d'une donnée est également l'adresse de son octet faible. Et celle de son octet fort en big-endian.

Il est fréquent de lire que la convention little-endian complique inutilement une notion qui était évidente : big-endian serait la façon naturelle de stocker des données en mémoire. Faisons la même manipulation que précédemment, mais sur la donnée 00000078h :

L'alternative endian sur 00000078h
figure a3.02 L'alternative endian sur 00000078h [the .swf]

Testons le code suivant (C++ Builder) :

byte * pb;
unsigned long dw = 0x00000078;
pb = (byte*) &dw;
Edit1->Text = IntToStr((int)dw);
Edit2->Text = IntToStr(*pb);

Par *pb, nous créons une variable octet, à la même adresse qu'une variable dw de 32 bits initialisée à 78h, soit 120. Elles affichent toutes deux cette valeur. En big-endian, *pb aurait valu 0. C'est *(pb+2) qui aurait été un octet de valeur 78h. Cette dernière phrase n'a pas été testée.

Les langages de haut niveau proposent des opérateurs permettant d'extraire les parties haute et basse d'une variable. Leur utilisation rendra le code portable, à l'inverse de celle des pointeurs.

Donc, si big-endian est typographiquement plus naturel, little-endian semble avoir des arguments à faire valoir.

Il est peut-être utile de revenir sur un point : l'effet endian, l'image de la mémoire, dépend de deux facteurs :

  La taille de la données primitive, de l'atome mémoire : l'octet dans le PC.

  La taille de la donnée traitée si elle est différente de celle de l'atome qui, dans le PC, peut varier dans de grandes proportions, de 16 à 128 bits.

Parler d'une routine de conversion big-little sur PC est incomplet. Il faut préciser : une routine de conversion big-little sur PC pour des données de 32 bits :

Influence de la taille de la donnée
figure a3.03 Influence de la taille de la donnée [the .swf]

Cette convention est généralement transparente, puisque, sur une même machine, la même est utilisée lors de chaque transfert, en lecture comme en écriture. Elle prend de l'importance lors d'échanges particuliers de données, et dès que nous souhaitons accéder à une partie de la donnée, ce qui reste rare en programmation normale . Elle est à considérer lors de l'inspection de la mémoire.

Vous constaterez que les outils afférents, les débogueurs en particulier, permettent toujours de paramétrer la taille de la donnée inspectée. Devant des octets 01 02 03 04 05 06 07 08, ces outils afficheront 01 02 03 04 05 06 07 08 en mode byte, 0201 0403 0605 0807 en mode word, 04030201 08070605 en mode double-word.

Il peut arriver (mais certainement pas dans les machines qui nous intéressent) que l'ordre des bits prenne une importance. Ce point correspond aux mots anglais consistent  et inconsistent . L'architecture Intel est Consistent Little-endian.

Ces notions se retrouvent dans les télécommunications et les réseaux. L'unité de transport est un paquet de mots. Le choix existe dans ce cas également entre les deux endians. Imaginons un ordinateur big-endian transmettant un fichier texte à un ordinateur little-endian, sur un réseau dont la taille de donnée de base transmise est le mot de 16 bits. Le texte UNIX sera alors reçu comme NUXI. D'où le terme "NUXI problem". Avec des mots de 32 bits, ce serait le "XINU problem". De bons titres pour une série B.

Attention, l'immense majorité des échanges inter-plates-formes, réseaux ou autres, se passent de façon transparente, à l'aide des couches logicielles successives. Ce sont justement les programmeurs de certaines de ces couches qui seront impliqués dans le problème endian.

Avec DEBUG

Si vous ne souhaitez pas effectuer de manipulation complexe, lancez debug, et saisissez :

a 100
mov ax, 1030
mov bx, 0204
add ax, bx
mov word ptr[110], ax

Ne vous préoccupez pas d'instruction de fin, désassemblez puis lancez le mode trace pour 4 instructions, et enfin affichez la mémoire.

Séance de test minimale
figure a3.04 Séance de test minimale

Le mov ax, 1030 se code en B8 30 10 . C'est un effet de bord de little-endian ; mais, en y réfléchissant un peu, c'était plus que prévisible : un automate doit être câblé quelque part dans la CPU, prêt à transférer une donnée 16 bits en little-endian. Il est donc évident qu'une donnée immédiate doive être dans le code objet dans le même format que celui qu'il aura en mémoire, ou que ce sera plus facile pour le moins. C'est peut-être à cette occasion que little-endian semble le plus tordu.

Les résultats du mode trace n'appellent pas de commentaires, la dernière instruction désassemblée, un JB ou toute autre instruction, n'étant que le fruit du hasard et ne sera pas exécutée.

Nous constatons que 1234h a bien été stocké en mémoire au format little-endian, 34h en 0110 et 12h en 0111.

Avec DELPHI

Nous allons saisir un programme de test sous Delphi, à partir du squelette présenté par ailleurs. Reportez-vous comme d'habitude au CD-Rom. Nous avons utilisé comme donnée primitive une variable I64_0 de type Int64, a priori propre à Delphi. Mais, en fait, les architectures 64 bits arrivent à grands pas, et ce format Int64 est conforme à une norme de fait. L'assembleur, s'il n'implémente pas dans son jeu d'instructions standard (hors FPU, MMX et autres SSE) d'instruction MOV sur 64 bits, propose CDQ, qui étend une donnée signée 32 bits sur 64 bits dans EDX:EAX. Dans cette norme, un entier 64 bits est implanté au format little-endian, et passe en registres dans EDX:EAX. La manipulation consiste à initialiser la variable I64_0 à la valeur 8F7E6D5C4B3A2910h, et à initialiser ensuite une batterie de variables de taille inférieure à partir de I64_0. Nous y ajouterons quelques lignes de code pour rapidement faire le tour de l'implantation des données les plus classiques en mémoire.

Voici le listing de notre programme de test :

var
I8_0, I8_1  :Byte;
I16_0, I16_1:Word;
I32_0, I32_1:Longword;
I64_0       :Int64;
ch_0        :Pchar;
begin
  try
    with MemoSortie.Lines do begin
    I64_0 := $8F7E6D5C4B3A2910;
    ch_0  := 'UNIX';
    asm
      mov eax, dword ptr[I64_0 + 0]
      mov I32_0, eax
      mov eax, dword ptr[I64_0 + 4]
      mov I32_1, eax
      mov ax, word ptr[I32_0 + 0]
      mov I16_0, ax
      mov ax, word ptr[I32_0 + 2]
      mov I16_1, ax
      mov al, byte ptr[I64_0 + 0]
      mov I8_0, al
      mov al, byte ptr[I64_0 + 5]
      mov I8_1, al
 
      lea eax, byte ptr[ch_0]
      mov eax, [eax]
      end;
    Add(IntToHex(I64_0, 16) + #$0D + #$0A);
    Add('I32_0: ' + IntToHex(I32_0, 8));
    Add('I32_1: ' + IntToHex(I32_1, 8) + #$0D + #$0A);
    Add('I16_0: ' + IntToHex(I16_0, 4));
    Add('I16_1: ' + IntToHex(I16_1, 4) + #$0D + #$0A);
    Add('I8_0 : ' + IntToHex(I8_0, 2));
    Add('I8_1 : ' + IntToHex(I8_1, 2) + #$0D + #$0A);
    Add('ch_0 : ' + ch_0);
    end;

Observez la facilité apportée par l'assembleur intégré. Pour obtenir le même résultat en code Pascal pur, ce serait beaucoup plus long et moins lisible, voire impossible par manipulation directe sur les pointeurs. En C++, c’et un peu plus facile. Ce commentaire ne constitue pas un point négatif pour Pascal Objet : c'est un langage fortement typé, plus rigoureux que C++. L'assembleur BASM est justement là pour ce type de manipulations.

Lançons le programme, cliquons le bouton Action 1 et observons :

Résultat attendu...
figure a3.05 Résultat attendu...

Le résultat est en effet conforme à ce que nous attendions, à notre représentation de la mémoire. Nous constatons que l'adresse d'une donnée de taille supérieure à l'octet est celle de son octet de poids faible. Nous constatons également que les autres octets de la donnée occupent des adresses supérieures. Ce qui se traduit bien dans la représentation suivante, qui pourra faire office de synthèse :

Représentation des données
figure a3.06 Représentation des données [the .swf]

Remarquez que nous avons choisi, dans cette illustration, de représenter la mémoire à l'envers

 

12.4 Annexe D - BASM, l'assembleur de Delphi

Ce résumé de la syntaxe de l'assembleur intégré à Delphi (BASM) à été mis à jour pour tenir compte de la version 7, tout en continuant à cibler principalmement la version 6. L'avenir de BASM semble incertain, puisque Delphi 8 n'incorpore plus d'assembleur en ligne. Il reste à espérer un avenir aux versions gratuites Delphi 6 et/ou Delphi 7.

Sur Internet, Borland met à disposition du public un serveur de news newsgroups.borland.com , ce qui peut être utile si votre serveur ne propose pas le groupe souhaité. Le groupe borland.public.delphi.language.basm bénéficie d'une activité régulière et intéressante. Ce n'est pas le cas de borland.public.cppbuilder.language.basm , ce qui n'est pas étonnant.

12.4.1 Syntaxe

Jeu d'instructions

Le jeu d'instructions reconnu par BASM dans les versions 6 et 7 de Delphi couvre toutes les instructions des processeurs jusqu'aux Pentium 4 et Athlon XP. Ce n'est pas le cas des versions antérieures. Il n'y a pas nécessairement synchronisme parfait sur toutes les versions entre les jeux d'instructions suivants :

  Celui accepté par BASM.

  Celui utilisé par le compilateur.

  Celui reconnu par le désassembleur de débogueur.

  Celui annoncé par l'aide en ligne, parfois pessimiste ou un peu datée.

C'est malheureusement parmi ces instructions manquantes que figurent celles que nous aimerions bien utiliser. Les possesseurs d'anciennes versions peuvent souvent s'en sortir en codant à la main, en hexadécimal, à partir de la documentation AMD et Intel. C'est une solution très courante pour CPUID, RDTSC et autres opcodes sans arguments. Dans d'autres cas, c'est plus compliqué.

Récapitulons ce qu'annonce l'aide de Delphi 7, qui semble à jour : BASM supporte toutes les instructions des familles de processeurs Pentium , Pentium Pro , Pentium II , Pentium III , Pentium 4 , ainsi que les jeux AMD 3DNow! (depuis AMD K6 ) et AMD Enhanced 3DNow! (depuis AMD Athlon ).

La syntaxe générale de BASM rappelle celle de TASM, donc de MASM, avec utilisation de spécificités Delphi.

Commentaires

Les commentaires respectent la syntaxe de Delphi :

{ Ceci est un commentaire
qui continue et peut durer tout un bloc} 
// Commentaire jusqu'à la fin de la ligne
(* Comme pour les parenthèses*)

Les instructions ne doivent pas être coupées par des commentaires, qui en dehors de ce point peuvent être placés n'importe où :

{commentaire savant}  lea eax, L1
lea eax, dword ptr[L1] {commentaire savant}
mov eax, G1 {comment
aire savant} mov eax, dword ptr[G1]

Ces quatre lignes se compilent. À éviter malgré tout, à part la seconde. Retenir que le point-virgule ; n'est pas accepté pour introduire un commentaire.

Labels

Il existe en Delphi des étiquettes, des labels, devant comme les variables être déclarés en début de bloc. Ils sont les compagnons de l'instruction goto . Autant dire que leur utilisation est rarissime. Ils sont positionnables dans des blocs assembleur. Il est préférable d'utiliser des labels locaux, dont la portée est limitée au bloc asm...end; . Ils n'ont pas à être déclarés préalablement, et doivent commencer par le signe @ . Les bonnes raisons d'utiliser autre chose que des labels locaux sont rares.

Sauts

Delphi optimise les sauts sans nécessiter de directive particulière (le .JUMPS  de TASM est implicite). Donc, il choisit au mieux le type de JMP, et remplace si nécessaire un saut conditionnel JC[cible] par la séquence JNC[justapré] JMP[cible]. Le RET est toujours NEAR .

Directives de réservation mémoire

Coder une instruction non implémentée est le cas le plus fréquent d'utilisation des directives de réservation mémoire db , dw , dd , ainsi que dq  depuis Delphi 6, qui réservent 1, 2, 3 et 4 octets en mémoire. Tous réservent plusieurs mots de même taille, en les séparant par des virgules. Par exemple dw 0, 0, 0 réserve 6 octets (3 mots de 16 bits). De plus, db accepte des chaînes à la place d’une liste d’octets. Nous en parlerons un peu plus loin.

Avec l'assembleur intégré, cette réservation se fait dans le segment de code. S'il s'agit d'une donnée, son utilisation est bien compliquée, et pour un avantage douteux. L'exemple suivant fonctionne :

  jmp @suite
@var:
  dw $0000
@suite:
  mov word ptr [@var], ax

Dans la réalité, les variables, simples ou complexes, seront créées en Delphi par la directive var  et accédées par leur nom, comme le seront tous les objets accessibles depuis la fonction. Écrire un bloc var en début de procédure assembleur ne remet pas en cause son statut 100 % assembleur.

Mots réservés

Quand le compilateur trouve un mot qui est dans sa liste de mots réservés, il ne cherche pas plus loin. Donc, n‘attribuez surtout pas à une variable un nom parmi la liste suivante :

AH         BX         DI         EBX        ESP        OFFSET     SP
AL         BYTE       DL         ECX        FS         OR         SS
AND        CH         DS         EDI        GS         PTR        ST
AX         CL         DWORD      EDX        HIGH       QWORD      TBYTE
BH         CS         DX         EIP        LOW        SHL        TYPE
BL         CX         EAX        ES         MOD        SHR        WORD
BP         DH         EBP        ESI        NOT        SI         XOR

Cette liste est directement copiée depuis l'aide de Delphi 6. Delphi 7 en propose une version plus étoffée :

AH         CL         DX         ESP        mm4        SHL        WORD
AL         CS         EAX        FS         mm5        SHR        xmm0
AND        CX         EBP        GS         mm6        SI         xmm1
AX         DH         EBX        HIGH       mm7        SMALL      xmm2
BH         DI         ECX        LARGE      MOD        SP         xmm3
BL         DL         EDI        LOW        NOT        SS         xmm4
BP         CL         EDX        mm0        OFFSET     ST         xmm5
BX         DMTINDEX   EIP        mm1        OR         TBYTE      xmm6
BYTE       DS         ES         mm2        PTR        TYPE       xmm7
CH         DWORD      ESI        mm3        QWORD      VMTOFFSET  XOR

Il semble bien que certains mots réservés apparus dans la liste de Delphi 7 sont reconnus par Delphi 6. C'est par exemple le cas des mm0 ... mm7. Nous avons déjà signalé ce problème de synchronisation de l'aide.

Certains de ces mots sont de vrais pièges, en particulier ceux à deux lettres, ch par exemple, qui conviendrai bien pour un caractère. Or, un caractère mesure un octet, tout comme CL. Donc, là où vous allez mettre la variable cl , le registre CL convient également, du point de vue syntaxique. La compilation s’effectue ainsi sans réagir ; le bogue n’est dès lors pas immédiat à détecter. Si vous devez absolument utiliser un mot réservé, dans le cas d'ajout de code assembleur à un code Delphi préexistant par exemple, préfixez-le de l'ampersand : &cl .

Saisie des constantes numériques

La taille maximale d'une constante numérique est celle d'un mot de 32 bits signé ou non signé. Tout dépassement effectif entraîne une erreur de compilation, comme dans mov al, $10FA . En revanche,  mov al, $00FA est accepté. Une erreur aurait été préférable, mais ne nous plaignons pas, d'autres produits se contentent de tronquer les constantes trop grandes.

Par défaut, la saisie est interprétée en décimal. Il est également possible de saisir :

  En octal : suffixe O ou o , comme dans : 7247O . Entrée directe en octal impossible en Pascal.

  En binaire : suffixe B ou b , comme dans 01111011b . Il n'est pas possible d'insérer des séparateurs de quartets, espaces ou autres, ce qui serait plus pratique : 0111 1011b n'est pas accepté. Maximum 32 caractères. Les entrées directes en binaire ne sont pas acceptées en Pascal, même si sous Delphi 6 dans une déclaration de constante x: Longint = 111b; se compile sous Delphi 6, mais x prend la valeur décimale 111. Peut-être un bug ? Il est corrigé dans la version 7.

  En hexadécimal, suffixe H ou h , ou alors préfixe $ . De plus, la règle veut qu'une constante numérique débute par un chiffre ou le signe $ . D'où : $1234 et $FFFF mais 1234h et 0FFFFh . Seule la forme avec $ est acceptée en Pascal.

  En chaîne de caractères, maximum 4 caractères. La chaîne est, si nécessaire, complétée par des caractères NULL à gauche, donc par des 0 .

Essayons d'être logiques dans nos choix ; nous éviterons ainsi des commentaires :

and ax, 010000000000b , est-ce utile de préciser qu'il s'agit d'un masque sur le bit 14 ?

mov al, 'a' est suffisant, mieux que mov al, 61h // code ASCII du a dans AL .

mov  al, "\0" fait penser à un caractère de fin de chaîne. Voir remarque un peu plus loin.

Pour ce dernier exemple, si vous chassez le cycle, vous préfèrerez peut-être un xor al, al  //fin de chaîne dans AL , encore que, sur un Pentium, cela n’est pas crucial, peut-être même plus lent...

Saisie des chaînes de caractères

La saisie sous BASM de chaînes de caractères ne sera pas fréquente. Nous avons vu que les vraies chaînes de caractères seront déclarées sous Delphi, qui demande des apostrophes, et le # pour introduire un caractère de contrôle. Par exemple, STR := 'Nestor'#32'Burma'; affectera "Nestor Burma" à STR, 32 étant le code de l'espace. Les guillemets sont refusés.

L'aide en ligne de Delphi affirme que, en assembleur, les apostrophes et les guillemets sont acceptés de la même façon. Cela n'est pas tout à fait vrai. Testez ce code, en plaçant un point d'arrêt sur le JMP :

jmp @1
db "Nestor\0Burma"
db "Nestor", "\0", "Burma"
db 'Nestor\0Burma'
db 'Nestor',0,'Burma'
@1:
mov al, "\0"

Une fois sur le BP, après avoir constaté que le mov al, "\0" devient un mov al, 0 ou mov al, $00 , observez dans la zone mémoire le contenu de la zone de code à partir du premier db, sans oublier le $ devant l'adresse :

La mémoire réservée
figure a4.01 La mémoire réservée

Tout se passe comme si les chaînes entre guillemets étaient d'abord interprétées, puis le résultat mis en mémoire. De la même façon, nous constaterions que le \n est interprété 0Ah (Line feed, à la ligne) entre guillemets, et comme \n entre apostrophes. Il faudra donc être prudent ; mais rappelons que la déclaration de chaînes sous BASM n'a logiquement pas lieu d'être. Nous utiliserons plutôt les constantes caractères. Mais qu'en est-il de la légitimité d'un "\0" ou d'un "\n" ?

Les constantes de Delphi

Il y a deux façons de déclarer des constantes, avec et sans type :

const
   NbreCylindres = 80 ;
   PisteParCylindre = 18;
   y: Longint = 55;
   z: Longint = 12;

Les deux premières constantes n’ont pas de type. Leur rôle est en gros celui d’une macro : simple remplacement dans les expressions du nom de la variable par sa valeur, comme si celle-ci était saisie au clavier.

Les deux autres, y et z, sont de vraies variables, ici de type Longint, mais dont on interdit la modification. Une option de compilation permet d’ailleurs leur modification. Elles deviennent alors de simples variables initialisées. Elles sont compilées comme des variables, qu’elles sont. Leur valeur n’est pas supposée connue au moment de la compilation.

Expressions interdites :

mov eax, y + z //comme pour des variables
mov ax, y //pas le bon type
z := 44 ; //z est quand même déclaré const

Expressions autorisées :

mov eax, NbreCylindres * PisteParCylindre // = mov eax, 1440
mov ax, NbreCylindres * PisteParCylindre // même chose, pas de type

La séquence suivante fonctionne très bien :

mov eax, z
shl eax, 2
mov z, eax

z vaut maintenant 48, valeur qui peut bien sûr être utilisée par la suite. Pour une constante…

12.4.2 Les classes et les types d'opérandes – Opérateurs

Ce paragraphe traite des opérandes d'une instruction assembleur. La notion de mode d'adressage est supposée connue. C'est le problème du rapport entre ces opérandes et les variables (ou constantes) Delphi qui nous intéresse ici.

Un opérande peut être Registre , Référence mémoire ou Valeur immédiate (opérande source dans ce dernier cas), dans des tailles de 8, 16 ou 32 bits. Les opérandes Registre n'appellent pas de commentaire particulier.

Pour les deux autres, nous allons utiliser des valeurs numériques et des noms de variables et de constantes Delphi. Le problème est de savoir si une expression est plausible et, si oui, comment elle sera interprétée par Delphi. Pour le résoudre, il faut avant tout avoir une idée claire de ce que connaît le compilateur. Il existe en gros trois catégories de valeurs numériques :

  Les valeurs absolues, qui sont absolument connues à la compilation. Sur ces valeurs, les opérateurs s'appliquent sans restriction, puisque le résultat remplacera l'expression pour former une valeur immédiate.

  Les valeurs absolument inconnues au moment de la compilation : ce sont les contenus des registres et des emplacements mémoire.

  Les valeurs relogeables, qui seront connues à l'issue du travail du lieur. Par exemple l'adresse d'une procédure, utilisée dans un CALL ou un JMP, sera donnée par le lieur. Cette caractéristique entraîne des restrictions quand à l'utilisation des opérateurs.

C'est une bonne intuition de la logique de compilation et de lien qui permet de déterminer ce qu'il est possible d'attendre d'une expression donnée. La pratique et l'utilisation du débogueur à des fins de vérifications permettront de s'en sortir ; ce ne sera peut-être pas immédiat. Nous allons passer en revue les opérateurs les plus importants, puis étudier quelques exemples.

L'aide Delphi vous donnera un ordre de priorité des opérateurs, utilisez plutôt les parenthèses en cas de doute. Le compilateur vous en sera reconnaissant, et ne génèrera pas un octet de plus, même si le source est moins compact. De plus, il aura ainsi plus de chances de produire le code que vous espérez.

Le type d'une expression en assembleur est tout simplement sa taille en octets, exprimée par un des mots suivants, à utiliser dans les expressions de transtypage :

1    BYTE
2    WORD
4    DWORD
8    QWORD
10   TBYTE

Le type d'une valeur immédiate est 0.

 

Les opérateurs de BASM

Opérateur(s)

Description

AND, OR, XOR, NOT, SHR, SHL

Opérateurs logiques bit à bit. Ne s'appliquent qu'à une ou deux valeurs immédiates absolues, pour obtenir une valeur immédiate absolue.

HIGH, LOW

Renvoie l'octet constitué des 8 bits de poids respectivement fort et faible d'un mot de 16 bits, valeur immédiate absolue.

*, /, MOD, +, -

Multiplication, division entière et reste de la division entière (modulo), addition, soustraction. Valeurs immédiates absolues en entrée pour obtenir une valeur immédiate absolue. + et - peuvent simplement précéder une valeur, sans effet pour le +, inversion de signe pour le -.

+, -

Une des expressions peut être une valeur relogeable, la première dans le cas du -.

(....)

Pour fabriquer une sous-expression et forcer un ordre de calcul. Ne pas avoir peur d'en abuser.

&

L'identificateur qui suit est interprété comme un symbole utilisateur, même si c'est également un mot clé. Voir paragraphe Mots réservés .

[....]

Référence mémoire. À l'intérieur du crochet, se trouve une expression correspondant à une référence mémoire, pouvant contenir des registres (modes d'adressage) ou des constantes.

OFFSET

Valeur du déplacement de l'expression qui suit. C'est une valeur immédiate.

PTR

Opérateur de transtypage. Précédé d'un type, et suivi d'une référence mémoire, il désigne l'objet d'un certain type à cette adresse mémoire.

TYPE

Renvoie la taille en octets d'une expression.

:

Surcharge de segment. Permet de spécifier un autre segment que celui par défaut.

.

Sélecteur de membre de structure. Conduit le compilateur à additionner l'adresse de la structure et le déplacement du membre dans la structure.

Appliquons ces renseignements à quelques exemple, tirés de l'aide de Delphi. Pour tous les exemples, nous supposons :

const

  konst = 12;

var

  var1 : Integer;

 

mov eax,konst donne mov eax, 12 , donc 12 dans EAX.

mov eax, var1 donne mov eax,[adresse de var1] , donc valeur de DS:adresse de var1 dans EAX. La syntaxe mov eax, [var1] est identique.

mov eax,[konst] donne: mov eax,[12] , donc valeur à l'adresse DS:12 dans EAX.

mov eax, OFFSET var1 donne mov eax, adresse de var1 , donc déplacement de l'emplacement de var1 dans EAX.

Dans les second et quatrième cas, adresse de var1 est une constante (valeur immédiate) calculée à la compilation.

Delphi 7 ajoute les directives SMALL  et LARGE . Elles permettent de modifier par surcharge la taille d'adresse. Le code Delphi s'exeécute dans un environnement 32 bits, et ce fait ne peut être changé. Par défaut, la déplacement est  donc exprimé sur 32 bits. LARGE ne fait rien d'autre que confirmer le comportement par défaut :

mov eax, [$F000] et mov eax, [LARGE $F000] compilées en mov eax, $0000F000 .

SMALL permet de coder un déplacement sur 16 bits;

mov eax, [LARGE $F000] se compilera en mov eax, $0000F000 et nous pourrions constater la présence d'un préfixe 67h. Il est préférable en cas d'essais de ne pas exécuter cette instruction, sous peine de violation d'accès.

OFFSET et [] sont deux opérateurs complémentaires. Donc :

mov eax,konst est identique à mov eax,OFFSET [konst]

mov eax,var1 est identique à mov eax,[OFFSET var1]

Identique signifie que le même code machine sera généré.

La logique permet de prévoir ce qui sera refusé :

mov ax, var1 (non concordance des types).

et ce qui sera accepté :

mov eax, [12] (12 n'est pas typé, donc EAX impose son DWORD).

mov ax, WORD PTR [var1] (il faut transtyper var1 vers le type WORD).

Deux autres syntaxes pour un transtypage :

mov ax, WORD(var1) et  mov ax, var1.WORD

Vous pouvez maintenant revoir l'exemple de code donné en tout début de chapitre 5. Il devrait être un peu plus clair.

Enfin, Delphi interprète bien entendu tous les modes d'adressage. Ainsi :

mov eax, [konst+ ebx + edx]

sera un adressage de type déplacement + base + index.

BASM accède aux entités Delphi généralement sous la forme d'un pointeur 32 bits, accompagné éventuellement d'un déplacement de champ dans le cas d'entités structurées comme des classes.

Delphi 7 a ajouté deux directives permettant d'accéder aux méthodes virtuelles et dynamiques d'une classe, VMTOFFSET et DMTINDEX. Leur utilisation suppose une solide maîtrise de la programmation objet.

VMTOFFSET renvoie le décalage en octets de l'entrée de la table des pointeurs de méthodes virtuelles à partir du début de la table des méthodes virtuelles (VMT) :

call    DWORD PTR [edx + VMTOFFSET MaClasse.MethodeVirtuelle]

EDX contient l'adresse de la VMT. Elle s'obtient à partir du pointeur d'instance par exemple par :

mov eax, UnObjet

mov edx, [eax]

DMTINDEX renvoie l'indice dans la table des méthodes dynamiques de la méthode dynamique transmise. La méthode dynamique s'appelle par System.@CallDynaInst. (E)SI doit contenir l'indice récupéré à l'aide de DMTINDEX, et EAX le pointeur d'instance :

mov eax, UnObjet

mov esi, DMTINDEX MaClasse.MethodeDynamique

call System.@CallDynaInst

 

Un exemple complet d'utilisation de VMTOFFSET et DMTINDEX est fourni dans l'aide de Delphi 7.

Utilisation des registres

Quand du code assembleur est simplement inséré en ligne dans un flux du programme en Pascal, la seule précaution à prendre est de ne pas modifier les registres EDI, ESI, ESP, EBP et EBX. En cas d'utilisation, il faut les sauver et les restaurer par des PUSH et POP en début et fin du bloc de code.

Les registres de segment CS, SS, ES, FS, GS ne doivent tout simplement pas être manipulés.

Les registres EAX, ECX et EDX sont librement utilisables. Ils seront souvent suffisants pour de courtes insertions.

Au début d'un bloc asm , EBP pointe vers le cadre de pile actuel, ESP vers le haut de la pile, SS et DS sont normalement les segments de pile et de données. Aucun autre registre n'a de valeur garantie.

Il n'est pas courant de sauvegarder le registre EFLAGS. Nous avons néanmoins constaté un dysfonctionnement dans les fonctions d'affichage de Delphi quand elles suivent, même d'assez loin, une modification en assembleur du flag de direction DF.

12.4.3 Fonctions et procédures

Une procédure accomplit une tâche, en prenant ou pas des paramètres. Une fonction est une procédure qui renvoie une valeur. Étudier les fonctions revient donc à étudier également les procédures. Nous utiliserons ce dernier mot pour désigner les deux nuances. L'interface d'une procédure est définie par sa déclaration, ou prototype.

Écrire des procédures entièrement en assembleur est de loin plus efficace que de parsemer son code de petits blocs asm. Et nous avons la chance que ce soit accepté par Delphi. La contrainte est qu'il nous faudra gérer nous-mêmes un certain nombre de points un peu rébarbatifs.

Cadres de pile

Soit une procédure dans laquelle nous déclarons des variables locales, et les utilisons pour être certains que le compilateur les crée :

var
  T_essai: Array[1..100] of Byte;
asm
  ..
  mov byte ptr T_essai[51], al
  ..
end; //asm

 

Mettons un point d'arrêt vers le début de la procédure. Que soient ou non cochées les lignes Cadres de pile et Optimisation dans les options du compilateur, nous trouvons, avant notre propre code, le passage suivant :

push ebp
mov ebp, esp
add esp, -$64

($64 font 100). Et en fin, inséré avant le RET :

mov esp, ebp
pop ebp

Nous avons suffisamment présenté la notion de cadres de pile ; reportez-vous-y si ce code n'est pas clair. Visual C++ utilise LEAVE, pour le même résultat.

L'utilisation des registres suit les mêmes règles que pour les blocs asm : préservation de ESP et EBP, dans le cas où le cadre de pile n'est pas créé par Delphi, de EBX, EDI et ESI. Les registres de segment ne doivent pas être modifiés du tout.

Penser à préserver également la FPU : ne pas réduire la précision, par exemple, sans la restaurer ensuite.

Passage de paramètres

Un des problèmes sera de passer les paramètres et de fournir un résultat. Delphi propose diverses conventions d'appel :

Conventions d'appel sous Delphi

Convention

Paramètres passés par

Sens

Nettoyage

Usage

register

Registre (3)

De gauche à droite

Procédure

Général (efficace)

pascal

Pile

De gauche à droite

Procédure

Obsolète (compatibilité)

cdecl

Pile

De droite à gauche

Appelant

C/C++ et autres

stdcall

Pile

De droite à gauche

Procédure

API Windows

safecall

Pile

De droite à gauche

Procédure

API Windows

Nous utiliserons essentiellement register, qui est la convention par défaut. Bien entendu, tout ne sera pas toujours passé par les registres. Seuls trois registres (EAX, ECX et EDX) sont utilisés. Si une fonction est une méthodes de classe, EAX est un pointeur sur l'instance (self) ; il restera donc deux registres disponibles.

Attention : si un paramètre sur 8 ou 16 bits est passé par registre, il en utilise un en entier, mais les bits restants ne sont pas connus, pas mis à 0 par exemple.

Avant de bien s’imprégner de la psychologie de Delphi, il est bon, comme dans beaucoup d'autres cas, de tester à l'aide du débogueur. À ce sujet, un nom de paramètre sera toujours compilé comme s'il était le registre dans lequel il est arrivé, et ce jusqu'à la fin de la procédure. Très dangereux, réserver l'utilisation des noms de paramètres au passages par la pile. En revanche, si vous exploitez temporairement ce nom, un coup d'œil au code désassemblé peut confirmer immédiatement le registre utilisé :

Résumé presque complet
figure a4.02 Résumé presque complet

Dans le cas des conventions Gauche vers Droite (register et pascal) et si les paramètres sont passés par la pile, l'ordre des paramètres est le suivant : ils sont empilés dans l'ordre normal de lecture du prototype de la procédure. Et vice-versa pour les conventions d'appel de type Droite vers Gauche.

Le comportement des variables locales est sur bien des points, par rapport à la pile, comparable à celui des paramètres. Rappelons encore une fois que le traitement de la pile par EBP est expliqué ailleurs dans cet ouvrage.

La contrainte de l'utilisation des registres sera de sauvegarder les données en début de procédure.

Delphi passe ses paramètres par valeur ou par référence. Dans le premier cas, la valeur sera copiée dans le registre ou sur la pile. Dans le second, c'est son adresse.

Retour du résultat

Vous pouvez bien entendu passer en tant que paramètre un pointeur vers un résultat attendu, voire plus souvent, comme les API Windows, un pointeur sur une structure. Mais vous pouvez également renvoyer directement un résultat, et un seul. Le gros avantage est syntaxique : le nom de la fonction sera alors traité dans les formules comme une variable du même type que le résultat (c'est une R-Value).

Rappelons que sous Delphi, en Pascal, une variable Result  est créée automatiquement dans chaque fonction, et que ce mot peut être remplacé par celui de la fonction :

function essai(a, b, c, d: Integer): Integer;
begin
  Result := a + b + c + d;
  essai  := a + b + c + d;
end;

Les deux lignes sont équivalentes. Delphi 7 propose le symbole spécial @Result , qui correspond à l'intérieur d'une fonction au Result de Pascal. C'est une variable réservée dans la pile si elle est utilisée. Elle est copiée dans EAX juste avant le retour. Si elle n'est pas utilisée dans la fonction, le comportement est celui que nous allons voir, à savoir le retour dans EAX.

function test(i:Integer):Integer;register;
  asm
    mov eax, 24
  end;

test() renvoie 24.

function test1(i:Integer):Integer;register;
  asm
    mov @Result, 5
  end;
 
function test2(i:Integer):Integer;register;
  asm
    mov @Result, 5
    mov eax, 24
  end;

test1() et test2() renvoient 5.

 

Au niveau assembleur, le résultat est renvoyé dans AL, AX et EAX, selon sa taille de 8, 16 out 32 bits. Plus EDX:EAX pour un int64. Même les pointeurs, objets, classes, références de classes, et pointeurs de procédures, sont renvoyés dans EAX.

Les types dépendant de la FPU sont renvoyés dans SP(0).

Tout pourrait fonctionner de cette façon, mais… Mais il en va différemment pour tout ce qui ressemble à un tableau et ne rentre pas dans 32 bits : chaînes diverses, tableaux, structures, variants.

Voyons la fonction IntToBin du chapitre 5, qui renvoie une chaîne de caractères. Si celle-ci était une variable ordinaire, elle aurait été déclarée, utilisée puis détruite dans l'appelant. Il est donc assez logique de faire réserver une chaîne par l'appelant, et qu'il en envoie les clés à la fonction.

Cette clé s'appelle Result, et constitue un paramètre supplémentaire passé par l'appelant vers la fonction. C'est un pointeur vers une mémoire où l'appelant s'attend à trouver… un pointeur vers l'objet retourné.

Attention

En LHN, un pointeur est une variable

En langage de haut niveau, un pointeur est une variable . Plus exactement, un pointeur typé est une variable de type PTYPE, pointeur sur type TYPE.

Donc, déclarer PCh comme pointeur sur chaîne consiste à réserver une mémoire de la taille d’une adresse, qui pourra contenir l’adresse d’une chaîne. À la déclaration, cette variable pointeur n’est pas initialisée, ce qui représente un danger. Il est courant de lui affecter la valeur particulière NULL.

Initialiser un pointeur consiste, comme pour toute variable, à lui affecter une valeur : ici, l’adresse valide d’une chaîne. Les fonctions qui allouent de la mémoire renvoient le plus souvent une adresse.

Deux pointeurs différents peuvent pointer vers le même objet, puisque deux variables différentes peuvent avoir la même valeur.

Dans PTYPE, TYPE peut être lui-même un pointeur. Un PPChar sera un pointeur vers un PChar, lui-même pointeur vers un Char. Un PPChar sera initialisé quand le PChar sera créé, mais pas nécessairement initialisé lui-même.

En assembleur, un pointeur est synonyme d'adresse complète (SEGMENT:OFFSET) et tient sur 4 ou 6 octets.

Procédons à une simulation. Une fonction F, qui prend deux paramètres a et b entiers, doit renvoyer un objet string. Au début du film, l'appelant MAIN s'apprête à invoquer F pour obtenir une string :

MAIN déclare (en cachette) une variable pointeur de type Pstring PstrA, sans l’initialiser, ou mieux en l’initialisant à NULL.

MAIN récupère l’adresse de cette variable PstrA, en fait un paquet avec a et b, et envoie le tout vers F. PstrA est un paramètre caché, une initiative du compilateur.

F demande au système un objet string, d’une taille adéquate.

Le système obtempère, et fournit en retour à F l’adresse de cet objet string.

F va consulter le paramètre caché, y trouve une adresse. Il utilise cette adresse pour sauver l’adresse de l’objet string. Il fait tout cela très simplement, par un mov dword ptr param_caché, adresse_de_la_string .

F accomplit son travail sur la_string, à l’aide certainement de a et b, puis rend la main à MAIN.

MAIN retrouve PstrA initialisé à l’adresse de la chaîne fournie par F. Il en est en quelque sorte propriétaire.

Pointeurs dans MAIN et F
figure a4.03 Pointeurs dans MAIN et F [the .swf]

Cette démarche est conforme à la notion de niveau lexical vue au chapitre 6. Elle évite qu'une donnée créée entièrement dans F ne survive à la fonction. C'est MAIN qui initie la création de la string, mais il délègue une partie du travail restant à F. Nous sommes là dans une logique de langage de haut niveau ; c'est bien ce que nous souhaitons : intégrer notre code assembleur dans Delphi.

Voici une partie du code du chapitre 5 qui correspond exactement à cette opération :

mov  esi,ecx // Récupération de l'adresse de Result
.
.
.
mov eax, edx    // Longueur
// Allocation mémoire, pointeur sur la nouvelle AnsiString dans EAX
call System.@NewAnsiString
mov [esi], eax  // Adresse de la chaîne en sortie

 

12.5 Annexe E - Cross-assembleurs et microcontrôleurs

Si vous lisez ces lignes, c’est que vous êtes attiré par la programmation en assembleur sur PC. Professionnellement, mais peut-être en amateur, parce que vous avez une âme de bidouilleur et le désir de tout contrôler dans un développement. Là, vous êtes frustré. Un jeu d’instructions pléthorique, des temps d’exécution imprévisibles, un programme qui s’exécute aux instants où Windows le veut bien, donc une programmation temps réel impossible. Peut-être possédez-vous un aquarium, un train électrique ou un labo photo, la vraie photo qui empeste l’hydroquinone et l'hyposulfite. Si, de plus, vous étiez lecteur du Haut-parleur , aucun doute, ce chapitre est fait pour vous, et surtout les portes qu'il entrebâille, puisqu'il ne s'agit que d'une présentation.

Cross-assembleurs

Un cross-assembleur, comme un cross-compilateur, c'est tout simplement un assembleur qui ne s’exécute pas sur la machine cible. Nous pouvons d’ailleurs imaginer que la machine cible n’existe pas encore (il faut bien assembler les ROM quelque part), ou que ce soit une carte de gestion de porte de garage, aux capacités d’édition de texte fort limitées.

Un assembleur est un programme prenant en entrée un fichier texte et générant un fichier objet. Ce programme n’a aucune raison d’être écrit en assembleur. Il existe même des dizaines d’assembleurs,  rudimentaires ou non, écrits en Basic. Donc, dans leurs versions ligne de commande, porter un assembleur d’un environnement à un autre n’est pas un problème. Simplement, le cross-développement micro/micro, PC-MAC le plus souvent, présente peu d’intérêt.

Le fait est que le PC est une excellente machine de bureau, qu’il a envahi la planète ; il est donc souvent choisi comme terminal de saisie et de développement. Très certainement pour dégrossir des applications devant fonctionner sur de gros systèmes. Certaines remarques sur le respect ou non de la norme IEEE 754 par la FPU peuvent nous laisser supposer des prédéveloppements en Fortran, destinés à des applications s’exécutant sur des machines plus puissantes.

Nous trouvons du cross-assemblage vers d’anciennes machines 8 bits, à base de 6502 ou de Z80, souvent lié à des émulations de ces vieux coucous sympathiques. La seule motivation de ces exercices est le plaisir. À chacun ses soirées Casimir ou Goldorak.

L’industrie de l’électronique fait largement appel au PC en tant qu’outil de développement. Nous avons signalé que toute fonction logique, même très complexe, peut s’intégrer dans une simple puce. C’est par exemple le cas des chipsets de nos PC. Il existe des circuits logiques non terminés, un peu comme une (xx)ROM vierge. Ces circuits, les réseaux logiques programmables, sont constitués de milliers de portes logiques de base, les connexions entre elles restant à réaliser. Vu de l’extérieur, c’est donc un énorme tableau de connexions. Sur le PC, s’exécutent des programmes, des compilateurs, qui permettent de passer de fonctions logiques saisies graphiquement à une liste de connexions à obtenir. Certaines versions, parfois onéreuses, de ces circuits sont reprogrammables. Une fois obtenu une programmation adéquate, à l’issue d’une phase de développement/déboguage, une version industrielle à faible coût du circuit pourra être mise en production. Il existe également les DSP, processeurs digitaux de signal, dont il faut programmer les fonctions.

Mais, le domaine qui nous intéresse particulièrement est celui des microcontrôleurs. Nous savons qu’il s’agit ni plus ni moins que de microprocesseurs dotés de mémoire, d’entrées/sorties, et plus généralement de la circuiterie annexe, permettant de concevoir un système opérationnel avec très peu de composants externes. Certains de ces circuits ont accédé à la célébrité du fait de l’influence bénéfique qu’ils avaient sur la qualité de l’image et du son émis par certaines chaînes payantes du réseau hertzien. Il semble que d’autres jouent un rôle un peu analogue par rapport à des consoles de jeu.

Le développement autour de ces composants se fait généralement sur une plaquette d'essai, soit standard soit développée pour le projet. Il existe généralement, d'un même modèle, des versions de développement et des versions de production. Seules les premières nous intéressent ici. Il en existe plusieurs types, différents par la façon d'écrire et d'effacer la mémoire programme. Il a même existé des modèles piggy back, affublés sur leur dos d'un support pour un circuit d'EPROM.

La solution la plus confortable pour le développeur, devenue courante et bon marché, est d'avoir sur un seul circuit de test une version Flash EPROM, programmable sur place. Cette carte est en liaison série avec un PC. Sur le PC fonctionne un cross-assembleur plus ou moins évolué, qui génère une image binaire de la ROM. Cette image est instantanément chargée sur la carte, et le microcontrôleur est reprogrammé. Le déboguage est ainsi rendu confortable. Ce qui est presque dommage, puisque c'était un des derniers domaines où il fallait réfléchir avant de coder.

Dans cette famille des micro-contrôleurs, un produit sort nettement du lot : la gamme des PIC du fabricant Microchip.

Les PIC de Microchip

Outre l'étendue de la gamme et la qualité des produits, le dynamisme commercial de Microchip est pour beaucoup dans le grand succès de la gamme PIC. En plus, bien entendu, de la qualité des circuits et de leur prix particulièrement attractif.

Microchip a pris le parti de ne pas réserver son support aux seuls professionnels. Le site (en anglais) est particulièrement bien fait, malgré un grande richesse : presque tout est téléchargeable et, à une ou deux exceptions près, gratuitement.

Page d'accueil de www.microchip.com
figure a5.01 Page d'accueil de www.microchip.com

Sur le site, figurent des ressources pédagogiques, toute la documentation, au format PDF, de tous les circuits disponibles, et enfin et surtout le logiciel de développement intégré MPLab. Ce logiciel possède un mode de simulation qui permet même de s'amuser, avant d'avoir fait l'acquisition de quoi que ce soit. C'est du bonheur…

De plus, il existe sur l'Internet en français un bonne activité autour de ces circuits, et au moins un bon site de référence. En revanche, pas de microchip.fr, semble-t-il.

Pour caractériser cette famille de circuits, nous dirons qu'ils sont à jeu d'instructions réduit RISC, architecture Harvard, et le nombre de références qui peut sembler immense est tempéré par une grande modularité : si une automobile propose 4 options indépendantes, cela donne 16 modèles. S'il y a 16 couleurs de peinture, vous arrivez à 256 références. 3 motorisations, 768. Les PIC ne sont disponibles qu'en noir, mais avec de nombreuses options.

L'architecture Harvard, rappelons-le, sépare physiquement le programme des données. Dans les données, sont compris tous les registres, dont les entrées/sorties.

Pour pouvoir réellement jouer avec ces circuits, il faudra résoudre le problème de la carte de tests et éventuellement du programmateur. Pour ce dernier point, il semble qu'il soit devenu inutile grâce aux versions Flash. En plus des cartes proposées par Microchip, il existe, de-ci de-là dans le commerce et dans la presse, des cartes d'évaluation. Nous avons relevé chez Electronique Diffusion un kit PICCOLO : le circuit imprimé est proposé pour 2€30, le total devrait se situer entre 20 et 30 €. Ces cartes peuvent facilement être réalisées par n'importe quel amateur un peu équipé : celle dont nous allons parler est faite en procédé classique, transferts en simple face, et reste aérée. Elle mesure 100 x 160 mm et embarque même le transformateur d'alimentation.

Nous n'allons bien entendu pas détailler ces circuits. Pour illustrer cette présentation, nous avons utilisé la version 5.70 de MPLab. Il s'agit d'une version compatible Windows 3.1, mais qui fonctionne parfaitement sous Windows XP. Nous ne l'avons toutefois testée qu'en simulation sous cet environnement. Une version 6 est disponible, du vrai Windows cette fois-ci ; mais, pour l’instant, tout les PIC n’ont pas été implémentés.

Nous avons utilisé du matériel pédagogique en provenance de l'école Don Bosco, à Tournai en Belgique, avec l'autorisation de l'auteur, Jean-François Dedecker, professeur d’électronique. Qu’il en soit remercié. Nous reproduisons pour information les schémas d’implantation et électronique de cette carte ; vous pourrez juger de la simplicité. Seuls deux circuits actifs sont à ajouter, et encore s’agit-il des transceiver RS232, et du port série synchrone, utilisable en I2C par exemple.

Schéma de principe
figure a5.02 Schéma de principe [the .swf]

 

Schéma d’implantation
figure a5.03 Schéma d’implantation

Le circuit choisi est la référence 16F874. C’est un circuit Flash, en boîtier DIL 40 (à l’ancienne, le même que le 8088). Malgré ces caractéristiques, il est proposé à 13 € chez un revendeur grand public de province. Résumons ses caractéristiques :

  Jeu d’instruction RISC, 35 instructions seulement, effectuées en un seul cycle, deux pour les sauts. Horloge jusqu’à 20 MHz.

  368 mots de 8 bits de données en RAM, 256 mots de 8 bits de données en EEPROM. Cela peut sembler peu, mais c’est bien dans ce type d’applications que la RAM pourra contenir les résultats de calculs ; l’EEPROM, sauvegardée, permettra de retrouver les valeurs importantes au redémarrage.

  Le programme occupe une zone de mémoire Flash de 8 Kmots de 14 bits. La taille du mot varie avec les modèles de la gamme ; chaque instruction est ainsi codée sur un seul mot.

  Le code peut être protégé contre la curiosité par un mot de passe. Un circuit de chien de garde est prévu en interne. En gros, si le processeur fonctionne normalement, il va effectuer une action régulièrement. Si cette action n’est pas effectuée, il peut en être déduit qu’il est bloqué. Il sera par exemple relancé par un reset.

  En termes d’entrées/sorties, il embarque 3 timers, des ports parallèle, un port série asynchrone, un port série synchrone. Il peut facilement gérer une liaison via tel ou tel protocole industriel de terrain. Puisque nous y sommes, remarquons que ce circuit sera tout à fait adapté pour rendre intelligent un capteur. Il gère de plus des E/S analogiques et la modulation de largeur d’impulsion, PWM.

Pour plus de détail, ainsi que la cartographie mémoire relativement compliquée, reportez-vous au CD-Rom et au fichier PDF adéquat.

Pour ce circuit et cette implantation, nous disposons d’un certain nombre de programmes débogués. Nous en choisissons un, pour sa simplicité. L’intérêt de la démarche est de tester sous simulateur un programme qui n’a pas été écrit dans cette optique.

;**********************************************************************
;* DEDECKER Jean-François                                             *
;*          Programme de test                                         *
;*          allumage d'une LED et d'un buzzer                         *
;*          sur appui d'une touche                                    *
;*                                                                    *
;*                                                     08/02/02       *
;**********************************************************************
;
;
;Définitions et inclusions
;*************************
 
        list    p=16f874, c=90, n=60
        include "p16f874.inc"
BUZ     equ     0
 
;programme principal
;*******************
 
        org 0
        goto 5
        org 5
 
debut
        banksel TRISB           ;se placer dans la bonne page
        movlw   0H              ;mettre
        movwf   TRISB           ;      le port b
        banksel PORTB           ;                en
        clrf    PORTB           ;                   sortie
 
        banksel TRISC           ;se placer dans la bonne page
        bcf     TRISC,BUZ       ;mettre le buzzer en sortie
                                ;(on aurait pu faire un BUZ equ 0)
 
waiton
        banksel PORTC           ;se placer dans la bonne page
        btfsc   PORTC,2         ;tester si le bouton est appuye
        goto    waiton          ;si non, retester
        call    allum           ;si oui, allumer led et buzzer
                                ;et attendre que le bouton soit relache
waitof
        banksel PORTC           ;
        btfss   PORTC,2         ;tester si le bouton est relache
        goto    waitof          ;si non, retester
        call    eteint          ;si oui, eteindre led et buzzer
        goto    waiton          ;et attendre que le bouton soit appuye
 
allum
        banksel PORTB           ;
        bsf     PORTB,0         ;allume la led 0
        bsf     PORTC,BUZ       ;allume le buzzer
        return
 
eteint
        banksel PORTB           ;
        bcf     PORTB,0         ;eteint la led 0
        bcf     PORTC,BUZ       ;eteint le buzzer
        return
 
 
        end

Les langages assembleur, c’est un peu comme les chats et les chiens : nous passons notre temps à en commenter les différences, mais en réalité c’est très ressemblant. Celui-ci fait presque exception à première vue.

Le vecteur RESET est à 0, d’où le goto 5 à cette adresse.

Les tests btfss – qui se lit Bit Test du registre F et Saute (skip) si Set (à 1) –, par exemple, sont un peu particuliers : ils ne font rien ou ne sautent qu’une instruction. C’est ce qui explique qu’aucun opérande n’indique d’adresse de saut. Les fonctions test et saut sont ainsi séparées.

Lançons MPLab, puis chargeons simplement, par File / Open , button.asm . Vérifions les options dans Options / Development Mode . Nous devons choisir le PIC16F874, ainsi que valider l’outil simulation :

Paramétrages de base
figure a5.04 Paramétrages de base

Négligeons les avertissements quand nous cliquons sur OK . Même sans avoir créé de projet, allons dans Project / Build Node  ; dans la fenêtre qui apparaît, paramétrez conformément à la capture d’écran :

Options de construction
figure a5.05 Options de construction

La compilation se termine sur un joyeux Build completed successfully . Il ne faut pas se laisser troubler par les deux messages qui ne sont que des warnings.

Examinons maintenant de plus près la barre d’outils en dessous de la barre de menus. D’abord, se souvenir de Windows 3.1 : point d’info-bulles d’aide, mais une barre d’état en bas de la fenêtre. C’est là qu’apparaît un résumé du rôle du bouton que survole la souris.

Le bouton le plus à gauche, au symbolisme imprécis, sert à basculer la barre d’outils d’une configuration à une autre, lui-même étant de toutes les configurations. Sinon, cela ne peut pas marcher. Choisissons-en une qui comprenne au moins les boutons ROM, RAM, SFR et les traces de pas. Cliquons sur SFR et RAM, ou même simplement les registres SFR, dans un premier temps. Arrangeons les fenêtres au mieux, puis commençons à nous promener dans le code source. Pas à pas, donc par les boutons traces de pas. Vous avez de façon habituelle le choix entre les deux modes de pas à pas : tracer dans les CALL ou pas. Découvrez le fonctionnement du programme et de MPLab ; il n’y a pas de grosse difficulté :

En plein déboguage
figure a5.06 En plein déboguage

Vous aurez sans doute envie de mettre en commentaire, comme sur la capture d’écran, la ligne goto waitof . Faites-le donc, et acceptez la reconstruction qui vous sera proposée. Vous voyez s’allumer et s’éteindre led et buzzer.

Il y a beaucoup de choses à découvrir dans ce programme. Remarquez que le déboguage de ce type d’applications de cette manière n’est qu’un minimum. Vous découvrirez, dans les menus, l’aide, la documentation, qu’il existe des produits logiciels et matériels permettant d’émuler et de déboguer sur carte l’ensemble de la gamme.

Voilà, il est maintenant temps de mettre le fer à souder en chauffe et d’aller chez votre pucier habituel faire l’emplette de quelques composants. Là au moins, vous ne vous plaindrez pas de ne rien contrôler.

Envoyer un message à l'auteur

se connecter pour pouvoir écrire

Ce site est créé et maintenu (laborieusement) par Pierre Maurette
hébergé par 1 & 1