Obligement - L'Amiga au maximum

Mardi 03 juin 2025 - 15:48  

Translate

En De Nl Nl
Es Pt It Nl


Rubriques

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

Articles in English


Réseaux sociaux

Suivez-nous sur X




Liste des jeux Amiga

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


Trucs et astuces

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


Glossaire

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


Galeries

Menu des galeries

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


Téléchargement

Documents
Jeux
Logiciels
Magazines
Divers


Liens

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


Partenaires

Annuaire Amiga

Amedia Computer

Relec


A Propos

A propos d'Obligement

A Propos


Contact

David Brunet

Courriel

 


Programmation : Initiation à l'assembleur 680x0 : l'environnement, la création d'exécutable, la syntaxe, les registres et la mémoire
(Article écrit par Victorien Ferry et extrait de GuruMed.net - juin 2003)


Ceci est un cours d'initiation à l'utilisation de l'assembleur 68020 sur Amiga écrit en 2003 : contrairement à des cours des années 1990, on utilisera uniquement AmigaOS, et on n'utilisera jamais directement d'adresses matérielles, ce qui promet a priori plus de compatibilité pour nos programmes quelle que soit la configuration Amiga.

L'assembleur est le langage le plus bas niveau avant le langage machine compris par le processeur. Il est en fait une image compréhensible de celui-ci. Un processeur n'exécute que du langage machine.

Tous les autres langages doivent d'une façon ou d'une autre traduire leurs algorithmes en langage machine pour s'exécuter. Il sera parfois fait allusion au langage C dans ce cours car les compilateurs C traduisent d'abord le programme C en assembleur avant d'en faire des exécutables (on peut demander à un compilateur C de faire apparaître la version assembleur d'un source .c), ce qui permet aussi de comprendre comment marche la création d'exécutable.

Cela dit, nous ferons des programmes 100% assembleur pour comprendre les subtilités de l'assembleur, ses avantages, ses pièges et comment bien travailler avec.

Quel environnement de développement et comment ça marche ?

L'assembleur se présente sous la forme de fichiers textes contenant le programme écrit en assembleur, nous verrons plus tard sa forme. Ces fichiers sont d'habitude terminés par ".asm" ou ".s", parfois ".i" pour les fichiers d'inclusion. Ils peuvent être "assemblés" (on compile un programme C, on assemble de l'assembleur).

Dans un monde "standard" dont fait partie le C, on n'assemble pas d'exécutable, on assemble un ou plusieurs ".o" (o pour objet) à partir des .asm, qu'on lie ensuite ensemble pour créer un exécutable. Par exemple, l'assembleur gratuit PhxAss peut créer directement des exécutables, mais aussi des .o à partir de .asm :

si on tape la commande DOS :

phxass monfichier.asm

...on vient d'assembler "monfichier.o" à partir de "monfichier.asm". Les .o ne sont pas des exécutables. Ils contiennent effectivement du code machine, mais il manque des relations de noms qui ne sont résolues qu'à l'édition de liens, par une autre commande DOS (comme parfois "ld" utilisé par le compilateur C GCC, ou "link"). Les commandes DOS d'éditions de liens d'exécutables prennent en paramètres une série de fichiers .o, par exemple : startup.o monfichier.o machin.o truc.o. L'éditeur de liens résout alors les liens de noms : les .o contiennent des noms de variables et de fonctions qui peuvent être demandé dans un autre .o. par exemple, un startup.o demande toujours une fonction "main" qui est forcément fourni par un des autres .o (voir commande assembleur XDEF et XREF plus tard). Si les résolutions de noms sont complétées, un exécutable est créée, et celui-ci ne contient plus que du code machine.

