require("../global.php"); entete(); ?>
Attention, baissez l'intensitée de votre moniteur jusqu'à obtention d'un beau texte blanc sur fond marron (parceque tel quel, blanc sur fond rouge vif, vous ne tiendrez pas jusqu'à la fin). (Sur écran VGA appuyez sur les touches F5 à F8).
Maintenant reprenons cette initiation...
Ce chapitre va donc tenter de vous décrire sommairement comment le processeur décode la mémoire pour déterminer les instructions.
hF2/hF3 (REPNZ/REPZ) hF0 (LOCK) h67 (Taille d'adresse non-defaut) h66 (Taille d'opérande non-defaut) h2E/h36/h3E/h26/h64/h65 (OverRide de segment)
Une instruction peut posséder plusieurs préfixes, et peut même, selon le processeur, posséder des préfixes sans rapport avec l'instruction (par exemple un OverRide de segment et/ou un REPZ devant un 'MOV AX,12').
LOCK est le moins utile de tous ces préfixes puisqu'il ne fait que bloquer le bus pendant l'instruction qu'il précède. Ceci n'est utile que pour les multiprocesseurs.
Pour faire assembler ce préfixe, il suffit d'écrire 'LOCK' devant l'instruction concernée.
REPZ et REPNZ n'ont de conséquences qu'avec certaines instructions, les instructions de chaînes (Cf. Chapitre suivant. Et oui, faut tout lire dans tous les sens, mais que voulez vous...) Devant ces instructions, chacun de ces deux préfixes occasionnera un test sur la valeur de eCX (eCX = CX ou ECX). Selon le résultat de ce test (eCX nul ou pas), l'instruction qui suit le préfixe sera exécutée ou pas. Après l'exécution, eCX sera automatiquement décrémenté et le processeur ré-exécute la même instruction, ce qui fait que eCX est à nouveau testé, puis décrémenté, etc...
Ce préfixe est donc très interressant pour exécuter plusieurs fois la même instruction (n'oubliez pas quand même que ça ne marche qu'avec les instructions de chaîne !).
Pour assembler ces préfixes, il suffit d'écrire soit 'REPZ' (ou 'REPE' ou 'REP', c'est identique), soit 'REPNZ' (ou 'REPNE') devant l'instruction.
Au fait : dans ce cas précis, 'REP' ne signifie par REPorter mais REPeat (REPéter) ;)
Les préfixes d'override de segment servent à spécifier au processeur que nous voulons utiliser un registre de segment particulier plutôt que le registre de segment par défaut pour l'instruction (qui dépend du mode d'adressage, cf. articles précédents). h2E correspond à CS, h36 à SS, h3E à DS, h26 à ES, h64 à FS et h65 à GS.
Selon les assembleurs, vous devez spécifier l'override devant l'instruction ("CS: MOV AX,[BX]"), ou devant l'opérande mémoire ("MOV AX,CS:[BX]").
Notez que suivant le syntaxe d'Intel, il correspond souvent plus d'un OpCode à une instruction, ce qui prête parfois à confusion.
L'OpCode peut être suivi, dans le cas d'instructions 386, d'un octet appelé ModR/M et servant de complément à l'Opcode. Dans le cas d'un mode d'adressage 32 bits, un autre octet, le SIB, spécifie l'index, la base et le facteur.
Spécifier que les opérandes ou que les adresses n'auront pas la taille par défaut laisse entrevoir que le processeur peut travailler dans deux modes : le mode 16 bits et le mode 32 bits. Dans le mode 16 bits, tous les registres utilisés implicitement seront les registres 16 bits, et en mode 32 bits les registres utilisés implicitement le seront sous leur forme 32 bits. Ceci vaut pour eCX dans les LOOPs, eIP pour pointer sur les instructions, et en fait tout registre 16/32 bit utilisé dans toute instruction. Le choix entre mode 16 ou 32 bit se fait dans la partie cachée de CS. Pour des raisons évidentes, le DOS et le BIOS sont écrits pour le mode 16 bits, et donc ne nous programmerons pas en mode 32 bits. Quoiqu'il en soit les préfixes h66 et h67 devant une instruction permettent de faire exécuter celle-ci en tant qu'instruction 32 bits (et inversement si nous étions en mode 32 bits). H66 permet de changer la taille des opérandes (registres) et h67 permet d'utiliser l'un des modes d'adressage 32 bits (cf. articles précédents).
Vous n'aurez pas à préciser manuellement ces préfixes si vous utilisez un assembleur 386. Sachez tout de même que lorsque vous tapez 'MOV EAX,15' l'assembleur assemble un simple 'MOV AX,15' en rajoutant le préfixe h66, et que lorsque vous utilisez un mode d'adressage 32 bits, l'octet h67 se rajoute devant l'instruction. Sur les assembleurs MASM/TASM, la directive USE16 ou USE32 permet à l'assembleur de déterminer si le mode par défaut est 16 ou 32 bits.
Le choix entre incrémentation ou décrémentation se fait grâce au flag D, dont les instructions CLD/STD permettent de fixer la valeur. Si D=1, les pointeurs seront décrémentés, et incrémentés dans le cas contraire.
Notez aussi qu'un override de segment sur cette instruction permet de changer le choix de DS pour référencer eSI.
Cette instruction est intéressante car c'est le moyen le plus rapide pour déplacer des octets (sur 386 et 486 en tout cas).
SCAS compare quant à elle de la même manière AL ou eAX à la valeur de même taille pointée par ES:eDI.
Retenez toutefois que ces instructions ne peuvent être utilisées que lorsque le hard les supporte, ce qui est rare.
Le but du jeu est d'afficher la liste des premiers nombres premiers (hum, pas terrible la phrase).
Pour cela il y a deux solutions simples (je ne doute pas une seule seconde que Pierre connaisse trente autres solutions meilleures que les miennes) : Verifier pour chaque nombre s'il est premier ou pas, en essayant de le diviser par tous les entiers qui lui sont inférieurs (en fait, pas nécessairement tous, mais bon), ou bien considérer une table de nombres et éliminer tous les multiples de 2, puis de 3, de 4, etc... Remarquez d'ores et déjà une optimisation : une fois que l'on a éliminé les multiples d'un entier, il n'est plus nécessaire d'éliminer les multiples des multiples de cet entier. Concrètement, une fois qu'on à retiré 4 - multiple de 2 - il devient inutile d'éliminer les multiples de quatres - multiples de 2 aussi par la force des choses.
C'est cette solution que nous allons mettre en application.
Nous allons utiliser une table de 64Ko pour représenter les 64*1024*8=524288 premiers nombres entiers (un bit par nombre). Si le bit n est à 0, sela signifiera que le n-ième entier est premier, et s'il est à 1 qu'il est multiple d'un autre entier différent de 1 (et sinon, qu'il y a un sérieux bug quelque part au niveau du hard).
La première partie du programme consiste donc à éliminer les multiples. Après cela, on affichera tous les nombres dont le bit est à 0.
mov dx,o message mov ah,9 int 21h ... message db 'Calcul des nombres premiers en cours...',10,13,36
10 et 13 sont les caractères de fin de ligne (pour que le curseur aille au début de la ligne suivante), et 36 est le code ASCII de '$' (Valeurs décimales).
<message> étant défini dans l'unique segment d'un programme .COM, et DS étant égal à CS au début du programme, il n'y a aucune raison de changer le contenu de DS.
Le "o" dans "mov dx,o message" est une abrévation pour "offset" (zut, je vais vous refiler mes sales manies de flemmard).
Puis il faut réserver 64Ko de mémoire. Pour cela on utilise la fonction 48h de l'interruption DOS, avec dans BX la taille en paragraphes (bloc de 16 octets) de la zone demandée. Mais avant cela, il faut préciser au DOS que le programme n'utilise pas toute la mémoire. En effet le DOS réserve toute la mémoire lorsqu'il charge un programme COM (alors que 64Ko suffiraient forcément, il est bête ce DOS), ce qui pose problème car si on sait que l'on dispose de mémoire supplémentaire, on ne sait pas où elle est. On ramène donc la taille de notre programme à 64Ko (ce qui est très large vu que le programme fait en fait 221 octets) grace à la fonction 4Ah :
mov bx,1000h ; 1000h pour 65536 octets, mov ah,4ah ; c'est la taille réservée pour le int 21h ; programme. mov ah,48h ; Et on alloue ensuite 65536 octets. int 21h mov es,ax ; ES:0 = DS:0 = début de la zone mov ds,ax ; allouée.
Il faut ensuite initialiser la table de 524288 bits à zéro. Pour cela on utilise l'instruction STOSd (équivalent à MOV ES:[DI],EAX ; ADD DI,4", mais plus court) :
xor di,di ; idem "MOV DI,0" mov cx,4000h ; 4000h doubles-mots = 65536 octets xor eax,eax ; idem "MOV EAX,0" cld ; Pour que DI soit incrémenté rep stosd ; Pour repeter 4000h fois STOSd
Remarquez que pour mettre un registre 16 ou 32 bits à zéro on fait généralement XOR Reg,Reg (vérifiez que cela efface bien tous les bits du registre) ou plus simplement SUB Reg,Reg. Ce n'est pas plus rapide qu'un MOV, mais c'est plus court. Pour un registre 8 bits par contre, on préferera MOV Reg,0 qui est quand même plus explicite.
1/ A = 2 (Pas 1 surtout !) 2/ Si le Aième bit est 1, GOTO 7 (A est-il un multiple ?) 3/ B = A*2 (le premier multiple est A+A. Attention de ne pas mettre le bit de A à 1 !) 4/ Mettre le bit B à 1 (B est multiple de A) 5/ B = B+A (Passe au multiple suivant) 6/ Si B<524288 GOTO 4 (Tant qu'on n'arrive pas à la fin de la table) 7/ A = A+1 (Sinon, on recommence avec l'entier suivant) 8/ Si A<262144 GOTO 2 (Tant que l'on arrive pas à la fin)
Plusieurs commentaires :
Traduire cela en assembleur est très facile à condition de connaître les instructions de bit. Nous utiliserons uniquement BTS pour mettre un bit à 1, et BT pour tester si A ou B ne sont pas supérieurs à leur plus grande valeur permise.
BT (Bit Test) recopie un bit dans le carry (CF). Le bit en question est repéré grace à l'adresse de la table et le rang du bit. Dans notre cas, l'adresse de la table est DS:0 et le rang est A ou B (le nombre entier considéré).
BTS fait exactement la même chose sauf qu'en plus de recopier le bit, BTS le met à 1 ensuite dans la table (Bit Test and Set).
Ceci nous donne, en prenant EAX pour A et EBX pour B :
mov eax,2 ; A = 2 boucle_A: bt ds:[0],eax ; Teste le A-ième bit jc nxt_A ; Si CF=1, GOTO nxt_A mov ebx,eax ; B = A add ebx,ebx ; B = B+B (B = 2*A) boucle_B: bts ds:[0],ebx ; Met le B-ième bit à 1 add ebx,eax ; B = B+A bt ebx,19 ; B >= 524288 ? jnc boucle_B ; Si non, boucle nxt_A: inc eax ; Si oui, passe au A suivant bt eax,18 ; A >= 262144 ? jnc boucle_A ; Si non, boucle ; Si oui, c'est fini
Quelques explications au sujet des BT : 524288=2^19, donc en binaire cela s'écrit 10000000000000000000b (19 zéros). Donc, quand ebx sera supérieur ou égal à 524288, le 19ème bit sera égal à 1, et 0 sinon. De même avec le 18ème bit pour eax (262144=524288/2 donc =2^18).
Voici comment nous allons procéder : Nous allons boucler sur tous les entiers en utilisant ecx comme compteur. Si le ECXième bit de la table est 0, on affichera ECX (ECX est premier dans ce cas là).
Le problème à résoudre est celui d'afficher un nombre entier (ECX). En assembleur, il faut tout décomposer : afficher un nombre entier, cela signifie envoyer à l'écran les caractères ASCII qui composent une représentation (décimale, hexadécimale, binaire, ou autre) du nombre. Le but n'étant pas de faire des choses compliquées nous allons afficher les entiers en hexadécimal.
Rien de plus simple. Les nombres étant tous inférieurs à 2^19, il suffit de 5 chiffres pour représenter les nombres (19/4 = 4.75, que l'on arrondi à 5 car envoyer trois quarts de caractère ASCII me pose un gros problème métaphysique). Si vous avez compris ce que j'ai raconté dans le premier article, vous devez savoir que le nombre à afficher en hexa peut se décomposer sous la forme :
Digit1 + Digit2*16 + Digit3*256 + Digit4*4096 + Digit5*65536
... avec chacun des digits pouvant aller de 0 à 15 (0 à Fh).
Plus fort encore : l'avantage d'afficher le nombre en hexadécimal c'est que les Digits sont directement déductibles de la valeur par manipulation binaire. Prenons un exemple, le nombre 132569 :
18 17 16 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | 0| 1| 0| 0| 0| 0| 0| 0| 1| 0| 1| 1| 1| 0| 1| 1| 0| 0| 1| +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
Vous devez vous souvenir que pour convertir du binaire en hexa il suffit de remplacer chaque groupe de 4 bits par le digit hexadécimal correspondant (cf table dans le premier article). Ceci nous donne : 205D9
Nous allons donc utiliser une table contenant les 16 digits hexa ASCII dans l'ordre, et isoler les 4 bits qui nous intéressent (masque + décalage), puis afficher le caractère se trouvant à l'adresse de la table plus les 4 bits en question.
En fait, nous n'allons pas afficher les caractères les uns après les autres mais plutôt constituer une chaîne de caractères que nous enverrons en une seule fois pour tout le nombre (plus simple et plus rapide).
Voici le programme :
; affichage de la liste des nombres premiers jusqu'a 52428 push cs ; DS=CS à partir de maintenant, pour utiliser pop ds ; la fonction 9 de l'int 21h (DS:DX -> texte). mov ecx,1 ; Premier entier à tester (0 est toujours exclu). boucle_m: ; Boucle sur tous les entiers. bt es:[0],ecx ; Ce nombre (ECX) est-il premier ? jc nxt_m ; Si non, on ne l'affiche pas. ; afficher ecx en hexadecimal sur 5 digits mov edx,ecx ; On va travailler sur EDX et laisser ECX. mov eax,4 ; EAX boucle sur les 5 digits (4 à 0). boucle_cur: mov bx,dx ; On isole les 4 bits faibles de EDX and bx,1111b ; dans BX. mov bl,[tbl_convert+bx] ; BL = l'ASCII du digit. mov [texte+eax],bl ; On écrit cet ASCII dans la chaine. shr edx,4 ; On décale EDX pour traiter les 4 bits du ; second digit (en partant de la droite). dec ax ; AX pointe sur la prochaine position dans ; texte. jns boucle_cur ; tant qu'il n'est pas négatif, on remet ca. mov dx,o texte ; Ici, AX=-1 donc on a constitué toute ; la chaîne. mov ah,9 ; Il reste à l'afficher. int 21h nxt_m: inc ecx ; Quand c'est fait, on passe à l'entier bt ecx,19 ; suivant. jnc boucle_m ; Tant que l'on n'arrive pas à la fin. ... tbl_convert db '0','1','2','3','4','5','6','7' ; Table de conversion db '8','9','A','B','C','D','E','F' texte db '00000 ; ',36 ; La chaîne de caractère à ecrire ; (après écriture des digits corects).
; exit mov ah,49h int 21h mov ah,4ch int 21h
Côté organisation, il me semble que certaines règles peuvent être salutaires aux débutants.
La plus importante de toutes vous la connaissez déjà, Warrant à déjà insisté sur ce point mais je le refait : ne commencez jamais à coder directement sur votre micro ! C'est le meilleur moyen pour passer sur un programme plus de temps que nécessaire ! En effet, avec les éditeurs avec fiches techniques intégrées, DOCs en lignes, environnements de programmation complets et gestionnaires de projets, on a tendance à ne plus éteindre sa machine pour programmer.
Grave erreur car réflechir à un programme avec un stylo et un bloc de papier est plus profitable que d'affronter d'entrée de jeu la machine : sur papier, on peut écrire en abrégé, ne pas se soucier des fautes, écrire du code non assemblable, etc, alors que, tout convivial qu'il soit, votre éditeur nécessitera de votre part une plus grande part d'attention que votre stylo. Du reste, on écrit souvent plus rapidement qu'on ne tappe au clavier, ce qui permet de ne pas être ralentit dans son raisonnement par le temps de frappe (n'oubliez pas qu'en asm, il faut généralement beaucoup de lignes pour faire bien peu de choses).
En fait, idéalement, vous devriez ne vous servir de votre éditeur que pour recopier votre programme, et non pour le créer. Bien sûr, comme tout le monde, il m'arrive souvent moi même de ne pas respecter ce principe, mais je vous assure que quand je m'y tiens, je gagne du temps (souvent du temps de débuggage, comme le précisait Warrant).
Tant qu'on y est, voici le second principe : ne pas forcément chercher à commencer par le début. En effet, le début d'un programme asm est souvent long et peu intéressant (initialisations en tout genre). Ce n'est pas la peine de se noyer dans cette phase pour ne pas arriver au point essentiel, comme cela m'est souvent arrivé. Contentez-vous donc du strict minimum pour faire tourner une version de test sous un debugger.
Troisième principe concernant l'assemblage : ne laissez pas l'assembleur faire des choses dans votre dos ! Je suis peut-être le seul à penser ça, mais je vois mal ce qu'apportent les directives de MASM/TASM en lisibilité, clarté, ou simplicité. Tenez vous en donc toujours aux instructions, c'est un conseil. Oubliez donc les 'RET', 'PROC', 'JUMPS', 'ARG', 'MODEL', 'STARTUPCODE' et compagnie, dont le but semble être d'embrouiller le codeur en lui cachant le résultat final.
Pour le debuggage, juste un dernier principe très important : Evitez à tout prix le "trash-code" ! Il n'y a pas grand chose de pire qu'un bug qui disparait sans que l'on comprenne pourquoi. Il vous arrivera souvent ceci : "j'ai changé ça et pis ça, et maintenant ça marche alors j'y touche plus". Il n'y a rien de pire ! Si vous ne comprennez pas d'où vient un bug, attendez-vous à le voir ressurgir sous une autre forme, et là se sera vraiment sans espoir. Si vous avez déjà taté d'un peu de code, vous comprenez sûrement pourquoi j'insiste là dessus...
Mais comme on ne code pas en assembleur uniquement pour le plaisir, et qu'il arrive que l'on recherche les performances, voici aussi quelques points à garder dans un coin de sa tête pour avoir un code rapide :
Comptabilisez les cycles, les pénalités, ainsi que la taille du code.
Quelques règles simples :
 
A partir de la prochaine fois, nous ne travaillerons plus qu'à base d'exemples. Relisez l'init vidéo de Mogar car on va sûrement faire une petite excursion au pays des graphismes, histoire de matérialiser un peu nos programmes (juste en mode 13h, mais je ne rappellerai pas comment ça marche ni où ça se trouve).
Codez bien d'ici là !