Obligement - L'Amiga au maximum

Vendredi 06 juin 2025 - 12:16  

Translate

En De Nl Nl
Es Pt It Nl


Rubriques

Actualité (récente)
Actualité (archive)
Comparatifs
Dossiers
Entrevues
Matériel (tests)
Matériel (bidouilles)
Points de vue
En pratique
Programmation
Reportages
Quizz
Tests de jeux
Tests de logiciels
Tests de compilations
Trucs et astuces
Articles divers

Articles in English


Réseaux sociaux

Suivez-nous sur X




Liste des jeux Amiga

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z,
ALL


Trucs et astuces

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Glossaire

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Galeries

Menu des galeries

BD d'Amiga Spécial
Caricatures Dudai
Caricatures Jet d'ail
Diagrammes de Jay Miner
Images insolites
Fin de jeux (de A à E)
Fin de Jeux (de F à O)
Fin de jeux (de P à Z)
Galerie de Mike Dafunk
Logos d'Obligement
Pubs pour matériels
Systèmes d'exploitation
Trombinoscope Alchimie 7
Vidéos


Téléchargement

Documents
Jeux
Logiciels
Magazines
Divers


Liens

Associations
Jeux
Logiciels
Matériel
Magazines et médias
Pages personnelles
Réparateurs
Revendeurs
Scène démo
Sites de téléchargement
Divers


Partenaires

Annuaire Amiga

Amedia Computer

Relec


A Propos

A propos d'Obligement

A Propos


Contact

David Brunet

Courriel

 


Programmation : Initiation à l'assembleur 680x0 : la représentation des nombres en informatique, les instructions de calculs
(Article écrit par Victorien Ferry et extrait de GuruMed.net - juin 2003)


Avant de voir la description des instructions qui opèrent des calculs mathématiques sur les registres, il est incontournable d'avoir une idée précise de la façon dont ceux-ci représentent leurs valeurs, et ce qui se passe quand on réalise un calcul dessus.

Souvent, les débutants en assembleur restent bloqués parce qu'ils n'ont pas compris le fonctionnement exact d'une opération. De plus, en assembleur, on manipule des registres ou des zones mémoire en tant que champs de bits, leurs types étant totalement implicite (valeurs entières, signé ou pas, valeurs réelles flottantes, textes...), contrairement aux autres langages où l'on sait toujours le type exact d'une variable. Pourtant, vous verrez que quiconque sait effectuer une addition, une soustraction, une multiplication, une division et une opération logique en sait assez pour comprendre ce qui suit.

Vous devez aussi avoir en tête ce vocabulaire mathématique :
  • nombre entier : nombre qui n'a pas de virgule.
  • nombre réel : nombre qui peut avoir des chiffres après la virgule.
  • nombre naturel : on dira non signé ; qui est forcément positif.
  • nombre relatif : peut être positif OU négatif.
Nous avons vu que le 680x0 possède huit registres de données parfaits pour réaliser des calculs, notés de d0 à d7. Chacun contient (est une suite de) 32 bits (ou quatre octets).

Représentation d'un registre 32 bits sous 680x0

registre 32 bits 680x0

On voit que, par convention décidée par le constructeur Motorola à l'époque, les bits sont notés de 0 à 31 et sont dessinés de droite à gauche, du bit 31 (dit "bit de poids le plus fort") vers le bit 0 (dit "bit de poids le plus faible"). On verra plus tard pourquoi.

Quand on fera du 680x0, on pourra désigner tel ou tel bit dans un registre par son numéro de 0 à 31. On ne peut donc pas se tromper. Par exemple, l'octet de poids faible désigne les bits 0 à 7 inclus. Certaines instructions assembleurs utilisent ces numéros pour désigner un bit donné (comme "btst", qui teste un bit). Notez toutefois que c'est la convention 680x0, par exemple en PowerPC, on note les bits dans l'autre sens (bit 0 poids fort).

Représentation de nombres entiers positifs - Ce n'est pas compliqué ! Bascule mec !

