Initiation à l'assembleur

    Nous allons ici présenter le langage de programmation assembleur sur PC. Pour bien comprendre cet article, il faut avoir quelques connaissances en d'autres langages de programmation (le basic suffit à mon avis). Cet article est très théorique et ne présente donc que très peu d'exemples. Vous avez donc besoin surtout de votre tête pour l'instant, nous verrons dans de futurs articles la programmation sous TASM, MASM ou A86. Nous traiterons donc les sujets suivant:

    1) Généralités:

    2) Les instructions:

    Bibliographie

    Annexe: les différentes bases

    Conclusion

    Ces thèmes ne traitent pas toutes les instructions assembleur PC, mais un certain nombre.

    1) Généralités:

    Les registres du processeur 80386:

    Registres généraux

    Registres de contrôle et d'état

    Registres de segments

    31..............15.........7.......0 bit n°

    31..............15..................0 bit n°

    15.................0 bit n°

    [Le 386 possède d'autres registres, mais ceux-ci ne sont utilisés que pour le mode protégé. Ce cours présentera la programmation assembleur en mode réel (mode standard), le mode protégé étant réservé à la programmation de systèmes multitâches.]

    C'est bien beaux tous ces tableaux, mais c'est quoi un registre et ça sert à quoi ? Les registres servent en fait à envoyer des informations directement au processeur. Considerez les registres comme des variables qui sont toujours là, non pas que leur valeur reste inchangée, bien au contraire, mais ils ont toujours accessible. C'est avec ces registres que vous allez pouvoir envoyer des informations au processeur et vice-versa, le processeur pourra vous informer de l'état d'une zone mémoire, d'une erreur, etc... Ces registres sont vitaux pour gérer les entrées / sorties.

    Il existe plusieurx types de registres: les registres quon appelera "généraux", les registres d'état et de contrôle et les registres de segments. Les seuls que vous pouvez toucher sans risquer de faire planter votre ordinateur sont les registres EAX, EBX, ECX, EDX, ESI et EDI. Tous les autres sont à prendre avec des pincettes ! Une petite modification d'un registre de segment peut donner un plantage clair et net du système... C'est là la différence entre l'assembleur et les langages de haut niveau (Basic, Pascal, C/C++, ADA, ...). En Basic, vous pouvez toucher à (presque) tout sans avoir de plantage, alors qu'en assembleur, il faut être sûr de ce que l'on veut faire.

    Une petite explication sur les registres: Comme vous le voyez sur le tableau précedent, les registres "généraux" ont tous 32 bits (4 octets). C'est à dire qu'on peut stocker une valeur allant de 0 à 2^32-1 (environ 4 milliards). Mais on peut accéder à des plus petites parties de ces registres: partie 8 et 16 bits des registres: ce sont les AL,AH, AX... Pour EAX, par exemple, la partie 16 bits est AX, et les parties 8 bits sont AL et AH. Mais AH fait partie de AX qui fait lui-même partie de EAX... En clair, quand vous modifiez AL, le premier octet de EAX est modifié; quand vous modifiez AH, le deuxième est changé, et quand vous modifiez AX, les deux premiers octets de EAX. Mais comment modifier les deux derniers octets de EAX sans toucher au reste ? Eh bien, c'est impossible ! En tous cas, il n'existe pas de registre qui représente les deux derniers octets de EAX. Ce n'est pas un oubli de Intel, c'est juste que sur les processeurs avant 286, il n'existait que AH, AL et AX, et ils n'ont pas voulu recréer d'autres registres. Les registres ont juste été "agrandis". Pour ma part, je ne trouve pas ça très pratique, mais bon...

    Compris ? les registres AL, AH, BL, BH, CL, CH, DL, DH sont modifiables à souhait et font 8 bits. Les registres AX, BX, CX, DX, SI et DI sont aussi modifiables et font 16 bits.

    Les registres EBP et ESP servent à la gestion de la pile, mais on y reviendra plus loin...

    Les registres EIP et EFLAGS sont très délicats à manipuler. Le registre EIP correspond à l'adresse d'offset de l'instruction en cours et le registre EFLAGS est là pour indiquer les résultats de différents tests. On y reviendra....

    Les registres de segments sont moins délicats, mais à traiter quand même en connaissant les effets secondaires d'une modification. Le registre CS n'est de toutes façons pas modifiable par vous, seulement par le processeur lui même, comme le registre EIP. Tous les registres de segment contiennet une valeur qui indique l'adresse d'un segment en mémoire (sans blague ?). En fait, un programme doit accéder à une zone mémoire par ces registres.

    Affectation de valeurs:

    Mais, comment modifier un registre ? C'est très simple: avec l'instruction MOV. C'est l'instruction de base de l'assembleur. Elle sert à affecter une variable ou un registre d'une certaine valeur qui peut être directe ou indirecte (zone mémoire). On écrit: MOV destination, source. Exemple: MOV EAX, 3 met 3 dans le registre EAX. C'est maintenant que nous allons voir les différents mode d'adressage.

    L'adressage immédiat est une valeur constante. Exemple: 3, 4, 5, 0ffh, 21h...
    L'adressage direct est un registre 8,16 ou 32 bits: EAX, BX, SI, EBP .... ou une valeur constante.
    L'adressage basé est la valeur d'une zone mémoire précise: le 386 admet le format suivant pour un adressage basé:
    [base + index * n]. base et index : registre général 32 bits ou aucun. n: 1,2,4 ou 8.

    Cet adressage permet de se déplacer plus vite dans des tableaux de BYTE (8 bits) , WORD (16 bits) ou DWORD (32 bits) avec le cpefficient multiplicateur n. Ce crochet peut être précédé par le segment auquel on veut accéder suivit du signe ":". Exemple: On veut modifier le 25ème octet du segment DS, on adressera de cette façon: DS:[25]. Maintenant on veut lire un tableau de DWORD se trouvant à partir du 25ème octet du segment DS, sachant que EAX prend les valeurs successives 1,2,3,4... on adressera donc de la sorte: DS:[25+EAX*4]. Dans un MOV et dans presque toutes les instructions faisant référence à une zone mémoire, on peut se servir de ces différents types d'adressage. Cet adressage pourra donc être utilisé pour la source ou la destination d'un MOV, mais jamais les deux.

    Les exemples suivants sont corrects: MOV FS:[EAX+EBX*2],DX met la valeur de DX dans la "case" mémoire numéro EAX+EBX*2 du segment dont l' adresse est dans FS; en clair: si FS est égal à 1234h (le "h" veut dire "base hexadécimale"), EAX à 8, EBX à 12 et DX à 4, alors cette instruction écrira 4 dans la zone mémoire 1234h:52d (8+12*2) (le "d" veut dire "base décimale"). MOV AX, ES:[EBX+ESI] met la valeur de la case mémoire numéro EBX+ESI dans le registre AX.

    Les exemples suivants sont incorrects: MOV DS:[EAX+2], FS:[EDX] car il contiennent la source et la destination sont des adressages basés, il faudra alors passer par un registres intermédiaire pour effectuer cette opération. MOV 3, DS:[ESI] est incorrect car cela voudrait dire qu'on met la valeur d'une zone mémoire dans une constante: c'est impossible. Et bien sûr MOV 3, 4 est aussi impossible.

    Les registres de segment ne sont pas modifiables immédiatement par l'instruction MOV, ni par un adressage basé, un registre de segment n'est modifiable que directement, c'est à dire par un registre seulement. Exemple: il est interdit de faire MOV DS, 2578. Par contre on peut faire MOV AX, 2578 puis MOV DS, AX pour résoudre ce problème. Tous les registres de segment sont modifiables de cette façon, sauf CS, qui lui est modifié automatiquement par le processeur lors d'un saut.

    Tous les registres sont modifiables sauf CS donc, EIP et EFLAGS, qui sont modifiés automatiquement par le processeur.

    Une petite précision pour l'adressage immédiat:

    Si vous faites MOV ES:[EAX+EBX*2], 4 l'assembleur ne sait pas s'il doit écrire la valeur comme un BYTE (04h), un WORD (0004h) ou un DWORD (00000004h). Il faut alors lui specifier par un préfixe devant l'adressage basé: "byte ptr" pour les BYTEs, "word ptr" pour les WORDs et "dword ptr" pour les DWORDs. Ce qui donne par exemple, si on veut écrire le nombre 0012h (WORD) dans l'adresse ES:1234h: MOV WORD PTR ES:[1234h], 12h. Ceci n'est obligatoire que pour les adressages immédiat, mais le processeur connait la taille de ces registres, vous n'avez donc pas à mettre de préfixe de taille pour l'instruction suivante: MOV ES:[EAX+EBX], CX

    En bref:

    Un adressage basé est possible pour toutes les instructions faisant référence à une zone mémoire.
    On ne peut pas avoir plus d'un adressage basé par instruction.
    On ne peut pas modifier les registres de segment immédiatement ou par adressage basé, il faut le faire directement.
    Tous les registres de segment sont modifiables, sauf CS, EIP et EFLAGS.
    N'oubliez pas le préfixe de taille devant les adressages basés.

    Cette partie est très importante, n'hésitez donc pas à la relire de temps en temps. J'ai essayé d'être le plus clair possible, seulement je connais déjà ce langage, et pour moi, il y a maintenant des choses qui me paraissent évidentes, et qui ne le sont peut-être pas forcément pour vous.

    Déclaration des variables:

    Il existe trois principales sortes de variables en assembleur: les BYTEs, nombres 8 bits que l'on déclare par DB, les WORDs, nombres 16 bits que l'on déclare par DW , les DWORDs ou DOUBLE WORDs, nombres 32 bits que l'on déclare par DD et les chaînes que l'on déclare par un DB. Il faut déclarer de cette façon: nom_variable DB|DW|DD valeur avec nom_variable qui est bien sûr le nom de la variable DB, DW ou DD le type et valeur la valeur d'initialisation. (Note: valeur peut être "?", dans ce cas là, les variables ne sont pas initialisées). Exemples:

    indice DB 0 ; un BYTE nommé "indice" qui s'initialise à zéro.
    pointeur DD 1234h ; un DWORD nommé "pointeur" qui s'initialise à 1234h.

    Il est aussi possible de créer des tableaux avec l'instruction DUP. La syntaxe devient nom_variable DB|DW|DD taille_tableau DUP(valeur) avec nom_variable toujours le nom de la variable tableau, DB, DW ou DD le type de chaque élément du tableau et valeur, la valeur d'initialisation. Exemples:

    tableaub DB 25 dup(?) ; tableau de 25 BYTEs
    tableaud DD 50 dup(20) ; tableau de 50 DWORDs (=200 BYTEs). Chaque est initialisée à 20.

    Note: on peut aussi déclarer aussi des sortes de listes de valeurs en séparant chaque valeur par une virgule. Exemple:

    liste1 DB 0,1,2,3,5,12,6 ; liste de 7 valeurs.

    C'est de cette manière que l'on crée des sortes de chaînes de caractères.

    chaine db 'Bonjour tout le monde' ; simple chaîne

    Et on peut "mixer" les deux:

    chainespec db 'bonjour',07,'$',0dh

    Voilà pour les différents types de variables. C'est bien beau tout ça, mais on y accède comment à ces variables ?

    Exemple: on veut modifier la variable pointeur déclarée en DWORD (voir plus haut) et y mettre 100h à la place.... Eh bien, on fait tout simplement un MOV dword ptr CS:[pointeur],100h eh oui, ici il faut mettre aussi le préfixe de taille, car malgrè que vous ayez déclarer la variable plus haut, le processeur ne reconnait pas la taille. Quand vous déclarer une variable, tout ce que fait l'assembleur, c'est de vous faciliter la tâche, car normalement, une variable est finalement une zone mémoire, on est d'accord ? Donc, c'est l'assembleur (le compilateur) et non le processeur qui "retient" la taille (DB|DW|DD) pour pouvoir initialisé 1,2 ou 4 octets dans votre fichier de sortie. C'est un peu compliqué ? c'est normal. Ce n'est pas évident à comprendre au début, mais si on a compris ça, on a compris le plus gros des problèmes en assembleur: les adresses. On reprends: vous avez dans votre fichier, les lignes suivantes:

    ...
    pointeur DD 1234h
    ...
    MOV dword ptr CS:[pointeur], 100h
    ...

    (les "..." sont là pour symboliser qu'il faut normalement d'autres instruction pour compiler un programme)

    En fait, l'assembleur va lire le fichier et ajouter dans le fichier de sortie (.COM ou .EXE) les octets 00 00 12 34 (héxa) quand il va tomber sur la variable pointeur, et il saura maintenant que si il y a plus tard un appel à pointeur, il faudra qu'il transmette l'adresse du premier octet de pointeur (00). C'est pour ça que le processeur a besoin de connaitre absolument la taille de la variable à manipuler.

    Deuxième question: Mais pourquoi CS ?... Parce que la variable se trouve dans le même segment que le code, segment de code qui est par défaut CS. Pour mieux comprendre, voici le même programme, mais cette fois-ci compilable...

    code segment

      assume cs:code

      jmp debut

    pointeur DD 1234h
    debut:

      mov dword ptr CS:[pointeur], 100h

    code ends
    end

    Ce programme est executable, mais plantera si vous le "linkez". (compiler avec MASM ou TASM).

    Structure du programme:

    On remarque tout d'abord en première ligne le "code segment", en fait on déclare ici un segment qui s'appelle "code" (original), ce segment contiendra le code du programme. Ensuite un "assume cs:code" qui est là pour assurer que CS refere au segment code (pour les sauts). On trouve après l'instruction jmp qui saute vers l'étiquette "debut" (GOTO en Basic). Cela e pour effet de sauter la zone de variables. Pourquoi les sauter ? Parce que si on ne les sautait pas, le programme executerait les instruction 00, 00, 12h puis 34h du langage machine, qui ne veulent peut-être rien dire dans ce contexte et apporterais le plantage. (De toutes façons, ce programme plante, parce qu'il ne quitte pas !). On trouve enfin ensuite notre instruction mov suivie de "code ends" qui marque la fin du segment et "end" qui marque la fin du programme. Il marche avec TASM, mais il doit marcher avec MASM.

    Mieux compris ? j'espère ...

    Maintenant, on veut accéder au troisième octet d'une chaîne de 10 octets. Mettons que la chaîne s'appelle "ch2"; on y accédera de la façon suivante: CS:[ch2+3]. Encore une fois, cet adressage ne peut apparaître qu'une fois par instruction, c'est en fait un adressage basé.

    Une ligne assembleur:

    Une ligne d'instructions assembleur peut contenir au maximum 4 champs et au minimum 1: une étiquette, une mnémonique, des opérandes, des commentaires. Ceci peut se restreindre à 1 champ: la mnémonique qui est en fait l'instruction assembleur. Les étiquettes srevent comme en Basic de faire une sorte de "GOTO" vers une partie du programme (GOTO fait par l'instruction jmp). Les opérandes sont par exemple deux registres entres virgules dans le cas d'un mov ou une adresse mémoire... Les commentaires doivent commencer par le sign ";" et n'être qu'en fin de ligne.

    Les comparaisons et les sauts comme tests de conditions:

    Vous vous souvenez sûrement de la programmation GWBASIC pleine de GOTO et d'étiquettes, c'était illisible non ? eh bien en assembleur c'est pareil ! C'est pourquoi l'assembleur ne doit être utilisé que pour écrire un programme impossible dans d'autres langages (dans très peu de cas) et surtout d'améliorer la rapidité d'une routine ou d'une boucle appelée très souvent. En fait les seules conditions qui existent en assembleur peuvent être assimilées au "IF" en basic mais avec des "GOTO" seulement. En clair, pour faire des boucles, il faudrait tester pleins de conditions.

    Une instruction de condition se décompose donc en deux instructions: une comparaison et un saut. L'instruction principale de comparaison en assembleur est CMP. Il s'utilise comme un MOV: CMP variable_à_analyser, valeur avec variable_à_analyser qui représente donc la variable mémoire ou le registre à comparer et valeur la valeur, le registre ou la variable mémoire avec laquelle comparer. Si on veux savoir si AX contient 2 par exemple, on écrira CMP AX,2. Bien sûr comme avec les MOV on pourra faire des adressages basés et on devra faire des wodr ptr dans certains cas. Mais qu'est ce qu'on en fait de ce CMP ?

    Le résultat de la comparaison est renvoyé dans le registre spécial de FLAGS (drapeau en français) qui n'est pas accessible directement. Comment faire pour savoir si AX est égal à 2 dans notre exemple ou pas ? C'est là qu'interviennent les sauts. Les sauts conditionnels vont regarder le registre FLAGS et se brancher vers d'autres instructions dans le cas d'une égalité, d'une inégalité, dans le cas où le nombre comparé est plus grand, plus petit, négatif...

    Le tableau des différents sauts:

    A<>B JNE (Jump if not Equal) ou JNZ
    A=B JE (Jump if Equal) ou JZ

    Nombres non-signés:

    A>B JA (Jump if Above)
    A>=B JAE (Jump if Above or equal)
    A<B JB (Jump if Below)
    A<=B JBE (Jump if Below or equal)

    Nombres signés:

    A>B JG (Jump if Greater)
    A>=B JGE (Jump if Greater or Equal)
    A<B JL (Jump if Less)
    A<=B JLE (Jump if Less or Equal)

    C'est pas compliqué ? si un peu...

    En fait les sauts testent des bits spécifiques du registre FLAG, mais n'en tenait pas compte pour l'instant, nous en reparlerons plus tard.

    Structure générale d'un test:

        cmp ax, 5

        je ok_5 ;si c'est 5, on va en ok_5

        jmp apres ; sinon on sort de la boucle

      ok_5:

        mov bx,ax ; on recopie ax dans bx (exemple)

        jmp apres

      apres:

        mov cx, ax ; on recopie ax dans cx (exemple)

    Comment faire des FOR, WHILE, LOOP ...

    Pour un FOR, la méthode la plus simple est de mettre le nombre d'incrémentations dans cx:

      mov cx, 1

    boucle:

      ...

      inc cx ; on décremente cx

      cmp cx, 10 ; 10 répetitions

      jne boucle ; si c'est pas fini, on recommence

      ...

    On voit ici une nouvelle instruction: INC qui sert à incrémenter une variable de 1. Son complémentaire est DEC qui permet de décrémenter une valeur.

    Pour les WHILE et les LOOP, la syntaxe est la même à peu de chose près, le cmp change un peu, et il n'y a pas d'incrémentation.

    On peut aussi faire une boucle simple grâce à l'instruction LOOP. Elle s'utilise avec une étiquette en opérande. Cette instruction va faire que le programme va sauter à l'étiquette tant que CX est différent de 0.

    Exemple:

      mov cx,10

    boucle:

      inc si

      mov es:[si],ax

      loop boucle

    On peut aussi effectuer des LOOP conditionnels avec LOOPE et LOOPNE, qui opère quand le résultat d'une comparaison est positif ou négatif (voir la manipulation de la mémoire)

    La pile:

    La pile est un espace pour ainsi dire imaginaire qui sert à sauvegarder et charger des valeurs. Ceci est très utile, car bon nombre de programme modifient des registres qui sont nécessaires au système d'exploitation ou autres. Il est très recommandé, par exemple de toujours sauvegarder les registres de segments avant de s'en servir pour des accès mémoire. Les instructions principales de la pile sont PUSH et POP, PUSH pour stocker, POP pour charger. PUSH|POP variable où variable est un registre, une variable mémoire ou une valeur constante (à partir du 286). On trouve la commande PUSHA et POPA qui permettent de stocker et récupérer tous les registres d'un seul coup, SAUF les registres de segment. Ces instructions s'appellent sans opérandes.

    Fonctionnement de la pile: pourquoi le terme de pile ? pas pour le sens de batterie, mais pour le sens de pile d'assiettes. Imaginez une pile d'assiettes. Vous ajoutez une assiette numérotée 1, puis une deuxième numérotée 2. Pour récupérer la n° 1 il faudra d'abord enlever la 2. Voilà en simplifié comment marche la pile. Le premier rentré est le dernier sorti. Vous voulez stocker les registres AX, BX, CX et DX (dans l'ordre), vous faites donc PUSH AX; PUSH BX; PUSH CX; PUSH DX. Et pour les récupérer en rechargeant les bonnes valeurs: POP DS; POP CX; POP BX; POP AX. Voilà.

    D'un point de vue technique, la pile est représentée par une zone mémoire pointée par SS avec le premier élément pointé par SS:BP et le dernier pointé par SS:SP. Attention: la pile n'admet que des registres 16 bits ou valeurs 16 bits.

    2) Les instructions:

    Le but de cette partie n'est pas de présenter les instructions en détail une par une, mais de les présenter par groupe: manipulation de la mémoire, opérations arithmétiques et logiques, etc ...

    Manipulation de la mémoire:

    Pour bien comprendre les opérations de manipulation de la mémoire, il faut d'abord comprendre comment est organisé la mémoire. En mode réel, il est adressable 1Mo de mémoire, c'est à dire que vous ne pouvez pas accéder à la mémoire contenue au dessus de 1Mo, même si vous avez 32 Mo. C'est pour cela qu'il a été inventé des bidouilles permettant d'y pénetrer en mode réel: l'EMS ou l'XMS.

    En mode réel, un segment contient un paragraphe ou 16 octets. Mais une adresse s'écrit sous la forme xxxx:xxxx en héxa. En fait, le segment 0000:0010 équivaut à 0001:0000 et 0025:0100 équivaut à 0035:0000. C'est pas évident à expliquer, mais avec un peu d'expérience, vous vous en rendrais compte, et vous comprendrais mieux.

    Les instructions les plus pratiques pour manipuler la mémoire sont les suivantes:

    MOVSB, MOVSW, MOVSD: copie une zone (1, 2 ou 4 octets) de DS:SI vers une zone (1, 2 ou 4 octets) de ES:DI

    CMPSB, CMPSW, CMPSD: compare une zone de DS:SI avec une zone de ES:DI

    SCASB, SCASW, SCASD: compare AL, AX ou EAX (suivant la taille) avec une zone de ES:DI

    LODSB, LODSW, LODSD: charge dans AL, AX ou EAX la valeur d'une zone de DS:SI

    STOSB, STOSW, STOSD: charge dans une zone de ES:DI, la valeur de AL, AX ou EAX

    Les instructions de terminant par "B" manipulent des BYTEs, celles se terminant par "W" des WORDs et par "D" des DWORDs. La source est soit DS:SI, soit AL, AX ou EAX suivant la taille et la destination est soit ES:DI, soit AL, AX ou EAX suivant la taille. L'avantage de ces instructions est que les registres SI et/ou DI sont incrémentés de 1, 2 ou 4 suivant la taille de la zone à traiter. Les comparaisons affectent le registre FLAGS qui permettra de faire des sauts conditionnels.

    Imaginons que vous ayez à copier une zone de 64000 octets à copier vers une autre zone (la mémoire vidéo par exemple), vous devrez donc repeter 64000 fois un MOVSB en mettant les bonnes valeurs dans DS, SI, ES et DI. Ce n'est pas très pratique. Vous pourriez faire une sorte de FOR avec des sauts conditionnels (voir plus haut). Mais la méthode la plus pratique et la plus rapide est l'instruction REP. REP repète une seule instruction CX fois. On a l'habitude d'écrire "REP instruction", tous les assembleurs digne de ce nom le reconnaisse. Voud faîtes donc comme avec un MOVSB, mais en mettant 64000 dans CX. C'est quand même plus simple non ?

    Mais comment faire pour repeter des instructions de comparaison (CMPSB par exemple). Il existe pour cela deux instructions REPE et REPNE qui repètent JUSQU'A CE QUE le résultat de la comparaison soit positif (REPE) ou négatif (REPNE). C'est très pratique pour trouver la longueur d'une chaîne en mémoire par exemple.

    Nous avons vu que les registres SI et/ou DI étaient incrémentés après ces instructions. Il est possible de les faire décrémenter avec l'instruction STD. Pour les refaire incrémenter, il faudra utiliser CLD.

    Les opérations arithmétiques:

    Pour bien comprendre le système d'opérations arithmétiques, il faut savoir que le processeur distingue deux types de nombres: les nombres signés et les nombres non-signés. Les nombres signés sont en fait des nombres qui peuvent être positifs ou négatifs. Les nombres non-signés sont ceux qui ne peuvent être que positif. Pour coder le signe du nombre, le processeur utilise le dernier bit: si un BYTE a son 7ème bit à 1, c'est que c'est un nombre négatif quand on travaille en signé, sinon il est considéré comme un nombre non-signé supérieur ou égal à 128 (2^7=128).

    Il est possible en assembleur d'effectuer toutes les opérations de bases sur les nombres entiers (les nombres à virgules ne sont disponibles que par le coprocesseur): l'addition, la soustraction, la division, la multiplication.

    ADD: addition de deux termes non-signés

    SUB: soustraction de deux termes non-signés

    INC: Incrémzntation de 1 d'une valeur

    DEC: Décrémentation de 1 d'une valeur

    Pour la multiplication, c'est plus complexe:

    MUL: multiplication d'une valeur non-signée par AL ou AX. Le r‚sultat est stocké respectivement dans AX ou DX et AX

    IMUL: même chose sur les nombres signés

    DIV: division d'une valeur non-signée stockée dans AX ou DX et AX par une valeur 8 ou 16 bits. Le résulat est stocké respectivement dans AL ou AX et le reste respectivement dans AH ou DX.

    IDIV: même chose sur les nombres signés.

    Quand on dit "dans DX et AX" cela veut dire que le premier mot est stocké dans AX et si le résultat dépasse 65535, la suite est stockée dans DX.

    Remarque: comment additionner ou soustraire des valeurs signées ? Il faut pour cela utiliser l'instruction NEG qui inverse le signe de l'opérande.

    -xxxx + yyyy = - (xxxx - yyyy). Dans le cas où vous voulez ajouterune valeur positive à une valeur négative, vous faîtes d'abord un NEG sur les deux opérandes, vous les soustrayez et vous faîtes un NEG sur le résultat. Normalement, ça devrais marcher. Pour soustraire une valeur postive d'une valeur négative, il faut faire une addition, etc... La solution pour ces problèmes et de poser l'opération et de n'avoir que des opérandes positives.

    Dépassement de capacité: si vous ajoutez 255 à un BYTE, vous êtes pratiquement sûr de dépasser son interval (0..255), donc le processeur ne va pas s'arrêter, mais va modifier un bit du registre FLAG: le bit CF, qui correspond au bit nommé "carry flag". Dans ce bit, on trouve en fait le neuvième bit du résultat. Attention: le bit CF n'est affecté qu'en arithmétique non-signée. En arithmétique, c'est au programmeur de se débrouiller pour ne pas commettre de fautes: le processeur ne vous avertira pas. Pour tester si ce bit est à 1, l'instruction JC effectue un saut dans ce cas.

    Je sais, tout ceci est un peu "repoussant" pour le début, mais vous allez voir qu'après avoir écrit quelques programmes ASM, ce langage est très puissant pour la manipulation de la mémoire. Sa scpécialité n'est évidemment pas les calculs.

    Un petit truc: pour effetuer très rapidement des multiplications non-signées par des puissances de 2, utilisez SHL ou SHR (voir plus bas)

    Les opérations logiques:

    Il faut savoir que les opérations logiques travaillent sur les bits et non plus sur des nombres. Chaque bit d'un nombre est traîté un par un et sur chaque bit il est opéré une modification suivant l'opération utilisée. On compte quatres opérations principales: AND, OR, XOR et NOT.

    L'opération AND ou ET LOGIQUE opère de la manière suivante (pour chaque bit) qu'on appelle table de vérité:

    0 AND 0 = 0

    0 AND 1 = 0

    1 AND 0 = 0

    1 AND 1 = 1

    Donc 01101100 AND 00001001 = 00001000. L'opération servira à isoler des bits d'un nombre. On voit ici que cette opération n'a recopié que les bits où il y avait des 1, les autres ont étés ignorés.

    Voici la table de vérité de l'opération OR ou OU LOGIQUE:

    0 OR 0 = 0

    0 OR 1 = 1

    1 OR 0 = 1

    1 OR 1 = 1

    Donc 01101100 OR 00001001 = 01101101 en utilisant la table de vérité. L'opération OR sert à mettre un bit à 1 qu'il y soit à 0 ou à 1. Exemple: 11110100 OR 00000001 = 11110101 ne change que le dernier bit.

    Voici la table de vérité de l'opération XOR ou OU EXCLUSIF:

    0 XOR 0 = 0

    0 XOR 1 = 1

    1 XOR 0 = 1

    1 XOR 1 = 0

    Donc 01101100 XOR 00001001 = 01100101 en utilisant la table de vérité. L'opération sert très fréquemment à mettre une valeur à zero: 10101010 XOR 10101010 = 00000000. Donc xxxxxxxx XOR xxxxxxxx = 00000000. N'oubliez pas cette particularité de XOR, elle est très utilisé dans les programmes, car très rapide (beaucoup plus qu'un MOV AX,0, on fera un XOR AX, AX).

    La dernière opération est NOT ou NON LOGIQUE qui n'utilise qu'une opération. Voici sa table de vérité:

    NOT 0 = 1

    NOT 1 = 0

    L'opération NOT inverse simplement les bits, ainsi NOT 01101100 = 10010011.

    La syntaxe pour toutes ces opérations, exceptée NOT est:

    AND|OR|XOR valeur1, masque Ce qui équivaut à faire valeur1 AND|OR|XOR masque.

    A part l'instruction NOT, toutes ces opérations modifient le registre FLAG, donc vous pourrez effectuer des sauts conditionnels, mais seulement dans le cas où le résultat est égal à zéro (ou différent de zéro donc). Ainsi, pour tester si un registre est égal à zéro sans passer par un CMP, on pourra faire un OR AX, AX par exemple. Cette opération ne modifiera pas AX, mais on pourra effectuer des sauts conditionnels. Un CMP est beaucoup plus lent qu'un OR.

    Les instructions de décalage et de rotation:

    Les instructions de décalage, comme leur nom l'indique, décalent les bits de l'opérande. On distingue deux types de décalage: à gauche et à droite.

    L'instruction SHL décale les bits de l'opérande vers la gauche (L comme left). Ansi, SHL AX,3 décale les tous les bits de AX de 3 vers la gauche. Les bits sont décalés en insérant des zéros. Donc si AX vaut au départ 10101101, il vaudra après le SHL de 3, 01101000.

    L'instruction SHR décale les bits de l'opérande vers la droite (R comme right). On l'utilise de la même manière que SHL.

    Avec ces instructions, on peut effectuer très facilement des multiplications (SHL) ou des divisions (SHR) entières par des puissances de 2. Si vous voulez multiplier AX par 16 (2^4), vous pourrez faire un SHL de 4. Ces instructions sont beaucoup plus rapides que les multiplications avec MUL ou IMUL (vraiment plus rapide). Donc, n'hésitez pas à vous en servir dès que vous pouvez, l'assembleur étant utilisé la plupart du temps pour accélerer des routines.

    Les instructions de rotation de bits, font un décalage en réinsérant les derniers bits en première position.

    L'instruction ROL fait "tourner" les bits vers la gauche. Les bits "sortants" sont "réinjectés" dans les premiers bits. Ansi ROL AX,3 quand AX vaut 10101101 donnera: 01101101.

    L'instructions ROR fait "tourner" de la même manière les bits vers la droite.

    Annexe: Les différentes bases:

    Ceci présente très rapidement les différentes bases utilisés.

    Les nombre décimaux, que nous utilisons tous les jours peuvent être décomposés comme ceci:

    12546 = 1*10000 + 2*1000 + 5*100 + 4*10 + 6*1

    En fait, on remarque que 1, 10, 100, 1000 et 10000 sont des puissances de 10, respectivement 10^0, 10^1, 10^2, 10^3 et 10^4. Les puissances de 10 sont utilisées en base 10, ou base décimale. Maintenant si on prend une base de 2, on aura que des puissances de 2: 1, 2, 4, 8, 16, 32, 64, 128, ... L' "alphabet" des chiffres est donc maintenant réservé à 0 et 1. Donc pour représenter 20 en binaire, on fera: 20 = 1*16 + 0*8 + 1*4 + 0*2 + 0*1, ce qui donne le nombre 10100 en binaire. Si on prend des puissances de 16, on aura 1, 16, 256, ... et l' "alphabet des chiffres" sera étendu à 16, l'inconvénient est qu'il n'y a que 10 chiffres dans notre système. Pour les 6 autres manquants, on prendra les premières lettres de l'alphabet, on comptera donc maintenant de cette façon: 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, 10, 11, ... Le nombre 20 s'écrit donc: 20 = 1*16 + 4*1, ce qui donne 14 en notation héxadécimale.

    On a pris l'habitude de noter les nombres décimaux terminés par un "d", les nombres binaires terminés par un "b" et les nombres héxadécimals terminés par un "h". L'assembleur reconnait ces notations; vous pourrez donc écrire un nombre de trois façons en assembleur.

    Donc, quand on parle de nombre à 8 bits, leur maximum est : 11111111 = 1*128 + 1*64 + 1*32 + 1*16 + 1*8 + 1*4 + 1*2 + 1*1 = 255 = (2^8)-1. Les nombres à 16 bits ont un maximum de (2^16)-1 = 65536.

    Mais, comment fait on pour les nombres négatifs ? Un nombre peut avoir deux états: positif ou négatif. Le dernier bit d'une variable (8, 16 ou 32 bits) est utilisé pour noter le signe. Si celui-ci est à 1 le nombre est négatif, sinon il est positif. On distingue deux types d'arithmetique en assembleur: signée et non-signée. La prmière tient compte du signe et code donc maintenant sur 1 octet, 7 bits pour la valeur et 1 bit pour le signe; ce qui fait varier ses extremum de -127 à 128. En arithmétique non-signée, on peut aller jusqu'à 255 sur 1 octet.

    Bibliographie:

    Assembleur facile - Philippe Mercier - Marabout Informatique n°885 (pour les bases et les instructions)

    La Bible PC Programmation Système, sixième édition - Michael Tischer - Micro Application n°1544 (pour les registres 286+)

    Conclusion:

    Cet article a donc traité des bases de la programmation assembleur sur PC. Nous verrons dans de futurs articles comment mettre en place des procédures, comment appeler des routines assembleur dans vos programmes Pascal, C et peut-être Quick Basic.

    A bientôt.

    Informations sur l'auteur:

    Je suis actuellement elève de 1ère Scientifique à Bernay dans l'Eure (27). Je m'intéresse donc à la programmation depuis longtemps et aux jeux de rôles. Je suis en fait le "rédacteur en chef" de cet e-zine. Je cherche des docs sur le Direct X en français, le CGI, les réseaux neuronaux et l'ésotérisme (pouvoirs psy, magie, phénomènes étranges...)

    E-mail: discase@mygale.org
    Page Web: http://www.home.ch/~spaw1372/index.htm