require("../global.php"); entete(); ?>
Sont abordés dans cet article :
|
+----------------------------------------------------------+ | OV MOV MOV MOV MOV M de l'anglais "move", mais at- | | V MOV MOV MOV MOV MO -tention, il s'agit bien de | | MOV MOV MOV MOV MOV copier et non de déplacer ! | +----------------------------------------------------------+
Cette instruction nécessite deux opérandes, une destination et une source, spécifiées dans cet ordre. Chacun des trois types d'opérande est utilisable (registre, contenu mémoire ou valeur immédiate), avec les restrictions suivantes :
Exemples :
MOV AL,BL copie BL dans AL (AL <- BL) MOV CX,Word [ES:BX] CL <- Byte [ES:BX] CH <- Byte [ES:BX+1] MOV DWord [h1038],h03BC2771 [DS:h1038] <- h71 [DS:h1039] <- h27 [DS:h103A] <- hBC [DS:h103B] <- h03
Exemples incorrects :
MOV 5,1 Incodable et idiot :) MOV Byte [FS:EDX*2],Byte [0] Incodable, dommage :( MOV AL,DWord [GS:BX+DI] Les deux opérandes n'ont pas la même taille MOV Word [SS:10+EAX+EAX],ECX Idem
Maintenant, que se passerait-il pour quelque chose comme ceci :
MOV BX,Word [BX] ???
Et bien, il se passe en fait la seule chose possible : BX prend la valeur du mot à l'adresse que contenait BX au moment du décodage de l'instruction. Aucun problème, donc.
+----------------------------------------------------------+ | MP JMP JMP JMP JMP JMP JMP JM de "jump", sauter. | +----------------------------------------------------------+Jmp appartient à une autre catégorie d'instructions. Son rôle est de modifier le pointeur d'instruction.
Pour ceci, un jmp ne requiert qu'une opérande. Cette opérande peut être une valeur relative signée à ajouter à (E)IP (on parle alors de jmp short ou jmp relatif), ou bien un pointeur à charger dans CS et (E)IP (jmp far), ce pointeur pouvant être spécifié par une valeur immédiate, par un registre ou par le contenu d'une adresse mémoire.
Exemples :
JMP h5D80:h134C CS <- h5D80 (E)IP <- h134C JMP 0 JMP relatif de déplacement nul : ne sert à rien JMP -50 JMP relatif (E)IP <- (E)IP-50 JMP h34BB JMP relatif (E)IP <- (E)IP+h34BB JMP hA5BC0012 JMP relatif (E)IP <- (E)IP+hA5BC0012
+----------------------------------------------------------+ | DD ADD ADD ADD ADD AD de "add", ajouter. | | B SUB SUB SUB SUB SUB de "substract", soustraire. | +----------------------------------------------------------+
ADD sert à additionner, et nécessite deux opérandes : une source et une destination. La destination prendra la valeur de source + destination. Les contraintes de validité sont les mêmes que pour MOV, mais cette fois-ci on ne peut pas du tout utiliser de registre de segment.
Exemples :
ADD EDX,EAX EDX <- EDX+EAX EAX reste inchangé ADD Byte [SI+2],-123 [SS:SI+2] <- [SS:SI+2] - 123 ADD Byte [SI+2],133 Idem (-123=133)
Exemples incorrects :
ADD 5,Word [GS:1+EBP+EDX*2] ADD BX,AL ADD DWord [0], DWord [1]
SUB soustrait deux valeurs. Le fonctionnement est le même que ADD, je n'y reviens pas.
+----------------------------------------------------------+ | OT NOT NOT NOT NOT NOT NOT NOT N de "not", non | | D AND AND AND AND AND AND AND AN de "and", et | | OR OR OR OR OR OR OR OR de "or", ou | | XOR XOR XOR XOR XOR XOR XOR XOR "eXclusive OR" | +----------------------------------------------------------+
NOT inverse tous les bits de l'opérande spécifiée (Cf. table logique à la fin de ce chapitre).
AND nécessite une destination et une source tout comme ADD ou SUB. La destination prendra la valeur de destination ET source, la relation ET s'appliquant bien évidement à chacun des bits (Cf. table logique à la fin de ce chapitre).
OR nécessite une destination et une source tout comme ADD ou SUB. La destination prendra la valeur de destination OU source, la relation OU s'appliquant bien évidement à chacun des bits (Cf. table logique à la fin de ce chapitre).
XOR nécessite une destination et une source tout comme ADD ou SUB. La destination prendra la valeur de destination OU-Exclusif source, la relation OU-Exclusif s'appliquant bien évidement à chacun des bits (Cf. table logique à la fin de ce chapitre).
Et vive le copier-coller !
Tables de vérité : ------------------ +-----+-----+ +-------+-------+-----+----+-----+ | Bit | NOT | | Bit 1 | Bit 2 | AND | OR | XOR | +-----+-----+ +-------+-------+-----+----+-----+ | 0 | 1 | | 0 | 0 | 0 | 0 | 0 | | 1 | 0 | | 0 | 1 | 0 | 1 | 1 | +-----+-----+ | 1 | 0 | 0 | 1 | 1 | | 1 | 1 | 1 | 1 | 0 | +--------------------------------+
Exemples :
NOT b10011010 = b01100101 b10011010 AND b11110100 = b10010000 b10011010 OR b11110100 = b11111110 b10011010 XOR b11110100 = b01101110
J'arrête là les présentations, vous avez maintenant une idée de ce qu'est une instruction. Le plus dur maintenant cela va être d'apprendre à programmer avec elles.
Mais avant de commencer à organiser ces instructions pour former un programme, il nous manque encore quelques notions telles que la pile, les interruptions, les indicateurs, etc... Ces notions vous sont expliquées dans les paragraphes suivants.
 
"LIFO" signifie "Last In First Out", c'est à dire "dernier entré, premier sorti". Plutôt que d'entrer dans les détails maintenant, je vais d'abord vous donner les deux principales instructions de pile, tout le fonctionnement apparaîtra ensuite clairement.
+----------------------------------------------------------+ | SH PUSH PUSH PUSH PUSH PUSH PUSH PU = "pousser" | | P POP POP POP POP POP POP POP = "extraire" | +----------------------------------------------------------+
Push nécessite une opérande (source) sur 16 ou 32 bits. Si l'opérande est sur 16 bits (respectivement 32 bits), le processeur commence par soustraire 2 à (E)SP (respectivement 4), puis il met l'opérande dans [SS:(E)SP]. Tout se déroule comme si l'on faisait:
PUSH s16 = SUB (E)SP,2 PUSH s32 = SUB (E)SP,4 MOV [SS:(E)SP],s16 MOV [SS:(E)SP],s32
... Sauf qu'ici s32 peut représenter une adresse mémoire.
POP est l'opération inverse et nécessite une opérande destination sur 16 ou 32 bits pour y recopier la valeur contenue dans [SS:(E)SP] avant d'augmenter (E)SP de 2 ou 4 (selon la taille de l'opérande, comme pour PUSH).
POP d16/32 = MOV d16/32,(D)Word [SS:(E)SP] ADD (E)SP,2/4
Vous commencez donc probablement à voir pourquoi on appelle un tel système "LIFO" : l'ordre dans lequel on récupère les valeurs de la pile est l'inverse de celui dans lequel on à envoyé ces valeurs dans la pile. Petit exemple :
PUSH Word 15 'Word' car cela pourrait PUSH Word -8 aussi être à prendre comme PUSH Word 51000 des DWords POP AX AX <- 51000 POP BX BX <- -8 POP CX CX <- 15 PUSH 1234ABCDh Ici c'est forcément DWord POP DX DX <- ABCDh POP SI DI <- 1234h
Nous pouvons maintenant revenir à la notion de pile...
Prenons la séquence de code suivante :
PUSH Dword v1 PUSH Word v2
On représente la pile grace à ce schéma :
. . | : : | Sens des | | | adresses +-----------+ | croissantes SS:(E)SP après ->| v2 | <----- Sommet de la pile | +-----------+ | | 0..15 v1 | | +-----------+ | | 16..31 v1 | | +-----------+ | SS:(E)SP avant ->| ? ? ? | <----- Base de la pile | +-----------+ | | | | : : | . . V
Vous remarquerez qu'aucune valeur n'est jamais écrite dans la base de la pile, et que (E)SP pointe toujours sur la dernière valeur envoyée.
Vous remarquerez aussi que l'aspect général ressemble bien à une pile : on rajoute des éléments au sommet, ou on retire des éléments du sommet. Avant de retirer un élément, il faut déjà retirer tous ceux qui sont au-dessus de lui (PUSHés après). Bref, cette fois plus de doute : c'est bien du LIFO...
Il faut en général veiller à l'équilibre de la pile, c'est à dire à POPer tout ce qu'on y a PUSHé.
Entre l'exécution de deux instructions, le processeur vérifie l'état de ces deux broches (NMI d'abord, puis INTr). Si l'une d'entre elles est active, cela signifie qu'il doit interrompre son travail pour exécuter du code plus important. La différence entre les deux broches est que la broche INTr est masquable (NMI signifie d'ailleurs Non-Masquable-Interrupt). En effet l'indicateur IF permet, s'il est à zéro, d'inhiber la broche INTr (Cf. Indicateur FLAG).
Chaque interruption est identifiée par son numéro, de 0 à 255. Lorsque le processeur doit servir une interruption, il faut tout d'abord qu'il en détermine le numéro.
Le numéro d'interruption associé à la broche NMI est automatiquement le numéro 2.
Pour la broche INTr, il doit communiquer avec la puce qui a enclenchée cette broche : le contrôleur d'interruption 8259 (ou encore PIC pour Programmable Interrupt Controler). C'est ce contrôleur qui est relié aux différentes extensions et qui leur permet de déclencher ces interruptions en associant à chacune d'elles une IRQ (Interrupt ReQuest), c'est à dire un numéro dont découlera le numéro d'interruption (par l'addition d'une valeur de base, 8 ou h70 en général). Le fonctionnement du 8259 dépassant largement le cadre de cet article, je ne détaillerai pas plus son fonctionnement.
Une fois le numéro de l'interruption connu, il reste à trouver l'adresse du code à exécuter. Pour cela, le processeur utilise une table, nommée table d'interruption, dont l'adresse est spécifiée par un registre spécial (IDTR). Dans plus de 99% des cas, cette adresse sera 00000. Cette table contient - en mode réel - une série d'adresses segment:offset dont la première est celle du gestionnaire de l'interruption 0, puis celle du gestionnaire de l'interrutpion 1, etc... L'adresse du gestionnaire de l'interruption N se trouve donc à l'offset N*4 (4 octets par adresse segment:offset).
Pour transférer le contrôle à cette adresse, le processeur fait plus qu'un simple JMP Far, car il doit préserver l'adresse actuelle du code interrompu pour pouvoir reprendre son exécution sitôt que le gestionnaire de l'interruption aura fini son travail. Référez-vous au chapitre sur les appels de routines pour voir les détails de ce transfert de contrôle (Cf. Architecture d'un programme / INT).
Voici le contenu du registre de flag, les bits les plus interressants seront détaillés ensuite :
32 ... 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 +------------+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ??????? |OF|DF|IF|??|SF|ZF| 0|AF| 0|PF| 1|CF| +------------+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ \___________________________ EFLAGS _____________________________/ \__________________ FLAGS ____________________/
Les bits marqués 0 ou 1 ne doivent pas être modifiés. Les bits marqués '??' ne sont pas étudiés.
Instructions associées : CMC (complement carry), qui inverse la valeur de CF, STC (set carry), qui le met à un, et CLC (clear) qui le met à zéro.
Instructions associées : CLI, qui efface ce bit, et STI, qui le remet à un.
Instructions associées : CLD et STD.
Instruction associée : INTO, qui déclenche l'interruption 4 si ce flag est armé (i.e. à 1).
Parmis cette classe d'instructions se trouvent les sauts conditionnels, autours desquels s'articulent les programmes. Ces sauts sont des JMP near ou relatifs qui ne s'effectuent que si certains indicateurs sont correctement positionnés.
Voici à titre d'exemple la liste de tous ces sauts conditionnels (que cela ne vous dispense pas de trouver une liste d'instructions !) :
Instructions Signification Indicateurs (Jump if...) testés JB/JNAE/JC Below/Not Above or Equal/Carry CF=1 JAE/JNB/JNC Above or Equal/Not Below/Not Carry CF=0 JE/JZ Equal/Zero ZF=1 JNE/JNZ Not Equal/Not Zero ZF=0 JO Overflow OF=1 JNO Not Overflow OF=0 JP/JPE Parity/Parity Even PF=1 JNP/JPO No Parity/Parity Odd PF=0 JS Signed SF=1 JNS Not Signed SF=0 JA/JNBE Above/Not Below or Equal CF=0 et ZF=0 JBE/JNA Below or Equal/Not Above CF=1 ou ZF=1 JG/JNLE Greater/Not Less or Equal ZF=0 et SF=OF JGE/JNL Greater or Equal/Not Less SF=OF JL/JNGE Less/Not Greater or Equal SF!=OF JLE/JNG Less or Equal/Not Greater ZF=1 ou SF!=OF
Dans la liste ci-dessus, vous remarquerez que JA/JB/etc s'appliquent aux valeurs non-signées et JG/JL/etc aux valeurs signées (Cf. SF et OF).
La plupart des appelations des ces sauts conditionnels est due à l'instruction suivante : CMP.
CMP est un SUB qui ne modifie ni la source ni la destination, mais qui se contente de modifier les indicateurs. Par exemple, "CMP AX,BX" équivaudra à "SUB AX,BX" sauf que AX ne sera pas modifié. Par contre, le flag ZF sera mis si BX=AX (AX-BX=0).
Il est donc plus naturel de faire suivre une instruction de comparaison par un saut tel que "JE", "JB", "JAE", etc, plutôt que par leurs équivalents "JZ", "JC", "JNC", etc, dont la signification dans le contexte est moins nette.
Pour réaliser un problème donné, la règle générale est de décomposer ce problème en problèmes plus petits, et de recommencer encore et encore, jusqu'à arriver au niveau des instructions (un compilateur accélère la manoeuvre car il n'est pas nécéssaire de décortiquer autant - mais vous savez déjà une petite partie de tout le mal que je pense des compilateurs, n'y revenons pas).
Pour la décomposition, on s'efforcera d'isoler les concepts qui s'appliquent plusieurs fois à des objets identiques ou similaires pour limiter la quantité de code à produire (les membres de la secte de la POO - dont le crie de raliement est "Virtualisons, virtualisons, mes frères !" - doivent me traiter de païen pour l'utilisation que je viens de faire du terme divin "objet").
Nous appellerons d'ailleurs "routine" toute partie de code paramétrée dont le but est d'être exécutée plusieurs fois dans le déroulement du programme pour traiter des objets similaires. Prenons un exemple trivial : un morceau de code qui met dans EAX le résultat de l'opération : EAX+EBX+ECX+EDX
... ADD EAX,EBX ADD EAX,ECX ADD EAX,EDX ...
Pour pouvoir exécuter une même routine à partir de plusieurs endroits différents dans le programme, on utilise des instructions qui permettent de sauter à l'adresse de la routine (comme pour un JMP), puis de revenir où l'on s'était arrêté lorsque la routine est terminée. Pour réaliser un tel retour, il faut, pendant l'exécution de la routine, garder en mémoire quelque-part l'adresse du retour. C'est évidemment la pile qui va être utilisée pour stocker cette valeur.
CALL Far :
CALL Far fonctionne comme un JMP Far, sauf qu'avant de réaliser le saut, le processeur PUSHe CS puis (E)IP sur la pile. CALL Far est donc équivalent à :
PUSH CS PUSH (E)IP+ (IP+ = IP + taille du CALLF) JMP Far v16/32
Notez cependant qu'il est impossible normalement de PUSHer IP avec un 'PUSH IP'.
RETF (RETurn Far) :
RETF est utilisé conjointement avec CALL Far pour effectuer un retour de procédure. Ainsi, l'action du processeur pour le RETF est-elle le contraire de cette pour le CALLF : POPer (E)IP puis CS. L'exécution reprend donc ensuite là où elle avait été laissée après le CALL Far.
RETF admet optionnellement une opérande Im16/32 qui spécifie une valeur à ajouter à (E)SP après le retour, dans un but que nous étudierons plus tard.
CALL Near :
Comme le CALL Far, CALL Near transmet l'exécution à une routine. La différence entre les deux vient du fait que CALL Near n'entraine pas le PUSH de CS, ce qui signifie que l'appel est limité au même segment de code.
RETN (RETurn Near) :
RETN est le retour de routine associé à CALL Near : Il s'agit donc uniquement du POPage de (E)IP, et éventuelement de l'ajout de l'opérande Im16/32 à (E)SP.
INT (INTerrupt) :
INT sert à appeler le code d'une interruption dont le numéro est fournit en opérande (octet).
Le processeur commence par sauvegarder les flags sur la pile (PUSHF - pour PUSH Flags), puis l'adresse complète segment:offset comme dans le cas d'une CALL Far.
C'est la même procédure qui est suivie lors du déclenchement d'une véritable interruption, à ceci près que le bit IF est mis à 0 avant que le processeur ne démare l'exécution d'une interruption hard.
IRET (Interrupt RETurn) :
IRET est le retour associé à INT : Il consiste en un déPUSHage de (E)IP, CS, puis (E)Flags.
En reprenant tout ceci, completons la routine :
ADD EAX,EBX ADD EAX,ECX ADD EAX,EDX RETN
Pourquoi RETN et pas RETF ? Car en général on n'utilise qu'un seul segment pour le code et donc CS ne change jamais de valeur - sauf en cas d'interruption, bien sur.
Pour receuillir dans EAX la somme EAX+EBX+ECX+EDX, il suffira donc de faire un CALL Near à l'adresse de la routine.
Le concept de la routine, s'il n'est jamais indispensable à un programme, permet toutefois d'économiser la mémoire et d'alleger le code.
La mode actuelle lorsqu'on veut écrire un programme est de s'assoir en face de sa machine, de lancer son éditeur, de se gratter la tête, puis de commencer à tracer les plans du programme.
Je vous arrête tout de suite : développer un programme sur ordinateur est une absurditée absolue ! Il vaut mieux un bon bloc de papier que le dernier éditeur high-tech avec fiches techniques incorporées.
En effet, on écrit généralement plus rapidement qu'on ne tape au clavier, et il est plus prudent de commencer par prendre des notes abrégées de ce que l'on veut faire plutôt que d'affronter d'emblée la rigueur d'un éditeur, surtout en asm où un concept même simple demande souvent plus d'une instruction. Sur papier, lors de l'ébauche, on ne se soucie pas d'être compris par la machine, et on peut économiser sa concentration.
De plus, cette approche satisfera bobonne qui sera heureuse de constater que, par moment, il vous arrive d'éteindre votre PC.
La première chose à faire est de déterminer précisément la conduite du programme, puis d'en tirer la structure générale, pour finalement approfondir et construire les algorithmes. Tout ceci donnera l'organigramme du programme.
Ensuite commence le codage du programme. Le but est alors de réaliser le code le plus optimal possible pour traduire les concepts de l'organigramme. C'est dans cet aspect de la programmation que le codeur s'exprime : il faut optimiser en taille, en vitesse, partout, chaque instruction, utiliser au maximum la machine et ses composants. Le code optimal existe toujours, mais on ne le trouve jamais, comme de juste.
Nous aurons tout le loisir de découvrir chacun de ces aspects le long de nos pérégrinations au royaume de l'assembleur.
Le code du BIOS dépend du constructeur mais les fonctions qu'il doit remplir sont standardisées. C'est tout d'abord le premier code à être exécuté au reset du processeur. Sa première tâche est donc de charger le système d'exploitation (Cf. Le DOS). Ses autres fonctions assurent la gestion des extensions, ce qui, considérant le nombre et la variété des extensions possibles, facilite grandement la tâche du système d'exploitation.
Malheureusement, le code des BIOS ne progresse pas aussi rapidement que les processeurs, et le BIOS est actuellement totalement obsolète (en cela qu'il ne fonctionne correctement qu'en mode réel).
Les fonctions du BIOS sont accessibles par des interruptions (ce qui laisse le choix des adresses des différentes routines au constructeur).
Ces fonctions sont regroupées en 7 catégories :
Notez que les numéros de ces interruptions sont très mals choisis car ces numéros sont réservés par Intel - Intel à réservé les interruptions 0 à h20 -
Je ne donnerai aucune description de ces fonctions ; si vous ne possedez pas encore votre liste d'interruption, dépéchez-vous d'en trouver une !
Le DOS assure deux fonctionnalités : la gestion du système de fichier, et la gestion de l'exécution des programmes (chargement, gestion mémoire, etc...). Si son système de fichier est suffisent (il s'agit d'une copie améliorée du vieux CP/M), sa gestion des programmes se limite au chargement de fichiers exécutables et à la gestion mémoire (i.e. allocation d'un bloc de mémoire à un programme, ou libération d'un bloc).
Le DOS dispose aussi de quelques petites fonctions assez inutiles concernant la video, le clavier, etc...
Pour le système de fichier, reférez-vous à votre documentation du DOS.
Mes derniers mots sur le DOS iront à la gestion mémoire : Le DOS ne gère que les 640 premiers Ko de la mémoire du PC. Ainsi, le reste de la mémoire est-il livré aux lois barbares des gestionnaires type XMS ou EMS.
Le DOS associe à chaque programme une structure de données appelée PSP (Program Segment Prefix). Ce bloc de 256 octets contient divers paramètres donc la ligne de commande (généralement la chaîne de caractères tapée après le nom du programme dans votre interpréteur de commande). Au démarrage du programme, ES et DS pointent sur le début du PSP (i.e. ES:0 = DS:0 = 1er octet du PSP).
Nous reverrons le PSP lors d'un chapitre consacré au DOS.
Un programme est toujours divisé en segments, et tout ce qui est assemblé doit être inclu dans un segment. On spécifie donc toujours à l'assembleur le début, le nom, le type (taille du code), et l'alignement de chaque segment (la fin est optionnelle : un segment s'arrête là où le suivant commence). Certains assembleurs offrent d'autres alternatives mais nous ne retiendrons que celle-ci. Pour déclarer un nouveau segment, on utilise généralement la directive SEGMENT (j'appelerai directive toute commande écrite dans le source du programme et qui n'est pas destinée à être assemblée mais qui fournit des informations à l'assembleur) :
{nom} SEGMENT {type : USE16/USE32} {alignement : 16,32, etc...}
USE16 ou USE32 déterminent la manière d'encoder les instructions qui figurent dans ce segment. En général, nous choisirons USE16 car sous DOS le processeur se trouve par défaut en mode 16 bits.
L'alignement permet d'aligner le premier octet du segment sur une adresse divisible par 16, 32, etc. Cet alignement ne sert strictement à rien au programme lui même mais permet à l'assembleur de connaître l'alignement de chaque octet du segment, ce qui est très utile pour aligner ensuite certains de ces octets sur des adresses que l'on sait divisibles par 2, 4, 16 ... (cela accélère leur traitement).
Notez à ce sujet qu'il vaut mieux éviter d'aligner un segment à moins de 16, car CS contiendra l'adresse physique divisée par 16. Si L'adresse physique n'était pas multiple de 16, IP devrait être initialisé à autre chose que 0 pour pointer le début du segment, ce qui poserait de sérieux problèmes d'organisation en plus de problèmes évidents d'"éthique"...
Il existe aussi un type de segment particulier, le segment de pile. Ce segment possède ceci de particulier qu'au chargement du programme, le DOS initialisera SS sur ce segment et enverra SP à la fin du segment (base de la pile). Nous noterons ce segment "STACKSEG" au lieu de "SEGMENT".
Autre point capital : les adresses.
Lorsque nous écrirons des programmes, nous n'allons pas calculer à chaque fois l'adresse d'une position de notre code. D'autant que dans le cas d'un JMP on pourait être ammené à calculer l'adresse relative par rapport à la position du JMP, ce qui ne serait franchement pas commode (immaginez un peu !).
C'est pourquoi tous les assembleurs actuels sont 'symboliques', c'est à dire qu'ils accèptent de travailler avec des références, des symboles, plutôt qu'avec des adresses véritables. Par exemple, une adresse pourra être appelée 'TOTO' et utilisée dans un MOV ou JMP grace à cette désignation. C'est l'assembleur qui déterminera la valeur de ce symbole au moment de l'assemblage.
Ce concept s'étend d'ailleurs à tout symbole, qu'il représente une adresse ou une constante ou même un texte.
Lorsque nous définirons une constante, nous procèderons de la sorte :
Nom_du_symbole = Valeur
Par exemple :
Nombre_de_dents = 32 ... MOV EAX,Nombre_de_dents
Notez que l'appelation "constante" ne signifie pas forcément que le symbole aura une valeur constante tout le long du source (il est permis de définir les constantes plusieurs fois).
Pour finir, notez qu'il existe des constantes définies automatiquement par l'assembleur. Nous n'en utiliserons qu'une, celle notée '$' et appelée "compteur de position".
'$' représente le compteur d'adresse utilisé par l'assembleur. Il est donc toujours égual à l'adresse à laquelle on le place. Par exemple :
ici = $ ... là = $
'ici' et 'là' seront égaux aux adresses auquelles on a positionné le symbole. S'il n'y à rien entre les deux, on aura bien naturellement ici=là ('ici' et 'là' sont des déclarations de symbole et ne sont donc pas assemblés ; ces deux constantes ne modifient donc pas la valeur du compteur de position '$').
Vous voyez que '$' est un bel exemple de constante qui ne garde pas la même valeur tout le long d'un source...
Pour un symbole de substitution de texte :
Nom_du_symbole = <texte>
Par exemple :
Mettre_dans_AX = <MOV AX> ... Mettre_dans_AX ,5
La seule utilité à la substitution de texte est à mon avis la définition d'instructions que ne connait pas l'assembleur (instructions "non-documentées").
Pour un symbole d'adresse (= un "label"), nous marquerons à la position représentée le nom du symbole suivit de ':' .
Nom_du_symbole:
Par exemple :
Routine1: ... CALL Routine1
Notez que l'on ne met pas les deux points ':' lorsque l'on utilise le symbole.
Cette virtualisation de l'adressage est pratique car c'est l'assembleur qui déterminera, en fonction de la position du symbole, le type de saut (relatif, near ou far). De plus, si l'on déplace le symbole (en rajoutant quelquechose devant par exemple), il ne sera pas nécessaire de modifier toutes les instructions qui font référence à l'adresse représentée.
Directives très importantes : Celles qui permettent d'éditer des zones mémoires au lieu d'assembler des instructions. Ces directives sont nombreuses, et nous ne verrons dans ce chapitre que le strict minimum : DB, DW, DD et DUP().
DB (Define Byte) permet de réserver des octets qui contiendront en général des données initialisées. Par exemple, quand l'assembleur rencontrera 'DB 5,3,1' il sortira dans l'exécutable la suite d'octet 5, 3 et 1.
Comprenez bien que l'interêt d'un DB est de générer des octets qui ne seront pas exécutés.
DW et DD (Define Word et Define Double-word) procurent les mêmes effets mais définissent des mots ou doubles_mots au lieu des octets.
DUP ne définit rien à lui tout seul mais permet d'initialiser de larges tables de données en répétant un nombre de fois spécifié une suite d'octets (DUP pour DUPlicate). Le format est :
Db/w/d Nombre_de_répétitions DUP(suite de b/w/d)
Par exemple :
DB 8 DUP(0) <=> DB 0,0,0,0,0,0,0,0 <=> DW 0,0,0,0 <=> DD 0,0 DW 4 DUP(24,h65C) <=> DW 24,h65C,24,h65C,24,h65C,24,h65C <=> DB 24,00,h5C,h06,24,00,h5C,h06,24,00,h5C,h06,24,00,h5C,h06
Ceci nous amène à un nouveau type de symbole : la variable.
Une variable est le nom donné à un champ d'octets. Notez la différence avec le label : le label marque une position et la variable désigne autant la position que la taille de ce qui est référé.
On définit une variable comme un label sauf qu'on ne rajoute pas ':' après le nom de symbole. La taille de la variable est la taille du 'Define' qui la suit.
Les variables ne sont pas indispensables mais permettent d'accélérer la frappe des programmes. Par exemple, imaginons que vous vouliez recopier AX à la position ES:h1245. Vous écririez :
MOV ES:[h1245],AX
Maintenant, comme vous ne souhaiteriez pas devoir modifer cette instruction si l'endroit auquel recopier AX est modifié, vous placez un label 'toto' à cette position dans le source. Pour faire la même chose, vous devriez écrire :
MOV ES:[Offset toto],AX
Le mot 'Offset' rajouté devant TOTO permet à l'assembleur de savoir que nous attendons de lui qu'il recopie la valeur 'offset' de l'adresse représentée par le label 'toto' (on aurait tapé 'SEG' pour avoir la valeur 'segment' de cette adresse).
Si nous déclarions plutôt une variable 'var1' de type mot à cette position (var1 DW 0), nous n'aurions plus qu'a écrire :
MOV ES:var1,AX
... ce qui est déjà plus court.
Pour éviter d'avoir à taper 'ES:', on utilise une autre directive de l'assembleur : 'ASSUME'
'ASSUME' permet à l'assembleur de connaître les registres de segment à utiliser devant les références aux variables. Pour cela, on indique :
ASSUME RSeg1:Nom_du_seg1, RSeg2:Nom_du_seg2, etc...
Après ceci, si vous ne spécifiez pas de registre de segment, l'assembleur utilisera celui qui est assumé pour le segment qui contient la variable (MOV var1,AX).
Nous allons maintenant voir un exemple concret de programme qui va nous éclairer sur la mise en pratique de tout ceci.
[ Source : Welcome.ASC ]------------------------------------------ 000 ; Welcome.ASC - Programme 1 001 ; =========== 002 ; Affichage d'un message 003 004 CODE SEGMENT USE16 ALIGN=16 005 $=h100 006 007 MOV DX,Offset Message ; Afficher le message. 008 MOV Ah,9 009 INT h21 010 011 MOV AX,h4C00 ; Mettre fin au programme. 012 INT h21 013 014 Message DB "Hello World !",36 ------------------------------------------------------------------
0..2 : Tout ce qui suit un ";" dans une ligne est considéré par l'assembleur comme étant un commentaire. Nous réservons donc ces trois lignes dans le source pour le titre.
4 : Ouverture d'un segment nomé 'CODE', aligné sur 16 octets et déclaré en mode 16 bits.
5 : On donne la valeur h100 (256) au pointeur d'instruction. Ceci est indispensable car nous allons réaliser un programme 'COM'. L'extention ASC signifie d'ailleurs ASm-Com. On utilisera ASE (ASm-Exe) pour le format de fichier EXE.
Nous reviendrons la fois prochaine sur ces deux formes de fichiers exécutables.
7..9 : On utilise la fonction 9 de l'interruption DOS h21 qui affiche le texte situé en DS:DX. Notez qu'on ne modifie pas DS car il est égal à CS au démarrage d'un programme au format COM.
11 et 12 : On utilise la fonction h4C de l'interruption DOS h21 qui met fin à un programme. On renvoit le code de sortie Al=0.
14 : On associe le symbole 'Message' à la chaîne d'octets représentée par notre texte.
Il faut savoir que l'assembleur considère un caractère entre guillemets ou apostrophes comme la valeur ASCII de ce caractère en octet (Cf. une table de référence ASCII), et un texte entre guillemets ou apostrophes comme la suite d'octets formée par les codes ASCII de la suite de lettre, ce qui est bien pratique.
Le ',36' rajouté à la fin sert à délimiter la fin de la chaîne pour l'afficher avec la fonction 9 de l'interruption h21.
Notez que 36 étant le code ASCII de '$', on aurait pu écrire :
014 Message DB "Hello World !$"
Pour ceux qui possèdent TASM ou MASM, voici la version de ce programme traduit dans la syntaxe MASM :
[ Version MASM ]-------------------------------------------------- ; Welcome.ASC - Programme 1 ; =========== ; Affichage d'un message CODE SEGMENT USE16 ORG 100h start: MOV DX,Offset Message ; Afficher le message. MOV Ah,9 INT 21h MOV AX,4C00h ; Mettre fin au programme. INT 21h Message DB "Hello World !",36 CODE ENDS END start ------------------------------------------------------------------
Vous pouvez constater que la syntaxe instaurée par MASM (TASM est compatible) est plus complexe et moins logique que la nôtre, mais qu'il est tout de même simple de passer de l'une à l'autre.
C'est parceque cette syntaxe est illogique et pénible que j'utiliserai toujours la petite syntaxe que j'ai décrit en début de ce chapitre.
Rixed
================================================================== Pfiouuu, c'était bien l'article le plus long que j'ai jamais écrit pour ce mag. Au prochain numéro, nous travaillerons sur un programme qui affiche des nombres premiers (un nombre premier est un nombre uniquement divisible par lui même et par 1). Vous pouvez donc chercher dès à présent comment vous procèderiez. Pour ce qui est de ces articles : Certains d'entre vous ont imaginé que cette initiation était faite "pour apprendre le 386". Mon but, c'est plutôt "apprendre l'assembleur tout court". J'espère qu'à la lecture de ce qui précède les lecteurs dans l'erreur auront rectifié d'eux mêmes. J'essaye de trouver un compromis entre DOS et 386, mais ce n'est pas simple. ============================[enFIN]===============================
PiedDePage(); ?>