Un bit est une bascule. Cela signifie qu'il peut prendre deux formes notées 0 ou 1. Génial. Mettons-en deux côte-à-côte. Chacun peut designer 0 ou 1, ça fait quatre expressions possibles : 00, 01, 10 et 11.

On peut vite en conclure que si on a un champ de "n" bits, on peut définir "2 puissance n" façons de placer ces bits à 0 ou à 1. Eh bien, figurez-vous qu'en comptant en "base 2", on peut lire dans un champ de "n" bits, les valeurs entières positives allant de zéro à "(2 puissance n)-1" (pour deux bits, on peut exprimer de 0 à 3).

Qu'est ce qu'une base ? Je vous renvoie à votre programme de cours préparatoire. Par exemple, dans la vie courante, c'est plutôt la base 10 qui est utilisée (décimale), et qui définie 10 chiffres possibles de 0 à 9 pour un "digit" (chiffre exprimé) : le principe de compter avec une base, c'est que quand ça dépasse, hou là là, il faut une autre colonne à droite. Exemple en décimal : 9+1 = 10 ; 99+1 = 100.

Dans chaque nouvelle colonne à droite, le chiffre est à multiplier par "10 puissance le numéro de la colonne", la première colonne à gauche étant numéroté zéro. Puis on additionne le tout pour avoir la valeur. C'est pareil en binaire (binaire = base 2), sauf que les chiffres sont à multiplier par "2 puissance le numéro de la colonne".

On peut donc dire qu'en binaire, le bit 0 "vaut" 1, le bit 1 "vaut" 2, le bit 2 "vaut" 4, etc. Et voilà. Vous devez déjà être en mesure de faire des conversions d'expressions binaires vers une expression décimale.

Pour des conversions d'expression décimale vers binaire, la méthode est simple : on part des valeurs des plus gros bits binaires à droite, on regarde si notre nombre décimal à convertir est plus grand, si oui, on soustrait la valeur de ce bit de notre nombre, et on allume ce bit, et on recommence avec le prochain bit de poids faible à gauche avec le reste de cette soustraction : par exemple, 146 est compris entre 0 et 255 donc 8 bits suffisent à l'exprimer. Le bit de poids fort vaut 128, donc :
  • 146-128 = 18.
  • Ensuite, on a le bit qui vaut 64. 18 est plus petit : on passe.
  • Ensuite, on a le bit qui vaut 32. 18 est plus petit : on passe.
  • Ensuite, on a le bit qui vaut 16 : 18-16 = 2
  • Au final, on obtient : 146 = 128+16+2
  • L'expression binaire de 146 sur 8 bits est donc : %10010010
Numéro du bit 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Valeur du bit 32 768 16 384 8192 4096 2048 1024 512 256 128 64 32 16 8 4 2 1

Binaire Décimal Hexadécimal
%00000101 5 $05
%10000000 128 $80
%10010010 146 $92
%00001111 15 $0F
%00001001 9 $09
%11111111 255 $FF
%10011010 154 $9A
%00000011 3 $03

Y a-t-il d'autres façons de coder des entiers positifs ? vive la diversité. (chapitre facultatif)

Nous en sommes seulement à définir une expression pour des nombres entiers positifs, tout devrait être simple, mais il faut noter que d'autres codages sont possibles pour des entiers positifs, dont le "décimal codé binaire" (BCD in english), je ne m'étendrais pas, ça consiste à prendre les bits quatre par quatre : on peut alors définir 16 valeurs avec chaque, mais voilà, on dit qu'on se sert que des 10 premières valeurs possibles (0 à 9), et donc les valeurs 10 à 15 sont impossibles.

A quoi ça sert ? À avoir un champ de bits qui explicite des nombres décimaux, en base 10 au lieu d'être en base 2. Donc, avec 32 bits, on a huit nombres possibles en base 10, ce qui fait 100 000 000 nombres différents. Cette représentation est utilisée par des vieux processeurs dans les calculatrices. Pourquoi cela nous concerne-t-il ? Parce que jusqu'au 68030, nous avons des instructions telles que ABCD qui travaillent en BCD ! (ABCD=addition BCD). Mais tout ceci disparaît dès le 68040 et, de toute façon, ça rame et ça craint.