Mais tout ceci ne constitue que la partie "finale" de ce que réalise un compilateur C (PhxAss est d'ailleurs souvent utilisé par les compilateurs C 680x0 sur Amiga).

Cette méthode consistant à créer plusieurs .o (objet donc) pour ensuite créer son programme permet de bien s'organiser et séparer des parties de codes indépendantes, Mais il serait bien pratique de créer directement un exécutable à partir d'un fichier assembleur court : c'est tout à fait possible avec Devpac ou ASMOne, qui ont une option pour spécifier si on crée un .o, ou un exécutable à partir du source assembleur.

Où ça commence ? Où ça fini un programme assembleur ?

Peut-être savez-vous déjà ce qu'est une sous-routine, ou un appel de fonction en algorithmique : à un moment dans un programme, on "saute" vers une autre fonction, qui une fois terminée, reviendra d'où elle est partie (juste après le saut). Eh bien AmigaOS traite tout lancement d'exécutable (on parlera de tâche) exactement comme s'il s'agissait d'un saut en sous-routines dans une fonction : la tâche commence quand elle y rentre et termine sa vie quand elle en sort. L'analogie marche aussi pour les passages de paramètres, en entrée et en sortie : AmigaOS donne des paramètres en entrée au programme, et le programme donne un "code d'erreur" en sortie (voir précision ci-après).

Mais alors, où se situe cette fonction qui "fait" le programme ? Eh bien, c'est la première fonction au début de l'exécutable.

Dans le cas d'un programme relié, startup.o est toujours en premier dans la liste des .o : pas de mystère, c'est parce que cela doit être la première fonction lancée. Habituellement, un startup.o mâche le travail bas niveau d'entrée/sortie des programmes C, réalise quelques initialisations, puis lance la fonction "main" que les programmeurs de C connaissent bien et qui est le début du programme C (et qui, elle, peut se trouver plus loin dans l'exécutable).

En assembleur pur, il est tout à fait possible de se passer de startup.o : réalisons notre premier programme assembleur sous Devpac, qui permet de créer des exécutables directement (ne pas activer l'option "créer .o"). Notez qu'on peut aussi créer le programme juste en mémoire avec Devpac, sans créer l'image sur le disque, puis l'exécuter depuis la mémoire. Attention alors : si on assemble en mémoire et qu'on exécute plus d'une fois, la valeur des variables n'est pas remise à jour (ceci n'est vrai que pour Devpac).

Pour tous les exemples qui suivront, réglez votre compilateur sur "68020" ou plus, car l'assembleur 68000 comprend moins d'instructions. Tapez ces quatre lignes : (n'omettez pas les espaces quand il y en a, et n'en rajoutez pas quand il n'y en a pas en début de ligne !)

debut:     ; un label pour savoir ou on est. 
        move.l #844,d0 je met 844 en sortie 
; on retourne à la maison: 
        rts 

Il n'y a presque rien, pas d'édition de liens, pourtant Devpac compile un exécutable. Lancez-le depuis le Workbench : vous aurez un code de retour d'erreur 844. Ça y est, vous codez assembleur, ouaaaah que c'est beau. En même temps, difficile de faire plus court. Ces quatre lignes seront expliquées plus loin : la question pour l'instant est pourquoi ce code accompagné par presque aucune déclaration s'exécute-t-il ? Parce que, comme je l'ai dit, c'est la première fonction en entrée dans l'exécutable. Mais la réalité assemblée est un peu plus complexe : bienvenue dans le monde des "sections" qui composent un exécutable, et qui vont se poser dans la mémoire au chargement de celui-ci. Quand Devpac constate qu'aucune section n'est déclarée dans un source assembleur, il agit comme s'il s'agissait d'une section code par défaut. Pour être clair, il aurait fallu écrire par exemple :

        section  nomdesectioncode 
debut:        ; un label pour savoir ou on est. 
        move.l #844,d0 je mets 844 en sortie 

        ; on retourne à la maison: 
        rts 

Ici, on voit que le code se trouve dans une section nommée "nomdesectioncode" : c'est la première de l'exécutable, donc l'entrée du programme. on peut en définir plusieurs (c'est même conseillé) mais attention aux restrictions d'accès entre sections (voir différences entre instructions bra et jmp, et entre bsr et jsr). Sur Amiga, la commande "section" permet aussi de demander des préférences sur la façon dont la section va être chargée en mémoire au lancement et s'il s'agit de donnée ou de code (en assembleur, on assemble du code et des données) :

    section nomcode, code ; défini le début d'une section code chargée en mémoire Fast
                          ; ou en Chip s'il n'y a pas assez de Fast. 
    section nomdat, data ; défini le début d'une section data en mémoire Fast ou en Chip. 

    section nomcode, code_f ; défini le début d'une section code forcément en mémoire Fast 
    section nomdat, data_f ; défini le début d'une section data forcément en mémoire Fast 

    section nomcode, code_c ; défini le début d'une section code forcément en mémoire Chip 
    section nomdat, data_c ; défini le début d'une section data forcément en mémoire Chip 

