L'objectif de cette page est de vous permettre de vous familiariser assez vite avec le langage d'assemblage de la série 680x0 (et par là même à la structure de tous les langages de d'assemblage).
Ensuite, nous aborderons progressivement en détail les premières notions à maîtriser sur le processeur 68010.
Le microprocesseur d'un ordinateur est la partie centrale de celui-ci. C'est à travers lui et avant tout autre chose que les programmes s'exécutent. Le microprocesseur cependant ne connaît qu'un seul langage : celui qui lui est propre, le langage machine. On donne communément le nom du processeur comme nom du langage machine qu'il comprend. Le 68010, par exemple, comprend donc... le 68010. De plus, les processeurs sont généralement compatibles avec leurs prédécesseurs ; dans le cas présent, le 68000.
Le langage machine est l'association de séquences d'octets à des actions (bien-sûr seul un certain nombre de séquences ont un sens). Pour mieux comprendre, revenons au fondement de ce qu'est un programme.
Un programme est une association entre code (instructions ou actions) et données. Plus précisement, un programme est une séquence d'instructions qui vont être exécuter à partir d'une mémoire à partir d'une adresse de départ et dont les instructions vont interagir plus ou moins directement avec les données se trouvant aussi en mémoire. De plus, certaines instructions permettent de briser le flot d'instructions pour le brancher en une autre adresse (avec les instructions de branchements ou de sauts, ainsi qu'avec les instructions d'appels à des sous-routines ou sous-programmes) ; d'autres permettent de contrôler le mode de fonctionnement du microprocesseur (comment le CPU doit-il réagir aux évènements extérieurs par exemple).
|
|
|||||
|
||||||
Sans rentrer dans les détails, une instruction est une très courte séquence d'octets dont une partie renseigne le CPU sur l'action à effectuer (OpCode, Operation Code) et dont une autre partie éventuelle renseigne sur quelles données faire porter l'action quand ce n'est pas déjà sous-entendu (opérandes).
Il est bien-sûr théoriquement possible de programmer en langage machine, mais on a trouvé un moyen plus facile pour un être humain de manipuler ces instructions : leur traduction en langue humaine en utilisant des mnémoniques, c'est-à-dire que l'on donne des noms aux instructions en rapport avec l'action effectuée. Il s'agit du langage d'assemblage. Bien-entendu, comme il y a un langage machine par CPU, il y a aussi un langage d'assemblage par CPU et bien-sûr on retrouve les mêmes compatibilités dans cette dernière catégorie de langages que dans la précédente. Le langage d'assemblage du 68010, par exemple, permet donc de programmer en langage d'assemblage du 68000. Ensuite, on utilise un programme qui permet de traduire les sources écrits en langage d'assemblage vers le langage machine ; un tel programme s'appelle un assembleur.
N.B. :Il est dans l'usage de pratiquer l'abus de langage qui consiste à employer le terme assembleur à la place de l'expression langage d'assemblage. Essayez dans tous les cas néanmoins de ne pas confondre ces deux notions. Le langage d'assemblage (en anglais : assembly language ou assembly) est le langage de programmation dans lequel on écrit le programme, alors que l'assembleur (en anglais : assembler) est le programme qui traduit le source écrit en langage d'assemblage vers le langage machine.
La première idée que l'on pourrait avoir est de donner l'adresse en mémoire des données à utiliser. Cette solution est malheureusement mauvaise. On a plutôt recours à ce que l'on appelle les registres (en anglais : registers). Les registres sont de petits espaces mémoires directement situés dans le CPU et donc directement accessibles (c'est fait pour !!) par les instructions du CPU. Mais, pourquoi l'accès direct à la mémoire est-il une mauvaise solutions ? Pour plusieurs raisons :
Vous vous demandez donc du coup, comment on fait pour mettre des données dans les registres si on peut pas faire d'accès direct à la mémoire. Et bien non ! On peut évidemment faire des accès à la mémoire, c'est juste qu'ils ne sont pas recommendés. Il existe donc des instructions permettant de transférer des données des registres vers la mémoire et vis-versa. Il est même possible de charger une constante (valeurs immédiates) dans un registre sans la charger de la mémoire ; mais comment me direz-vous ? En codant cette constante directement dans l'intruction en langage machine (à côté de l'OpCode et des opérandes). Notez néanmoins que contrairement à ce que je laissais présentir, les processeurs tels que le 68010 (qui sont CISC) sont très souvent capables d'utiliser des opérandes mémoires à la place des registres dans beaucoup d'autres instructions que les deux instructions de transfert citées ci-dessus. Néanmoins, certains processeurs n'offrent pas cette possibilité supplémentaire (il s'agit des processeurs de type RISC).
La suite portera donc sur les instructions de base existant effectivement sur le 68010 (vous trouverez la liste complète dans le poly #5 (2001-2002) ou poly #11 (2000-2001)), et sur la syntaxe des opérandes en particulier dans le cas des opérandes mémoires ; ceci après une brève présentation des registres du 68010.
N.B. :CISC signifie Complex Instruction Set Computer, alors que RISC signifie Reduced Instruction Set Computer. La différence est la suivante : Les processeurs CISC ont en général peu de registres, des instructions dont la taille varie afin de réduire la taille du code et des instructions qui savent réaliser des tâches un peu complexe car si on les réalisait avec plusieurs instructions, cela prendrait plus de temps, donc certains se sont dit que cela valait le coup de les microcoder directement. Ce type de philosophie correspond à une époque où la mémoire coutait chère et les processeurs n'étaient dans l'absolu pas assez rapide. Les processeurs RISC se fonde sur l'observation suivante : si l'on veut accélérer la cadence d'un processeur il faut réduire sa taille et par conséquent le simplifier, c'est-à-dire que l'on espère gagner plus de vitesse par l'architechture et la fréquence que par la complexité des instructions. Ils ont donc beaucoup de registres pour minimiser les accès mémoire, que des opérations simples qui n'utilisent que les registres (pas d'accès mémoire) sauf pour les deux opérations de transfert, et la taille du code est standardisée pour accélérer le chargement du code, puisqu'il est aligné, c'est-à-dire permettant le plus petit nombre d'accès mémoire pour charger l'instruction dans le CPU. En effet, la mémoire étant moins chère, la taille des instructions est devenue une préoccupation moindre.
Réferrez-vous au paragraphe 1.4. pour la définition des registres. Le 68010 possèdent 16 registres 32-bits à usage général (en anglais : general purpose registers) divisés en deux groupes de 8, outre quelques autres registres dont voici une liste (dont je n'assurerai pas l'exhaustivité) :
| Registres de données | Registres d'adresses |
|---|---|
| D0 | A0 |
| D1 | A1 |
| D2 | A2 |
| D3 | A3 |
| D4 | A4 |
| D5 | A5 |
| D6 | A6 |
| D7 | A7 ou (USP (A7)/SSP (A7')), le(s) pointeur(s) de pile d'exécution. |
Dorénavant, je noterai R (ou Rx) une opérande, si elle peut être de type registre, si nécessaire D (ou Dx) pour les registres de données et A (ou Ax) pour les registres d'adresses, et par ailleurs, je noterai I une opérande, si elle peut être une valeur immédiate (une constante).
Les registres de données et d'adresse peuvent être utilisés partiellement. On pourra utiliser les bits de poids faible suivant :
Il s'avèrera nécessaire dans certains cas d'ajouter à un registre un suffixe .W ou .L, si l'on voudra utiliser respectivement les 16-bits de poids faible ou l'ensemble des 32-bits du registre. Le cas échéant, ce sera dit explicitement.
N.B. :Le 68010 peut fonctionner dans deux modes opératoires, le mode superviseur ou le mode utilisateur. Dans le premiers, l'exécution de toutes les instructions est autorisée, alors que dans le second, cetaines sont interdites et provoquent des exceptions. Sans s'étendre, cela permet à un système qui tournerait en mode superviseur de faire tourner les applications utilisateurs en mode utilisateur de telle sorte qu'elles ne puissent pas faire ce qu'elles veulent. Le point que je tiens à expliquer ici est l'existence de deux pointeurs de pile d'exécution. En fait, il y en a un pour chaque mode : SSP pour le mode superviseur et USP pour le mode utilisateur. Notez seulement que dans l'utilisation courante du CPU, vous n'avez pas à vous soucier duquel vous vous servez car c'est invisible. Vous n'auriez à vous soucier de ceci que si vous voulez entrer dans des considérations de programmation système ce qui n'est a priori pas votre cas. Utilisez donc le registre A7 par le nom "A7" sans réfléchir.
Un source en langage d'assemblage est divisé en lignes, chacune d'elle peut soit être vide, soit avoir un commentaire qui commence par une astérisque '*' et se termine par le saut à la ligne, soit avoir une directive directement suivi d'un commentaire éventuel (l'astérisque n'est pas nécessaire ici), soit enfin avoir une vrai instruction éventuellement précédée d'un label et éventuellement suivie d'un commentaire. Il y a néanmoins quelques règles supplémentaires à respecter, surtout si l'on ne veut pas passer 3 heures à tabuler un source de 2000 lignes... Enfin, un source se termine par une directive end (il ne s'agit pas de la fin du programme, mais de l'endroit où l'assembleur arrêtera d'assembler, autrement dit, ce qui suit la directive end est du commentaire).
Je permets une petite parenthèse qui n'a pas avoir avec le langage d'assemblage lui-même, mais plutôt à l'utilisation de programmes avec les simulateur de microprocesseurs : pour terminer un programme, c'est-à-dire qu'il arrête de tourner, il suffit d'exécuter l'instruction TRAP #15.
Une instruction commence par son mnémonique (MOVE, ADD, AND, BRA...) qui est éventuellement concaténé avec un suffixe indiquant la taille des données manipulées, lorsque l'instruction en manipule (.L 32-bits (long ou double word), .W 16-bits (word), .B 8-bits (byte)). Ensuite, vient la liste d'opérandes, dont les opérandes sont séparées par des virgules (souvenez-vous, il ne faut pas insérer d'espaces ou de tabulations dans la liste d'opérandes). Une opérande se classe parmi 3 catégories : les opérandes Registres (quand on cherche à manipuler le contenu d'un registre), les opérandes Mémoires (quand on cherche à manipuler le contenu de la mémoire à une adresse donnée, adresse qui peut alors éventuellement être calculée avec le contenu de certains registres qu'il faudra choisir), et les opérandes Immédiates, autrement dit les constantes (qui sont directement encodées à l'intérieur de l'instruction en langage machine). Dans le code, les valeurs immédiates sont précédées d'un dièse '#'.
Exemples :Les exemples de lignes de codes présentées ci-dessous sont des sources incomplets. En particulier, il manque systématiquement la directive end qui doit terminer tout source en langage d'assemblage du 68000. Je n'utilise pas non plus le "TRAP #15" qui termine un programme.
MOVE.L D0,D1 * Copie le contenu 32-bits du registre D0 dans le * registre D1, écrasant l'ancienne valeur contenue * dans D1. D0 reste inchangé. CCR est modifié en * conséquence (N, Z) - si le résultat de D1 est nul * par exemple... MOVE.L A0,D0 * Copie le contenu 32-bits du registre A0 dans le * registre D0, écrasant l'ancienne valeur contenue * dans D0. A0 reste inchangé. CCR est modifié en * conséquence (N, Z). MOVEA.L D0,A0 * Copie le contenu 32-bits du registre D0 dans le * registre A0, écrasant l'ancienne valeur contenue * dans A0. D0 ainsi que CCR restent inchangés. MOVEA.L A0,A1 * Copie le contenu 32-bits du registre A0 dans le * registre A1, écrasant l'ancienne valeur contenue * dans A1. A0 ainsi que CCR restent inchangés.
ADD.B #1,D0 * Ajoute la constante 1 au contenu 8-bits de D0 (il * s'agit des 8-bits de poids faible). Rappel : ce '#1' * est ce que l'on appelle une valeur immédiate. Par * exemple, si D0 valait 0x25, D0 vaudra 0x26. Le * registre CCR est modifié en conséquance (C, Z, N, V). * En pseudo-C, on aurait pu écrire (ceci pour mieux * comprendre la signification de l'action effectuée * par l'instruction ci-contre) : * "D0 += 1;" ou "D0 = D0 + 1;" * Comprenez qu'il ne s'agit pas de C ici, mais * simplement d'un moyen pour moi d'expliquer ce * que fait une instruction avec des notions que * vous êtes supposé(e)s connaître. On ne peut pas * nommer les registres en C !! C'est le * compilateur C qui se charge de manière invisible de * les utiliser. Ne confondez pas tout s'il vous plaît.
EOR.W D1,D0 * Réalise un OU exclusif entre les contenus de D1 et de * D0. Le résultat est placé dans D0. L'opération se * fait sur 16-bits (les 16-bits de poids faible). D1 * reste inchangé. CCR est modifié...
l1 SUBQ.L #1,D0 * l1 est un label (c'est-à-dire un symbole dont la * valeur est l'adresse de la donnée ou de * l'instruction à côté de laquelle elle est placée), * on s'en sert pour pouvoir réaliser un branchement ou * saut vers cette adresse. Un label n'a pas à être * déclaré : n'hésitez pas à faire des références en * avant. Attention : si vous mettez un label sur une * ligne différente de celle de l'instruction, le label * risque d'être décalé dans certains cas d'un octet par * rapport à l'instruction, ce qui pourrait être * catastrophique. * SUBQ décrémente D0 de 1. Le Q, c'est pour quick ; en * effet, l'instruction en langage machine prend moins * de place que la version sans le Q, mais il y a un * inconveignant, elle ne peut qu'ajouter (ADDQ) ou * retrancher (SUBQ) une valeur immédiate dans une * fourchette de -8 à +8. CCR est modifié... BNE l1 * Saute en l1 si le bit Z de CCR est à 0. Ce bit aura * préalablement été modifié par SUBQ. Notez qu'il * existe une instruction (que l'on qualifira de CISC) * qui réalise à peu de choses près la même opération en * une instruction, c'est-à-dire la décrémentation et le * saut conditionnel : il s'agit de l'instruction DBRA.
cte equ $10 * Associe au symbole cte la valeur $10 (16). Il * s'agit ici d'une définition d'une constante. Elle * ne consomme pas d'espace mémoire dans le programme * compilé ou dans la mémoire. Par contre, elle * pourra servir à calculer des valeurs immédiates ou * des adresses, qui seront encodées dans les * instructions en langage machine ou bien à calculer * des expressions pour des données initialisées. org $80 * La suite du code et des données commencent à * l'adresse $80 (128). m1 dc.l $13 * On définit 32-bits contenant la valeur $13 (Big * Endian). Accessoirement, on associe l'adresse * courante de cette ligne à un label (symbole valant * l'adresse, c'est-à-dire $80 ici) MOVEA.L #cte,A0 * Initialise A0 avec la valeur $10. MOVEA.L #$10,A0 * Identique à la ligne ci-dessus. MOVEA.L #m1,A0 * Initialise A0 avec la valeur $80. MOVEA.L #$80,A0 * Identique à la ligne ci-dessus. MOVE.L #((m1+2)*4),D0 * Initialise D0 avec la valeur de l'expression : * (m1 + 2) * 4 = ($80 + 2) * 4 = $82 * 4 = $208 (520) MOVE.L A0,D0 * Copie le contenu du registre A0 dans le regsitre * D0, écrasant l'ancienne valeur de D0. A0 reste * inchangé. CCR est modifié...
if TEST D0 * Teste si D0 est nul. Seul CCR est modifié. BE then * Saute au label then si CCR.Z (le bit Z de CCR) * vaut 1. else MOVE.L #$3F,D0 * Initialise D0 avec la constante 63 (0x3F en * hexadécimal). BRA fi * Saute en fi then MOVE.L #$7F,D0 * Initialise D1 avec la constante 127 (0x7F en * hexadécimal). fi * Notez 2 choses : * - if, else, then, fi ne sont que des labels. * - Notez que l'exécution de la close commençant par * then a fait exécuter un branchement (pris ou * non - ici BE est pris) de moins que l'exécution de * la close commençant par else (BE non pris et * BRA forcément pris). * Par conséquent, si vous utilisez ce type de * structure, pensez à mettre le code le plus utilisé * dans la close qui utilise le moins de branchement * et donc qui perd le moins de temps : ici la close * then.
Vous aurez constaté que je n'ai donné aucun exemple jusqu'à maintenant avec des instructions utilisant des opérandes mémoires. Au pire, on a chargé des adresses, mais jamais des données sur lesquelles elles pointaient. Nous allons donc voir les opérandes mémoires maintenant à part, étant donné qu'il existe beaucoup de moyens d'adresser des données en mémoire.
Pour accéder à une donnée en mémoire, il faut que le processeur connaisse l'adresse de cette donnée. Il faut donc qu'elle soit soit directement encodée dans l'instruction en langage machine, soit qu'une manière de la calculer (avec une éventuelle constante et des choix de registres) y soit encodée. L'adresse encodée s'appelle l'adresse effective. Nous allons voir la syntaxe particulière pour l'ensemble des manières d'adresser une donnée en mémoire, c'est-à-dire les adressages.
c1 equ $108 org $0 * Ce qui suit commence à l'adresse $0. MOVE.L $104,D0 * La donnée 32-bits se situant à l'adresse $104 (260) * est copiée dans D0, écrasant l'ancienne valeur de * D0. La mémoire est inchangée. En l'occurence la * donnée copiée vaut $C0 (regardez la directive * org qui précède). CCR est modifié... * Rappel : Les registres sont dans le CPU, pas * en mémoire !! Quand je dis que "la mémoire est * inchangée", je parle bien-sûr de l'opérande source, * c'est-à-dire l'opérande mémoire, ou bien encore la * donnée 32-bits se situant à l'adresse $104. MOVE.L m1,D1 * La donnée 32-bits se situant à l'adresse de la * valeur du symbole m1, c'est-à-dire $100 * (regardez la directive org qui précède), * est copiée dans D1. En l'occurence la donnée copiée * vaut $40. CCR est modifié... MOVE.L c1,D2 * La donnée 32-bits se situant à l'adresse de la * valeur de la constante (donc du symbole - synonyme) * c1, c'est-à-dire $108 est copiée dans D2. En * l'occurence la donnée copiée vaut $60. CCR est * modifié... MOVE.L m1+2*2,D3 * La donnée 32-bits se situant à l'adresse de la * valeur de l'expression (qui sera calculée avant * d'être encodée en langage machine par l'assembleur) * m1+2*2, c'est-à-dire $104 est copiée vaut D3. En * l'occurence la donnée copiée vaut $C0. org $100 * Ce qui suit commence à l'adresse $100 m1 dc.l $40 * 32-bits à l'adresse $100 dc.l $C0 * 32-bits à l'adresse $104 (32-bits = 4 octets) dc.l $60 * 32-bits à l'adresse $108
Le mode d'adressage par déplacement (displacement) permet d'accéder directement à une donnée en mémoire, en utilisant tout simplement l'adresse où elle est stockée. Notez que le déplacement pourra être encodé dans l'instruction en langage machine soit avec 32-bits (même si seuls 24-bits servent), soit 16-bits signés (tels que l'adresse effective soit dans la fourchette 0xFF8000 - 0x007FFF en passant par 0).
Ne confondez pas ce mode d'adressage qui sert à accéder à une donnée en mémoire avec la syntaxe permettant d'écrire des valeurs immédiates. Les valeurs immédiates sont toujours précédées d'un dièse '#'.
N.B. :Il existe une instruction qui risque de vous déranger ; il s'agit de l'instruction LEA. Elle permet d'initialiser un registre d'adresse avec une adresse effective calculée selon l'un des modes d'adressage dont on vient d'en voir un. Par conséquent, elle n'accède pas à la mémoire, bien qu'il y ait une opérande mémoire en jeu. Notez-bien que c'est un cas particulier ; LEA sert à ça (Load Effective Address). Par conséquent, seulement parce que c'est le travail de cette instruction et si vous voulez initialiser un registre avec une adresse exprimée avec le mode d'adressage par déplacement que l'on vient de voir, il ne faudra donc pas mettre de dièse, puisqu'il s'agit d'une adresse et pas d'une valeur immédiate dans l'encodage de l'instruction en langage machine. C'est l'encodage en langage machine qui prime !
Exemples :LEA $100,A0 * Initialise A0 avec l'adresse effective de * l'opérande source, c'est-à-dire $100. A0 vaut donc * $100 après l'exécution de cette instruction. * L'instruction est toujours 32-bits. LEA #$100,A0 * ILLEGAL !!!!
org $0 MOVEA.L #$100,A0 * Initialise A0 avec la valeur immédiate * "#$100", dont la valeur est $100. A0 vaut donc * $100 après l'exécution de cette instruction. MOVEA.L $100,A0 * Initialise A0 avec la donnée se situant à * l'adresse effective, dont la valeur est * $100. A0 vaut donc $80 après l'exécution de * l'instruction. org $100 dc.l $80
Vous comprendrez plus l'interêt de l'instruction LEA, lorsque vous connaîtrez d'autres modes d'adressage (L'intérêt de pouvoir charger dans un registre une adresse effective dont le calcul est complexe en une seule instruction, pour pouvoir sauvegarder l'adresse d'une case d'un tableau par exemple...). Je vous dirai quelques mots sur LEA à chaque mode d'adressage.
Le mode d'adressage indirect par registre permet d'accéder à une donnée en mémoire en calculant l'adresse effective à partir d'un registre d'adresse Ax. La syntaxe consiste à écrire l'opérande avec le registre entouré par des parenthèses : "(Ax)". Il faut donc bien-sûr s'assurer que le registre d'adresse choisi contienne bien l'adresse souhaité avant de l'utiliser dans une instruction ayant une opérande mémoire utilisant le mode d'adressage indirect par registre avec ce registre, sinon vous risquez d'accéder à n'importe quoi... Segmentation fa... :)
Exemple :org $0 MOVEA.L #$100,A0 * Initialise A0 avec la valeur immédiate * "#$100". MOVE.L (A0),D0 * Initialise D0 avec la donnée dont l'adresse est * l'adresse effective de l'opérande source, donc la * valeur du registre A0. L'adresse effective "(A0)" * vaut donc $100, par conséquent D0 vaut $80 après * l'exécution de l'instruction. CCR est modifié... * En C, on aurait pu dire que A0 joue le rôle de * pointeur. org $100 dc.l $80
Vous vous rappelez ? Je vous avez dit que je vous dirai quelques mots sur LEA à chaque mode d'adressage, et bien nous y voilà ! Rappel : LEA est une instruction qui charge dans un registre d'adresse une adresse effective et qui n'accède absolument pas à la mémoire, comme le font les autres instructions utilisant des adresses effectives. Je vous rappelle que c'est un cas particulier : c'est le travail de LEA (Load Effective Address). Donc faites attention à ne pas vous tromper losrque vous utiliserez LEA.
Exemples :MOVEA.L #$100,A0 * Initialise A0 avec la valeur immédiate * "#$100". A0 vaut donc $100 après l'exécution de cette * instruction. LEA (A0),A1 * Initialise A1 avec l'adresse effective de * l'opérande source, c'est-à-dire la valeur de A0. * A1 vaut donc $100 après l'exécution de cette * instruction. L'instruction est toujours 32-bits. LEA A0,A1 * ILLEGAL !!!!
org $0 MOVEA.L #$100,A0 * Initialise A0 avec la valeur immédiate * "#$100". A0 vaut donc $100 après l'exécution de cette * instruction. MOVEA.L A0,A1 * Copie le contenu du registre A0 dans le registre A1. * A1 vaut $100 après l'exécution de l'instruction. MOVEA.L (A0),A1 * Initialise A1 avec la donnée dont l'adresse est * l'adresse effective de l'opérande source, donc la * valeur du registre A0. L'adresse effective "(A0)" * vaut donc $100, par conséquent A1 vaut $80 après * l'exécution de l'instruction. org $100 dc.l $80
Le mode d'adressage indirect par registre et déplacement permet d'accéder à une donnée en calculant son adresse effective à partir de la somme d'un registre d'adresse 32-bits (appelé base dans ce contexte) et d'un déplacement (constante). Le déplacement est un nombre 16-bits signés. La syntaxe est "disp(Ax)". ATTENTION : cela peut poser de réels problèmes que le déplacement soit 16-bits, en particulier si vous choisissez d'utiliser le déplacement pour adresser la base d'un tableau et le registre d'adresse (base) pour indexer ses éléments, car ce tableau devra avoir une base comprise dans la fourchette 0xFF8000 - 0x007FFF en passant par 0. Il faudra donc éventuellement chercher autre chose...
Exemple :org $0 LEA $0,A0 * Initialise A0 avec la valeur de l'adresse * effective de l'opérande source "$0", * c'est-à-dire $0. Attention, c'est LEA... * La première opérande est donc une adresse * effective, pas une valeur immédiate, donc * il ne faut pas mettre de dièse '#'. MOVE.L #($F * 4),D0 * Initialise D0 avec la valeur immédiate * "#($F * 4)", qui vaut $3C (60). l1 MOVE.L D0,tab(A0) * Copie D0 en mémoire à l'adresse représentée par * l'opérande destination, qui est une opérande * mémoire, dont la valeur de l'adresse effective * est la somme du contenu de A0 et du déplacement * qui est "tab". Or tab est un label (symbole), donc * sa valeur est l'adresse de l'endroit où il est * défini, c'est-à-dire $100, donc la valeur de * adresse effective "tab(A0)" est $100 + A0. * Au premier tour de la boucle, A0 vaut $3C donc * "tab(A0)" vaut $13C. SUBA.L #4,A0 * Retranche $4 au contenu de A0. Au premier tour de la * boucle, A0 vaut $3C, donc après la première * exécution de cette instruction, A0 vaudra $38. Et * aux tours suivants, ainsi de suite : $34, $30, * $2C... DBRA D0,l1 * en Pseudo-C : * --D0; * if (D0 != -1) goto l1; * A chaque tour de boucle, D0 est décrémenté et tant * que D0 est différent de -1, on refait un tour en * sautant à l'adresse correspondant au label l1. * ATTENTION : c'est D0.W qui est * utilisé, autrement dit les 16-bits de poids faible * uniquement. Par conséquent, -1 signifie ici $FFFF * (65535 ou -1 en 16-bits signés) et pas $FFFFFFFF * (4294967295 ou -1 en 32-bits signés). org $100 tab ds.l $10 * Déclaration d'une zone de $10 (16) double-mots * (32-bits). Sa valeur est inconnue avant * initialisation.
Voici donc comment réagit notre chère Léa, euh... LEA !
Exemples :MOVEA.L #$104,A0 * Initialise A0 avec la valeur immédiate * "#$104". A0 vaut donc $104 après l'exécution de cette * instruction. LEA -$4(A0),A1 * Initialise A1 avec l'adresse effective de * l'opérande source, c'est-à-dire la somme entre A0 et * le déplacement qui vaut -$4, c'est-à-dire $100. * A1 vaut donc $100 après l'exécution de cette * instruction. L'instruction est toujours 32-bits.
org $0 MOVEA.L #$104,A0 * Initialise A0 avec la valeur immédiate * "#$104". A0 vaut donc $104 après l'exécution de cette * instruction. MOVEA.L -$4(A0),A1 * Initialise A1 avec la donnée dont l'adresse est * l'adresse effective de l'opérande source, donc la * somme du registre A0 et du déplacement qui vaut -$4, * c'est-à-dire $100. Par conséquent A1 vaut $80 après * l'exécution de l'instruction. org $100 dc.l $80
Le mode d'adressage indirect par registres de base et index permet d'accéder à une donnée en calculant son adresse effective à partir de la somme entre le registre de base et le registre d'index. Le registre de base est un registre d'adresse 32-bits "Ax", alors que le registre d'index est un registre quelconque "Ri" (données "Di" ou adresse "Ai") 16-bits signés ou 32-bits. Il faudra donc utiliser les suffixes .L (par défaut) ou .W pour sélectionner le registre "Ri" 16-bits signés ("Ri.W") ou 32-bits ("Ri.L"). La syntaxe d'une telle opérande est la suivante : "(Ax,Ri.s)" avec 's' qui est soit ".L", soit ".W". Le registre de base et le registre d'index peuvent être les mêmes. Rappel : "Ri" est juste un racourci pour ne pas avoir à dire "Di ou Ai" constamment dans mes explications, alors bien évidemment n'écrivez jamais d'opérande avec un registre qui s'appelerait R0 par exemple...
Exemple :org $0 LEA tab,A0 * Initialise A0 avec la valeur du symbole "tab", qui * est un label. A0 vaut $100 après l'exécution de cette * instruction. Attention, c'est LEA... * La première opérande est donc une adresse * effective, pas une valeur immédiate, donc * il ne faut pas mettre de dièse '#'. MOVE.L #$8,D0 * Initialise D0 avec la valeur immédiate * "#$8". D0 vaut donc $8 après l'exécution de cette * instruction. MOVE.L (A0,D0.L),D1 * Initialise D1 avec la donnée dont l'adresse est * l'adresse effective de l'opérande mémoire, qui * est l'opérande source. Sa valeur est la somme des * deux registres A0 et D0.L, donc vaut * $100 + $8 = $108. Par conséquent, D1 vaut 7 après * l'exécution de cette instruction. org $100 tab dc.l 3 * L'adresse de cette élément est $100 ; dc.l 5 * $104 ; dc.l 7 * $108 ; dc.l 11 * $10C.
MOVEA.L #$104,A0 * Initialise A0 avec la valeur immédiate * "#$104". A0 vaut donc $104 après l'exécution de cette * instruction. MOVEA.L #-$4,A1 * Initialise A0 avec la valeur immédiate * "#-$4". A0 vaut donc -$4 ($FFFFFFFC) après * l'exécution de cette instruction. LEA (A0,A1.L),A1 * Initialise A1 avec l'adresse effective de * l'opérande source, c'est-à-dire la somme entre A0 et * A1, c'est-à-dire $100. * A1 vaut donc $100 après l'exécution de cette * instruction. L'instruction est toujours 32-bits. * Cela ne pose aucun problème d'utiliser un registre * particulier dans les deux opérandes.
org $0 MOVEA.L #$104,A0 * Initialise A0 avec la valeur immédiate * "#$104". A0 vaut donc $104 après l'exécution de cette * instruction. MOVEA.L #-$4,A1 * Initialise A0 avec la valeur immédiate * "#-$4". A0 vaut donc -$4 ($FFFFFFFC) après * l'exécution de cette instruction. MOVEA.L (A0,A1.L),A1 * Initialise A1 avec la donnée dont l'adresse est * l'adresse effective de l'opérande source, donc la * somme du registre A0 et du registre A1, c'est-à-dire * $100. Par conséquent A1 vaut $80 après l'exécution de * l'instruction. * Cela ne pose aucun problème d'utiliser un registre * particulier dans les deux opérandes. org $100 dc.l $80
Le mode d'adressage indirect par registres de base et d'index et par déplacement permet d'accéder à une donnée en calculant son adresse effective à partir de la somme du registre de base "Ax", du registre d'index "Ri" et du déplacement, qui est une valeur 16-bits signés. La syntaxe est "disp(Ax,Ri.s)" avec s dans { ".L"; ".W" }.
Exemple :Dans la suite je ne commenterai que les instructions nouvelles ou celles sur lesquelles je souhaite détailler un point.
org $0 LEA tab,A0 MOVE.L #$8,D1 MOVE.W $2(A0,D1.L),D0 * Copie la donnée dont l'adresse effective est la * valeur de "$2(A0,D1.L)", c'est-à-dire * $2 + $100 + $8 = $10A dans D0.W. D0.W vaut donc 17 * après l'exécution de cette instruction. org $100 tab dc.w 3 * [0] [0] $100 (Element 0 champ 0 adresse $100) dc.w 5 * [0] [1] $102 (Element 0 champ 1 adresse $102) dc.w 7 * [1] [0] $104 dc.w 11 * [1] [1] $106 dc.w 13 * [2] [0] $108 dc.w 17 * [2] [1] $10A
MOVEA.L #$104,A0 * Initialise A0 avec la valeur immédiate * "#$104". A0 vaut donc $104 après l'exécution de cette * instruction. MOVEA.L #-$4,A1 * Initialise A0 avec la valeur immédiate * "#-$4". A0 vaut donc -$4 ($FFFFFFFC) après * l'exécution de cette instruction. LEA (A0,A1.L),A1 * Initialise A1 avec l'adresse effective de * l'opérande source, c'est-à-dire la somme entre A0 et * A1, c'est-à-dire $100. * A1 vaut donc $100 après l'exécution de cette * instruction. L'instruction est toujours 32-bits. * Cela ne pose aucun problème d'utiliser un registre * particulier dans les deux opérandes.
org $0 MOVEA.L #$104,A0 * Initialise A0 avec la valeur immédiate * "#$104". A0 vaut donc $104 après l'exécution de cette * instruction. MOVEA.L #-$4,A1 * Initialise A0 avec la valeur immédiate * "#-$4". A0 vaut donc -$4 ($FFFFFFFC) après * l'exécution de cette instruction. MOVEA.L (A0,A1.L),A1 * Initialise A1 avec la donnée dont l'adresse est * l'adresse effective de l'opérande source, donc la * somme du registre A0 et du registre A1, c'est-à-dire * $100. Par conséquent A1 vaut $80 après l'exécution de * l'instruction. * Cela ne pose aucun problème d'utiliser un registre * particulier dans les deux opérandes. org $100 dc.l $80
Le mode d'adressage indirect par PC et par déplacement relatif permet d'accéder à une donnée en calculant son adresse effective en sommant la valeur de PC (le registre qui contient l'adresse de l'instruction en train d'être exécutée (et même l'adresse du morceaux en train d'être exécuté)) et le déplacement relatif (la différence entre l'adresse de la donnée à adresser et l'adresse de l'instruction courante (l'encodage du déplacement relatif en fait)). Fort heureusement, ce n'est pas à vous de vous amuser à calculer cette différence, l'assembleur le fera pour vous. Tout ce que vous avez à faire, c'est de donner l'adresse de la donnée à accéder comme si de rien n'était. La syntaxe est "disp(PC)" où "disp" est le déplacement absolu (comme pour les adressages vus précédemment). L'avantage de cet adressage est que le code de cette instruction ne dépend pas de l'adresse à laquelle il est implémenté tant que la donnée recherchée reste toujours à la même distance (relative) de l'instruction. C'est particulièrement intéressant pour les bibliothèques... Notez que c'est loin d'être la seule fonctionnalité du CPU à utiliser des déplacements relatifs, en particulier beaucoup de branchements sont encodés avec un déplacement relatif, alors que bien-sûr vous n'avez jamais que le déplacement absolu à écrire ; c'est l'assembleur qui fait la conversion.
org $0 MOVE.L $100(PC),D0 * Initialise D0 avec la donnée se situant à * l'adresse effective calculée à partir de * "$100(PC)", c'est à dire tout simplement * $100 !! En fait, ce n'est pas $100 qui est * encodé dans l'instruction en langage machine, mais * la différence entre $100 et l'adresse de l'encodage * de la distance relative (quelque chose comme $4 - * je ne suis pas très sûr), soit quelque chose comme * $FC (mais, je ne suis pas sûr). Ensuite, le CPU * réalise la somme entre la précédente différence, * qui est le fameux déplacement relatif, et * le registre PC, ce qui permet de retrouver $100. * D0 vaut donc $80 une fois l'instruction exécutée. org $100 dc.l $80
LEA $100(PC),A0 * Initialise A0 avec l'adresse effective de * l'opérande source, c'est-à-dire $100. * A0 vaut donc $100 après l'exécution de cette * instruction. L'instruction est toujours 32-bits.
org $0 MOVEA.L $100(PC),A0 * Initialise A0 avec la donnée dont l'adresse est * l'adresse effective de l'opérande source, donc la * $100. Par conséquent A0 vaut $80 après l'exécution de * l'instruction. org $100 dc.l $80
Le mode d'adressage indirect par PC, par index et par déplacement relatif permet d'accéder à une donnée en calculant son adresse effective à partir de la somme entre PC, le registre d'index "Ri.s" avec s dans { ".W"; ".L" } et le déplacement relatif (cf. Adressage indirect par PC et par déplacement relatif). La syntaxe est "disp(PC,Ri.s)" avec "disp" le déplacement absolu et pas relatif.
Exemple :org $0 LEA $FC,A0 MOVE.L $4(PC,A0.W),D0 * Initialise D0 avec la donnée se situant à * l'adresse effective calculée à partir de * "$4(PC,A0.W)", c'est à dire la somme entre $4 et * le contenu signé de A0.W, c'est-à-dire $FC, donc * $100 (Faites comme si PC n'existait pas). * D0 vaut donc $80 une fois l'instruction exécutée. org $100 dc.l $80
Hein !! Mais, qu'est-ce que ?!
- MAIS, QU'EST-CE QUI SE PASSE ICI ?! ET POURQUOI
MES REPLIQUES SONT ECRITES AVEC UNE COULEUR NULLE ?? |
Les modes d'adressage indirects par registre avec pré-décrémentation ou post-incrémentation permettent d'adresser une donnée comme avec l'adressage indirect avec registre (base). La différence réside dans le fait que le registre en question va être soit pré-décrémenté ou post-incrémenté, c'est-à-dire respectivement décrémenté avant ou incrémenté après son utilisation dans le calcul de l'adresse effective. Attention : la valeur de la décrémentation ou de l'incrémentation dépend de la taille des données manipulées : 1 si .B, 2 si .W ou 4 si .L. La syntaxe est "-(Ax)" pour la pré-décrémentation et "(Ax)+" pour la post-décrémentation. Notez que ce comportement et la syntaxe sont similaires à ceux de la décrémentation et de l'incrémentation en C : la valeur de la variable décrémentée ou incrémentée dans l'expression dans laquelle est écrite dépend la position du "--" ou du "++" par rapport à la variable (avant ou après). Ce mode d'adressage est roi dans l'art de la manipulation de chaînes (de caractères ou octets (byte/8-bits), mais aussi de mot (word/16-bits) ou même de double-mots ou mots longs (long/double word/32-bits)). Illustrons donc ceci avec une copie de chaîne "mirroir" !
Exemple :org $0 LEA s1,A0 LEA s2end,A1 MOVE.L #s1len,D0 * Combien y-a-t-il de caractères à copier ? l1 MOVE.L (A0)+,-(A1) DBRA D0,l1 TRAP #15 * Fin du programme avec le simulateur. org $100 s1 dc.b 'Hello World!' s1end s1len equ s1end - s1 even * Aligne l'adresse courante sur l'adresse * paire suivante. s2len ds.b s1len s2end end
| Déplacement | disp |
|---|---|
| Indirect par registre de base | (Ax) |
| Indirect par registre de base et déplacement | disp(Ax) |
| Indirect par registres de base et d'index | (Ax,Ri.s) |
| Indirect par registres de base et d'index, et déplacement | disp(Ax,Ri.s) |
| Indirect par PC et déplacement relatif | disp(PC) |
| Indirect par PC et registre d'index, et déplacement relatif | disp(PC,Ri.s) |
| Indirect par registre de base avec pré-décrémentation | -(Ax) |
| Indirect par registre de base avec post-incrémentation | (Ax)+ |
La pile d'exécution utilise le registre A7. Voici les instructions pour la gérer : ("s" peut valoir ".W" ou ".L", tandis que "S" peut valoir ".B", ".W" ou ".L")
MOVE.s Ri,-(A7) * Empile la valeur du registre Ri. Il est préférable * que A7 reste pair, sinon si A7 est impair et qu'on * empile quelque chose d'autre qu'un octet... * plantage. MOVE.s (A7)+,Ri * Dépile dans le registre Ri. Il est préférable que * A7 reste pair, sinon... re-risque-de-plantage... LINK Ax,I * Empile Ax, initialise Ax à la valeur actuelle de * A7, puis retranche I à A7. Cette instruction sert * à se créer un stack frame. On utilisera * traditionnellement cette instruction au début * d'une routine. UNLK Ax * Initialise A7 à la valeur actuelle de Ax, puis * dépile dans Ax. Cette sert instruction sert à * annuler l'action de LINK. On utilisera * traditionnellement cette instruction en fin d'une * routine avant le retour à l'appelant.