Oubliez ce chapitre, le BCD craint et n'existe pas.

Quelques trucs pour bien maîtriser son domaine

Quand vous aurez à choisir un type de données pour une variable, il faudra bien réfléchir à bien définir le nombre d'octets nécessaires en fonction de vos besoins. Vous ne pourrez pas exprimer "256" avec un seul octet, ça s'arrête à 255. En assembleur, 255+1 en 8 bits donne 0, plus une information comme quoi, il y a dépassement sur le dernier calcul. Et ainsi de suite, sur 16 bits, en non signé, vous pouvez exprimer de 0 à 65 535, pas au-delà. Il faut donc savoir exactement, sur un nombre de bits donné, "le domaine de variation" des valeurs possibles. 32 bits peuvent représenter les valeurs de zéro à "4 gigaoctets -1", je peux vous le dire du tac au tac. Comment ? En informatique, on parle de kilo qui représente "2 puissance 10=1024" (et en aucun cas 1000). De même, mégaoctet représente "2 puissance 20" et gigaoctet "2 puissance 30". Si vous connaissez cette règle mathématique ("^" signifie "puissance") :

(2 ^n) x (2 ^m) = 2 ^(n+m)

...vous devinez que 2^32 = (2^30) x (2^2). Soit 1 Go multiplié par 4, soit 4 Go.

Exercice : quelle est la valeur entière maximum exprimable avec 24 bits ? Facile : 2^24 = 2^4 x 2^20 = 16 mégaoctets. Donc la réponse est : "16 mégaoctets moins un".

Si vous connaissez votre table de puissance de 2, multiplier une puissance de 2 avec une autre est aussi très facile pour les mêmes raisons : 64x512 semble difficile à réaliser de tête, mais : 2^6 x 2^9 = 2^(6+9) = 2^15 = 32 768... rapide !

Et vous voilà devenu le nouveau Hans Rudiguer !

Comment le 680x0 voit-il les nombres entiers relatifs ? La magie du codage "complément à 2"

En informatique, il existe deux moyens de définir des entiers négatifs. Le premier ne nous intéresse pas en 680x0 ou peu et a pour nom "valeur absolue plus signe" : ça consiste à utiliser le bit de poids le plus fort pour indiquer si le nombre est négatif, et le reste des bits pour exprimer la valeur entière à multiplier par -1 ou pas. Dans ce codage, pour "n" bits, on aurait le domaine de variation suivant :

de : -(2^(n-1) -1) jusqu'à : +(2^(n-1) -1)

(sur quatre bits, on aurait de -7 à 7)

Il y a énormément de désavantage à utiliser ce codage, le moindre étant qu'on peut exprimer "-0" et "+0", ce qui est stupide.

Non, tous les processeurs sérieux travaillent avec des entiers relatifs en "complément à 2" : de quoi s'agit-il ? D'abord, souvenez-vous de vos cours de primaire, où on vous faisait faire des soustractions à la main, colonnes par colonnes, avec des retenues à reporter sur la colonne suivante. En binaire, c'est pareil. Quand on opère une soustraction de 1 sur le nombre 0 en binaire, on obtient tous les bits à 1, et le bit spécial qui avertit d'un dépassement s'allume (il se trouve dans un registre spécial) :

%00000000 - %00000001 = %11111111