Si vous connaissez l'Amiga Classic, vous savez qu'il existe deux types de mémoire accessible : la mémoire Fast, accessible uniquement depuis le 680x0 et la mémoire Chip, plus lente mais accessible à la fois par le 680x0 et par tous les jeux de puces graphiques. Si nous utilisions certaines bidouilles avec des listes Copper ou avec le Blitter, il faudrait forcer des données en mémoire Chip. Le code 68k peut s'exécuter en mémoire Chip comme en Fast, donc il n'y a aucune raison de le forcer en Fast, ce qui empêcherait le code de s'exécuter sur des Amiga non pourvus de cette mémoire.

Youpi ! J'apprends la syntaxe de l'assembleur

Expliquons le code minimal écrit plus haut, Cela va nous familiariser à la syntaxe magique de l'assembleur :

Dans un source assembleur, on a des instructions (move.l, add.l, etc.), des labels, qui sont des noms donnés arbitrairement à un endroit du code pour le repérer (comme "debut" dans l'exemple), et des commandes (comme "section" dans l'exemple), et des commentaires : on ne peut avoir qu'une instruction ou commande par ligne. On peut mettre des commentaires après un point virgule ; sur des lignes où il n'y a pas d'instruction, ou sur la même ligne qu'une instruction, après l'instruction (le point virgule est alors facultatif).

Toute instruction assembleur doit commencer après au minimum un espace ou une tabulation sur sa ligne. Tout label commence toujours collé au début de la ligne, et peut ou non être suivi du caractère deux points ":". On peut mettre une instruction après un label puis un espace, sur la même ligne.

Attention : un assembleur peut, selon les options, faire la différence ou pas entre majuscule et minuscule pour reconnaître les instructions et les références aux labels. Je vous conseille de toujours faire comme si la différence existait, ainsi, votre code passera partout. Notez que dans ce cas "Move" sera accepté comme "move". Pour la même raison, je conseille de ne pas mettre de ":" après les labels : certains assembleurs ne veulent pas de ":" sauf option, ce qui est gênant quand on inclut un fichier ".i" formaté différemment. Voici une version à peine plus riche de l'exemple précédent :

        section nomdesectioncode , code 

debut   ; un label pour savoir où le code est. 
        move.l  #844,d0 je mets 844 dans le registre d0 
        move.l  variable1,d1 
        add.l   d1,d1 ; 22*2=44 
        sub.l   d1,d0 ; 844 - 44 = 800 
; on retourne à la maison: le code d'erreur retourné est 800. 
fin 
        rts 
;--------------------- déclarons une variable 32 bits: 
        cnop 0,16 ; alignement de la mémoire sur 16 octets (facultatif) 
variable1 
        dc.l 22 ; 4 octets initialisés à 22 par défaut. 

Que se passe-t-il dans cet exemple ? La valeur "844" est placée dans les 32 bits du registre volatile d0 de manière immédiate, Puis, la valeur 22 est placée dans les 32 bits du registre d1 par lecture dans la mémoire. Une addition de d1 sur lui-même permet de passer sa valeur à 44, puis d0 est soustrait de 44, et la commande "rts" ferme le programme.

Tout cela n'est peut-être pas clair pour vous. Expliquons plus en détail ce qu'il se passe dans ces lignes :

L'instruction move