Lu en tant que non signé, tous les bits à 1 représentent le plus grand nombre possible (255, le compteur a tourné, mais à l'envers). Eh bien, en tant que signé "complément à 2", cela veut dire "-1". Si on refait -1 sur -1 :

%11111111 - %00000001 = %11111110

"%11111110" exprime -2 en signé, ou 254 en non signé.

On l'aura compris, en "complément à 2", un nombre négatif commence par un ou plusieurs "1". Voici le nombre signé le plus petit sur 8 bits : -128 = %10000000. Et voici le plus grand : 127 = %01111111.

En "complément à 2", sur "n" bits, le domaine de variation est donc :

de : -(2^(n-1)) jusqu'à : +(2^(n-1) -1)

(sur quatre bits, on aurait de -8 à 7)

On a vu que le type d'un champ de bits (registre, mémoire) n'était pas défini en assembleur, donc comment va-t-on définir qu'un registre représente un signé ou un non signé ? Par les instructions que l'on va utiliser. Par exemple, les instructions de multiplications et de divisions ont des versions signées et non signées (Muls, Divs sont signées ; Mulu, Divu sont non signées).

L'énorme avantage du complément à 2, c'est que les additions et soustractions sont les mêmes pour du signé ou du non signé. Simplement, la notion de dépassement ("overflow") n'intervient pas au même "endroit".

Note : l'instruction "neg" réalise une multiplication par -1 en complément à 2 rapide :

neg.w d0

Un truc : comment le faire sur papier sans s'embêter ? Il suffit d'inverser tous les bits et de faire +1, ça marche dans tous les cas. Voici donc un équivalent un peu plus lent de "neg.w d0" :

not.w d0
add.w #1,d0

Dernière note : remarquez que dans une représentation "valeur absolue + signe" comme en "complément à 2", tester le bit de poids fort indique si le nombre est négatif ou positif (pour tester cela, une instruction existe de toute façon : "tst").

Changement de format d'un entier relatif. Complément à 2, la suite

Nous savons maintenant que le type d'un registre en assembleur est implicite, et dépend de ce que l'on en fait avec les instructions. Le format (".b", ".w", ".l") permet de définir 8, 16 ou 32 bits, et l'usage d'instructions signées ou pas définissent sa nature.

Au cours d'un programme, il arrive que parfois on ait besoin de changer le format d'un nombre, signé ou pas. Par exemple, additionner un nombre exprimé sur 8 bits signé dans la mémoire, sur un nombre exprimé en 16 bits signé dans d0.

move.b (a0),d1 ; ceci lit un octet à l'adresse a0 et le met dans d1
add.b d1,d0 ; faux

Première erreur possible : la bonne valeur est dans d1, mais l'addition est en ".b" alors qu'on a dit que la valeur de d0 est sur 16 bits (".w") : l'addition risque d'être fausse.

move.b (a0),d1 ; ceci lit un octet à l'adresse a0
add.w d1,d0 ; ceci est archi-faux. Bogue

Qu'est-ce qui ne va pas ? "move.b" a bien rempli son rôle en remplissant les 8 bits de poids faible de d1, mais n'a pas touché les 24 autres bits de poids fort, qui peuvent contenir n'importe quoi. Quand bien même, "add.w" n'utilise que 16 bits de poids faible, donc une instruction "clr.w" (clear) peut mettre à 0 ces 16 premiers bits avant le "move.b" :

clr.w d1 ; efface les 16 premiers bits de d1
move.b (a0),d1 ; rempli les 8 premiers bits de d1
add.w d1,d0 ; toujours faux

On est sûr que d1 est constitué des bons huit premiers bits et que les huit suivants sont vides. Ceci marcherait si on avait dit que les 8 bits en mémoire étaient non signés. Mais on a dit qu'ils étaient signés. Voici la solution :

move.b (a0),d1 ; charge donnée signée en 8 bits
ext.w d1 ; extension signée de 8 bits vers 16 bits
add.w d1,d0 ; addition 16 bits

Pour passer une valeur signée en complément à 2 vers un format plus grand, une instruction spéciale est nécessaire. "ext.b" va remplir correctement nos 8 bits de poids fort, soit avec des 1, soit avec des 0, selon que l'expression "8 bits" est négative ou positive, pour que le sens "signé" soit gardé. Ces autres formes d'extensions sont possibles :

ext.w dx ; extension signée de 8 bits vers 16 bits (.b->.w)
ext.l dx ; extension signée de 16 bits vers 32 bits (.w->.l)
; à partir du 68020 :
extb.l dx ; extension signée de 8 bits vers 32 bits (.b->.l)

Pour faire des conversions de nombres signés vers un format plus petit, il n'y a pas d'instruction à utiliser, mais il peut y avoir des dépassements, puisqu'on "coupe" des bits de poids fort.

De la représentation hexadécimale et de son intérêt

Vous connaissez la représentation décimale, nous avons vu la représentation binaire ou les bits sont directement visibles. En assembleur 680x0, on les note précédés d'un "%". Exemple :

move.b #%01010101,d0

Familiarisons-nous maintenant avec l'hexadécimal : en assembleur 680x0, un chiffre exprimé en hexa se précède d'un "$". Cette écriture est équivalente à la précédente :

move.b #$55,d0

Il s'agit en fait d'écrire en base 16, les chiffres possibles étant : 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e et f (donc avec f=15). Donc "$0f" signifie "15", et "$10" signifie "16". L'hexadécimal est souvent utilisé quand on débogue, par exemple pour donner le contenu d'un registre, ou pour montrer une adresse mémoire. Seulement huit chiffres sont nécessaires pour exprimer 32 bits, chaque chiffre exprimant exactement quatre bits :

move.l #$84fa8bc5,d0 ; rempli 32 bits
move.l #$00010000,d0 ; 65 536 = 64 kilo
move.l #$0000ffff,d0 ; 65 535 = 64 kilo -1

Le décimal permet de se faire une idée de l'ordre de grandeur, mais pas de la position des bits. Le binaire permet de se faire une idée de l'ordre des bits, mais pas de l'ordre de grandeur. L'avantage de la notation hexadécimale, c'est qu'elle permet de se faire rapidement une idée de l'ordre de grandeur mais aussi de la position des bits. N'oubliez pas aussi qu'il s'agit de trois notations qui désignent la même chose. Je ne veux pas entendre de questions du genre : "je dois faire un "move.l" en décimal ou en hexadécimal ?"

Les instructions de multiplications

Il existe deux instructions pour la multiplication : "mulu" pour multiplier deux valeurs non signées ("u" pour "unsigned", ne pouvant prendre une valeur négative), et "muls" pour la multiplication signée (avec résultat signé donc).

muls.w d0,d1 ; d1 = d0*d1
muls.w memoire,d1 ; d1 = memoire*d1 (cette forme d'adressage est possible)
muls.w (a0),d1 ; celle-là aussi (".w" indiqué par a0) ;)
muls.w #valeur,d1 ; cette forme immédiate aussi

Comme d'habitude, en 680x0, c'est le registre de gauche qui reçoit le résultat. Maintenant, la chose importante à retenir : "muls.w" réalise la multiplication de deux expressions 16 bits, et met le résultat sur 32 bits. Donc, les 32 bits de d1 sont modifiés par "muls.w d0,d1". Pourquoi ? Parce que, de part le mécanisme de la multiplication (et quelle que soit la base utilisée), si vous multipliez un nombre de "n" chiffres avec un nombre de "m" chiffres, il faut prévoir (n+m) chiffres pour exprimer le résultat (la crise mystiiique !). Donc pour "mulu/s.w" : 16+16=32. Pour ces raisons, "muls.w" ne peut pas créer de dépassement. :)

Note importante : si on écrit dans un source "muls d0,d1" sans préciser de format de taille (".b", ".w", ".l"), le format ".w" est toujours utilisé à défaut. C'est en fait vrai avec toutes les instructions 680x0 par convention, ".w" (16 bits) est à défaut. Je conseille fortement de toujours spécifier ce format : certains assembleurs (Devpac) peuvent forcer le format à défaut en ".l". De plus, "muls.l" n'apparaît que depuis le 68020.

muls.l d0,d1
mulu.l d0,d1

On l'aura compris, ces instructions réalisent une multiplication de 32 bits x 32 bits vers un résultat... en 32 bits dans d1. Il peut donc y avoir ou pas dépassement (il faudrait 64 bits pour exprimer le résultat). Malheureusement, ah là là le 68020 n'est pas un processeur 64 bits. Si ? Ah bon :

On a la forme :

muls.l source,resultatFort:resultatFaible

Exemple :

muls.l d0,d1:d2

Depuis le 68020, cette forme réalise (d0xd1), mais le résultat est posé dans 64 bits, en utilisant deux registres 680x0 ! Notez que même un PowerPC implémenté comme dans nos Amiga ne peut pas le faire ! On m'a dit que sur 68060, cette forme était en fait émulée.

Mauvaise nouvelle : jusqu'au 68040, les multiplications sont très lentes comparées à une addition ou un "move", qui ne mettent que de deux à quatre cycles, un "muls" mettra 24 ou 40 cycles. Comme l'intérêt de l'assembleur est d'optimiser, quelques trucs existent (en 68060 et en PowerPC, une multiplication prend le même temps qu'une addition).

Première note de bon sens : "add.w d0,d0" est l'équivalent optimisé de "muls.w #2,d0", sauf qu'ici, les 16 bits de poids fort sont effacés.

De plus :

move.w d0,d1
add.w d0,d0
add.w d1,d0 ; est équivalent mais plus rapide que :

muls.w #3,d0 ; ...sauf qu'ici, les 16 bits de poids fort sont effacés

Une autre technique consiste à précalculer des tables de résultats de multiplications, puis d'indexer dessus pour trouver sa valeur. C'est en effet plus rapide, mais il faut alors faire des choix entre la taille mémoire de la table et la précision en nombre de bits exprimée par la multiplication.

Les instructions de décalages de bits et les réels en virgule fixe

Il est possible de réaliser des "décalages" sur les registres de données, cela signifie changer la valeur des bits comme s'ils glissaient tous à droite ou à gauche, en spécifiant le nombre de bits de décalages. Cela peut être joli dit comme ça, on pourrait imaginer un défilement (bien lent) utilisant ces instructions. L'intérêt est plus mathématique : comme on l'a vu plus haut, chaque bit "vers la gauche" vaut deux fois plus que le bit qui est à sa droite. Donc si vous avez compris, décaler un registre d'un bit vers la gauche multiplie sa valeur de "2 puissance le nombre de bits de décalages vers la gauche", et décaler un registre vers la droite le divise de "2 puissance le nombre de bits de décalage vers la droite".

Première application de la chose : vous pouvez considérer qu'un décalage prend environ de quatre à six cycles, beaucoup plus rapide qu'une multiplication avec mulu/muls, et infiniment plus rapide qu'une division (divu/divs).

muls.l #64,d1 ; est aisément remplacé par :

lsl.l #6,d1 ; logical shift left (décalage gauche de six bits).

Et c'est carrément de quatre à huit fois plus rapide ! (sauf sur 68060 où les multiplications sont aussi rapides que des additions). Jusqu'au 68040 inclus, pour multiplier par 320 par exemple, ceci est plus rapide qu'un "muls.l #320,d0" :

move.l d0,d1
lsl.l #8,d0 ; *256
lsl.l #6,d1 ; *64
add.l d1,d0 ; *320

Cela dit, "add.l d0,d0" est plus rapide mais équivalent à "lsl.l #1,d0". Le nombre de bits de décalages maximum est huit. Si vous voulez décaler de dix 10 bits, il faudra utiliser deux instructions : décaler de huit, puis de deux.

Voyons les instructions disponibles pour réaliser ces décalages (à noter que "shift" signifie décalage, "left" signifie gauche, "right" signifie droite) :

lsl(.b,.w,.l) #nbbit,dx ; logic shift left (multiplie)
lsr(.b,.w,.l) #nbbit,dx ; logic shift right (divise)

asl(.b,.w,.l) #nbbit,dx ; arithmetic shift left (multiplie, signée)
asr(.b,.w,.l) #nbbit,dx ; arithmetic shift right (divise, signée)

rol(.b,.w,.l) #nbbit,dx ; rotation left (un peu plus lent)
ror(.b,.w,.l) #nbbit,dx ; rotation right (un peu plus lent)

swap dx ; échange les 16 bits de poids fort avec les 16 bits de poids faible

Notez qu'il existe une version signée (asr) et une version non signée (lsr) de décalages à droite (asr garde le signe en mettant des 0 ou des 1 dans les nouveaux bits apparaissant à gauche). A gauche, je n'ai jamais compris d'ailleurs pourquoi il existait deux instructions (asl et lsl) puisque leurs comportements est le même. Un truc con : les instructions finissant par "l" muLtipLient, et celles qui finissent par "r" Réduisent.

"rol" et "ror" sont des versions qui font apparaître les bits qui "dépassent" de "l'autre côté" du registre, mais sont un peu plus lentes.

"swap dx" est, par contre, très intéressant. Les 16 bits de poids fort et de poids faible sont échangés, et c'est une des instructions les plus rapides d'exécution (avec "move" et "add" : un ou deux cycles). Si vous imaginez travailler avec quelques valeurs ".w", vous pouvez en "cacher" d'autres dans les poids fort de registre déjà utilisés, et y faire appel entre deux "swap" (on a toujours besoin d'utiliser les registres au maximum). En ".w", un simple "swap" suivi d'un "clr.w" multiplie la valeur en ".l" par "(1 puissance 16)=65 536", en signé ou en non signé. Un autre "swap" la redivisera par le même nombre.