L'instruction "move" (déplacer en anglais) réalise de manière générale la copie d'un bloc de mémoire vers un autre (en BASIC ou en C, on pourrait la comparer à l'opération "=").

Ce bloc peut avoir trois "tailles" différentes. Move a toujours la forme :

crix move.? source, destination Ce qui copie les bits de la source dans la destination. Les bits ("bit" in english) sont la plus petite donnée possible en informatique : un bit peut être égal à 0 ou à 1 (deux valeurs possibles pour un bit). En en regroupant plusieurs, plus de valeurs sont possibles : avec deux bits, quatre valeurs sont possibles, avec huit bits, 256 valeurs différentes sont possibles selon que chacun soit égal à 0 ou 1.

En fait, "2 puissance le nombre de bits" sont possibles. La mémoire de l'ordinateur et les registres sont uniquement constitués de bits. Quand on copie une série de bits avec "move", on copie au minimum huit bits à la fois, ce qu'on appelle un octet ("byte" in english, don't confondre bite and byte). La mémoire pouvant être représentée par une suite d'octets linéaire, on utilise des "adresses" pour désigner tel emplacement d'un octet ou le début d'une suite d'octet dans la mémoire (les labels que nous avons vus représentent une adresse).

Avec "move", on peut aussi copier 16 bits (deux octets, ou "mot" au sens 68000, en anglais "word") ou enfin 32 bits (quatre octets, ou "long" au sens 68000, en anglais "long").

Dans la forme ci-dessus, ".?" peut être ".l" (4 octets, 32 bits), ".w" (2 octets, 16 bits) ou ".b" (1 octet, 8 bits). Exemple d'instructions possibles :
  • move.l d0,d1 : copie des 32 bits du registre d0 dans d1.
  • move.w d2,d3.
  • move.b d4,d5 : copie des 8 bits de poids faible de d4 à d5.
Sources et destinations peuvent prendre plusieurs formes.

Les registres internes du processeur. Qu'est-ce qu'un registre ?

Dans une instruction comme "move", on peut utiliser un registre (d0...d7,a0...a6,...). Ce sont des "emplacements" de 32 bits chacun, qui résident dans le processeur et pas dans la mémoire. Ils ne sont pas dans l'espace adressable et sont juste référencés par leurs noms. Ils réagissent comme des variables toujours accessibles et c'est avec eux que le processeur réalise ses opérations de calcul, ses comparaisons, etc.

Un calcul réalisé entre registres, sans accès à la RAM (mémoire externe au processeur), sera toujours beaucoup plus rapide qu'un calcul qui fait des écritures et lectures en mémoire (à cause des accès sur le bus entre processeur et mémoire).

Les voici :
  • Les registres de données : d0 d1 d2 d3 d4 d5 d6 d7.
  • Les registres d'adressages : a0 a1 a2 a3 a4 a5 a6 (pas touche à a7, aussi connu comme "sp", le pointeur de pile).
  • Il existe aussi des registres spéciaux (voir plus tard).
Les registres de données sont habituellement utilisés pour des calculs. La valeur contenue dans les registres d'adresses est d'habitude utilisée pour pointer (indiquer) un emplacement dans la mémoire.

Note : d'une manière générale, un source assembleur consiste presque toujours à donner des valeurs initiales à ces registres, puis à faire un calcul ou un algorithme avec, puis finir par recopier le résultat en mémoire.

L'adressage de valeur dans la mémoire. En lecture ou en écriture !

Autre forme possible pour une source ou une destination d'un "move" : un adressage en mémoire permet de lire ou d'écrire une valeur définie dans un emplacement de la mémoire adressable depuis un registre, ou même de réaliser une copie de mémoire à mémoire sans même passer par un registre.

Attention ici, beaucoup de syntaxes sont possibles pour réaliser la même opération :

    move.l   nomdelabeldesignantunedonne,d0   ; lecture de 32 bits de la mémoire
					    ; vers un registre
    move.l   d0,nomdelabeldesignantunedonne   ; écriture dans la mémoire

    move.l   label1,label2   ; écriture mémoire vers mémoire.
    move.l   (a0),d0	   ; a0 pointe une adresse mémoire. 
                           ; (a0) représente la valeur contenue à cette adresse.
			   ; ceci est donc une lecture de la mémoire dans un registre.

    move.l   d0,(a0)         ; écriture dans la mémoire "pointée" par a0.
    move.l   d0,a0	   ; ceci est très différent et est juste une copie registre à registre.

 	; en 68020 ceci est possible:

  move.l    64(a0),d1	  ; identique à "(a0),d1" mais la valeur est lue 64 octets plus loin dans la mémoire.
  move.l    -32768(a0),d1  ; valeur lue 32768 octets avant a0.
  move.l    (a0,d0.w),d1   ; valeur lue en a0 + la valeur des premiers 16 bits de d0.
  move.l    (a0,d0.w),d1   ; valeur lue en a0 + la valeur 32 bits de d0 (mais un peu plus lent à l'execution)

  move.l    (a0)+,d1       ; très utile:  équivalent à "(a0),d1" mais a0=a0+4 APRES la lecture.
  move.w    (a0)+,d1       ; très utile:  équivalent à "(a0),d1" mais a0=a0+2 APRES la lecture.
  move.b    (a0)+,d1       ; très utile:  équivalent à "(a0),d1" mais a0=a0+1 APRES la lecture.

  move.l    -(a0),d1       ; très utile:  équivalent à "(a0),d1" mais a0=a0-4 * AVANT * la lecture.
  move.w    -(a0),d1       ; très utile:  équivalent à "(a0),d1" mais a0=a0-2 * AVANT * la lecture.
  move.b    -(a0),d1       ; très utile:  équivalent à "(a0),d1" mais a0=a0-1 * AVANT * la lecture.
	; On est bien d'accord, tout le monde a noté la subtilité du * AVANT *: dans le cas -(a0)
	; le registre a0 est d'abord reculé, puis la donnée lue a cette adresse.
	; dans le cas (a0)+, la donnée est lue en a0, PUIS après a0 est déplacé en avant.
	; ça semble bizarre comme ça mais vous verrez l'utilité quand on parlera de pile.
	; notez aussi que dans ces cas, la calcul effectué sur a0 et représenté par - ou + est gratuit en temps machine.

        ; enfin le très puissant mais bizarre:

        move.l    64( a0 , d0.l*8 ),d1

"move.l 64( a0 , d0.l*8 ),d1" signifie que les 32 bits contenus en mémoire à l'adresse "a0 + 64 octets + (la valeur de d0 multipliée par 8)" sont copiés dans d1 (on peut spécifier *2,*4 ou *8 sur ce registre de données, cette multiplication est gratuite en temps machine).

Nous y reviendrons, mais sachez que parfois une forme d'adressage est possible ou pas, il est parfois nécessaire de se munir d'une bonne documentation 68000 ou 68020 où figurent des tableaux indiquant quel adressage est possible selon qu'on utilise un registre de donnée ou d'adressage en source ou destination, ou dans un "move" ou une autre instruction. Dans les faits, essayez toujours, votre assembleur vous dira si c'est possible ou non ! (attention, le 68000 plantera devant certains modes d'adressages disponibles seulement depuis le 68020. Réglez bien les options de vos assembleurs !).

L'assignation immédiate !

Dernière forme possible dans un "move", mais uniquement avant la virgule (là où on indique la source copiée) : l'assignation immédiate, ou directe, toujours représentée par un dièse "#" précédent une valeur écrite telle quelle. C'est une façon très simple de donner une valeur à un registre ou dans un emplacement mémoire :
  • move.l #666,d0 : met la valeur 666 dans les 32 bits du registre d0.
  • move.w #25,label1 : met la valeur 25 dans les deux octets à l'adresse label1.
  • move.w #32,(a0) : met la valeur 32 dans les 16 bits pointés par a0.
Quand on veut spécifier une valeur immédiate après "#", on peut le faire comme dans les exemples précédents, en décimal. Mais il est tout à fait possible de la spécifier en hexadécimal (voir chapitre 2) avec un "$" :
  • move.l #$00078af2,d0 : équivaut à move.l #494322,d0.
  • move.w #$7fff,d1 : équivaut à move.w #32767,d1.
  • move.b #$7f,d2 : équivaut à move.b #127,d2.
Ou même en binaire avec "%" :
  • move.b #%11001111,d0 : équivaut à move.b #207,d0.
Note : C'est une commodité d'écriture de pouvoir représenter un nombre dans une base ou une autre (binaire, hexadécimal, décimal...). Le code machine, lui, ne connaît que des bits donc les codes équivalent seront les mêmes une fois assemblés. De la même façon, il est possible d'écrire des équations là où l'assembleur demande un nombre constant :

move.w #(256+$100)/%0011,d0 ; 256+256/3= 170.666 

Cette écriture mettra "170" dans d0 (les registres représentent des valeurs entières).

Attention : il est fondamental de comprendre que :

move.l 340,d0 

...va aller lire la mémoire à l'adresse 340 (Dieu sait ce qu'il s'y trouve !), cet espace mémoire n'est pas à vous. Si nous avions un label à l'adresse 340, cette écriture serait équivalente à :

move.l label,d0 

Tout ceci est tout à fait différent de :

move.l #340,d0 

...qui va juste mettre la valeur 340 dans d0. Dans ce cas, le nombre "340" est contenu dans l'instruction même du "move" (la valeur se trouve donc dans le cache instruction et non donnée, pour ceux qui ont une idée de ce qu'est un cache).

Qu'est-ce qu'une variable globale ?

Vous devez maintenant avoir une idée claire et nette de ce qu'est un registre et ce qu'est la mémoire, et un adressage...

Dans un exemple plus haut, nous avons créé une variable globale nommée "variable1". Une variable globale est un emplacement dans la mémoire, qui prend un nombre d'octets donné dans celle-ci et qui est accessible depuis n'importe quel endroit du code, par son label qui indique sa position en mémoire. En assembleur 68000, on peut en déclarer n'importe où dans son code, pourvu qu'on soit sûr qu'il ne figure pas au beau milieu d'une fonction qui sera exécutée (le code assembleur contient du code et des données).

L'instruction :

dc.l valeurinitiale 

...va réserver quatre octets (32 bits, .l) à l'endroit où on la déclare et l'initialiser d'après la valeur ("dc" signifie "declare constant"). On pourra ensuite lire ou écrire des valeurs à cet endroit avec un "move.l" par exemple.
  • dc.w valeurinitiale : fera pareil avec deux octets (16 bits).
  • dc.b valeurinitiale : fera pareil avec un octet (8 bits).
On peut aussi déclarer une suite de valeurs initialisées en les séparant avec des virgules :

dc.l 0,256,4,9 

Une des grandes différences entre l'assembleur et des langages comme le C est qu'en assembleur, les données ne sont déclarées que par la taille qu'elles prennent en octet et leur adresse, alors que dans les autres langages, on définit une donnée par son "type" (entier, flottant, caractères, pointeurs...) dont la taille en octet est implicite. Ici, en assembleur, si nous avons besoin d'un entier 32 bits, on déclarera un espace de quatre octets. Si on a besoin d'une donnée d'un autre type qui nécessite quatre octets aussi, on déclarera quatre octets de la même façon.

Pour déclarer des données dans un source, on peut aussi utiliser l'instruction "ds" (declare storage) :
  • ds.l 30 : assemble 120 octets non-initialisés ici.
  • ds.w 60 : assemble 120 octets non-initialisés ici.
  • ds.b 120 : assemble 120 octets non-initialisés ici (aussi).
La valeur spécifiée est alors le nombre de fois que la taille de la mémoire est répétée (ici 4*30=2*60=120), mais contrairement à "dc", on ne sait pas quelles vont être les valeurs initiales écrites dans cet espace (il faudra l'initialiser nous-même).

Dernière variante : "dcb" (declare constant bloc) va déclarer une table de "n" éléments, comme "ds", mais va initialiser chacun de ces éléments avec "valeur" :
  • dcb.l n, valeur : forme.
  • dcb.b 64,0 : 64 octets initialisés à 0.
  • dcb.l 64,1 : puis 64*4=256 octets représentant 64 long à 1.
Voyons ce que pourrait donner une déclaration d'un tas de variables et de tableaux globaux déclarés dans un source assembleur (dans sa propre section, tiens) :

;--------- ma section de variables ----------
    section    datasection,data

    cnop 0,16

tableau16entier
    dcb.l    16,0

valeurx
    ds.l     1

valeury
    ds.l     1

booleen1
    dc.b     0
    
    even
entier2   dc.l    666
phrase    dc.b   'ben mon cochon',0

   cnop 0,16
cachelinedata   
          dcb.b 16,0

Alignement de la mémoire

Nous voyons dans l'exemple ci-dessus que des variables ont été déclarées en réservant de la place dans le source, et que des labels désignent ces emplacements pour y faire référence. Mais des éléments nouveaux apparaissent : d'abord les commandes "even" (signifie "pair") et "cnop". Ce sont des commandes qui forcent un alignement de mémoire. Nous avons vu qu'un emplacement dans la mémoire s'adresse (se désigne) par une valeur entière désignant un emplacement mémoire, et que l'unité utilisée est l'octet. Ceci implique que :
  • label1 dc.l 0
  • label2:
  • label3:
Dans cet exemple, label2 = label1 + 4, puisque "dc.l" les espace de quatre octets, alors que label2 est identique à label3. Notez qu'il est possible et parfois intéressant de mettre plusieurs labels au même endroit : un label à un nom qui lui donne un sens, rien n'empêche que le même emplacement ait plusieurs sens (comme fin de la boucle d'avant, début de la boucle d'après).

Sachant ceci, voici une mauvaise nouvelle. Retenez bien ça : en 680x0, réaliser une lecture ou une écriture en temps que ".l" ou ".w" sur une adresse mémoire impaire est interdit. Au mieux, ça fait tout planter la machine dans 100% des cas (c'est une "exception" au sens processeur, comme les divisions par zéro). Avec des instructions ".b", la mémoire peut être lue sur des adresses paires ou impaires. Voilà, j'espère que c'est bien retenu !

Donc si vous avez compris, si on a par exemple :
  • var1 dc.l 0 : aligné OK (adresse paire).
  • var2 dc.b 0 : aligné OK (adresse paire).
  • var3 dc.l 0 : forcément pas aligné sur deux octets (adresse impaire).
Un "move.l var3,d0" ferait planter. La commande "even" réalise, si besoin est, l'addition d'octets inutiles pour "caler" var3 :
  • var1 dc.l 0 : aligné OK (adresse paire).
  • var2 dc.b 0 : aligné OK (adresse paire).
  • (espace) even
  • var3 dc.l 0 : aligné OK (adresse paire).
La commande "cnop" permet d'aligner de la même façon l'instruction qui suit, mais sur des adresses "multiple de n". les octets inutiles sont initialisés par la valeur donnée en premier.
  • cnop 0,2 : équivaut à "even" (force adresse paire) !
  • cnop 0,16 : aligne l'adresse sur un multiple de 16.
A quoi peut bien servir un alignement de mémoire supérieur à 2 puisque les "move.l" et ".w" y ont accès ? Pour des raisons d'optimisations par exemple : à partir du 68030 jusqu'au 68060, le processeur lit la mémoire 16 octets par 16 octets (c'est la taille du "cacheline" qui rempli le cache de données). En clair, si 16 octets sont à la suite alignés à partir d'une adresse multiple de 16, si vous avez lu un premier octet, les 15 autres sont lus en même temps (et donc disponibles très vite par la suite).

L'autre nouveauté est la chaîne de caractères :

phrase: dc.b 'une phrase ASCII est écrite',0 
even

Un caractère ASCII étant un octet, on peut écrire des suites des phrases en tant que tableau d'octets. Une valeur 0 indique la fin de la chaîne. Comme la taille de cette chaîne peut varier, une commande "even" viendra assurer un recalage par la suite.

Allez, pour votre culture, cette dernière instruction très utile pour construire ses données globales :

labeltableau: incbin 'monfichierbinaire.bin'
(espace) even

"incbin" permet en effet d'inclure tel quel un fichier binaire du disque dans le source (très utile pour des tables mathématiques par exemple, ou des images).


[Retour en haut] / [Retour aux articles]