"bfins", "bfext", etc. Ces instructions de décalages compliqués mais parfois utiles, disponibles depuis le 68020, ne seront pas expliquées ici (il s'agit de déplacer une suite de bits d'un registre vers un autre, en décalant ou pas. Les adeptes du PowerPC reconnaîtront rlwimi).

Les réels en virgule fixe

Une des applications possibles des décalages est de les utiliser pour "créer" des réels en virgule fixe, ce qui est une bonne idée d'optimisation en assembleur : nous avons vu que les registres de données sont des entiers, donc l'unité est alors la valeur du bit de poids le plus faible : 1. Impossible d'exprimer "2,5" ou "0,75", ou "256,125". Il existe bien parfois une unité FPU (floating point unit) aux 680x0, présente systématiquement à partir du 68040, et qui fourni des registres et des instructions "réels à virgule flottante", mais nous n'en parleront pas ici.

Il est possible de simuler des réels avec une précision de bit "après la virgule" constante, en appliquant à nos registres entier un "changement de repaire", en les multipliant et en les divisant par un nombre défini : par exemple "65 536", ça correspond à 16 bits de décalages. Si, en entier, on réalise n=10/3, on obtient 3, alors que le résultat réel est 3,33333... Si on imagine qu'on pose, en ".l" : "le 16e bit vaut 1, il est l'unité, le 17e bit vaut 2, le 18 vaut 4, etc," d'une part, et d'autre part : "le 15e bit vaut 0,5, le 14e bit vaut 0,25, le 13e bit vaut 0.125, etc.", il est alors possible de réaliser ce calcul :

move.l #10,d0
move.l #3,d1
swap d0 ; *65536
swap d1
divu.l d1,d0 ; prend la valeur de 3,333... *65536

Les valeurs après la virgule (exprimant 0,3333...) sont alors dans les 16 bits de poids faible de d0.

Les instructions de division. Ça rame comme ce n'est pas permis

On peut, en effet, atteindre les 80 cycles avec des "divs.l". N'oubliez pas d'en virer le maximum dans vos codes. Cela dit, quand il en faut, il en faut. Vous devez connaître par coeur le comportement étrange de cette instruction. On a donc en ".w" :

divs.w d0,d1 ; divise d1 par d0, etc.
divs.w (a0),d1 ; diviseur lu en mémoire
divs.w labelmemoire,d1
divs.w #valeur,d1 ; division immédiate possible évidemment

divu.w d0,d1 ; version non signée (un peu plus rapide)
divu.w (a0),d1 
divu.w labelmemoire,d1

Attention : en ".w", la valeur 32 bits de d1 est divisée par la valeur 16 bits de d0, après quoi le résultat de la division est placé dans les 16 bits de poids faibles de d1 (quotient) et le reste de la division est placé dans les 16 bits de poids fort de d1. Donc lire une valeur ".l" sur un registre juste après une division ".w" est une hérésie.

divs.l d0,d1 ; depuis 68020

Beaucoup plus simple, cette version divise d1 32 bits par d0 32 bits, le résultat (quotient) est placé dans d1, point final. Impossible de récupérer le reste directement, sauf si :

divs.l d0,d1:d2 ; méga lent, mais 64 bits

...divise la valeur 64 bits donnée par d1:d2 par la valeur 32 bits d0, puis place le reste dans les 32 bits d1 et le quotient dans les 32 bits de d2.

divsl.l d0,d1:d2

N'est pas pareil : divise seulement 32 bits (d2) par 32 bits (d0), puis place reste et quotient comme pour "divs.l dx,dy:dz" (les versions non signées divu existent).

Les additions, soustractions... Y a-t-il des choses à dire là-dessus ? oui

On a vu que pour des expressions signées ou pas, les opérations d'additions et de soustractions sont les mêmes. Voici des expressions possibles :

add.l d0,d1 ; "add" réalise une addition
sub.l d0,d1 ; "sub" réalise une soustraction
add.w (a0),d1 ; + la valeur à l'adresse donnée par d1
add.w d1,(a0) ; possible aussi !
add.b memoirelabel,d1
add.w #valeurimmediate,d1

Si vous allez faire un tour du côté de la table des instructions, vous verrez qu'il existe une foule d'instructions dérivées, comme "adda" qui n'est pas intéressante :

adda.l d0,a0

"adda" est en fait la version spéciale pour les registres d'adressage. Il se trouve que taper "add ...,ax" revient au même, l'assembleur remplacera le "add" par un "adda". Attention : en ".w", contrairement à une addition sur un registre de données, un "adda.w" additionne une valeur 16 bits vers un ".l" ("adda" affecte toujours les 32 bits d'un "ax"). "adda" n'accepte pas d'addition 8 bits (".b"). "addq" (add quick, rapide) a plus d'intérêt :

addq.l #4,d0

"addq", "subq" et "moveq" sont des formes possibles plus rapide lorsqu'on donne une valeur immédiate (avec #), comprise entre -8 et 7 (je crois, je n'ai pas pu vérifier ici, je crois aussi que le calcul s'applique forcément en ".l" même si on spécifie ".b" ou ".w" : le piège !).

Une note sur le très intéressant "addx", notamment utilisée pour optimiser le placage de textures :

add.l d0,d1
addx.l d2,d3 ; équivalent d'une addition 64 bits (d2:d0 + d3:d1)

Lors d'une addition (comme ici le 1er "add"), le dernier bit de retenue impossible à appliquer sur un "33e bit" est placé dans le bit X du "registre spécial invisible". "addx" réalise une addition classique, mais rajoute encore ce bit X (donc 0 ou 1) dans l'état où il a été laissé par la dernière instruction qui l'a modifié (ici le "add"). Certaines instructions touchent des bits du registre spécial ou pas, voir les instructions de tests : par exemple, "move" ne touche pas le bit X.

Le résultat de ces deux lignes est donc une addition 64 bits (2x32) puisque "addx" continue le travail du "add" précédent. De plus, "addx" est aussi rapide qu'un "add" normal (mettez cela en parallèle avec les notes sur les réels en virgule fixe, et payez-vous une crise mystique).

Voilà, le chapitre est fini. Je pourrais aborder les instructions d'opérations logiques (or, xor, and,...) mais elles sont assez évidentes dans leur compréhension et leurs comportements (ah si : "and.l d0,d1" est deux fois plus rapide que "and.l #valeur,d1").


[Retour en haut] / [Retour aux articles]