ASSEMBLEUR

 

 
COURS D'ASM 68000
(par le Féroce Lapin)

retour au VOLUME 2

            
******************************************************************
*                                                                *
*             COURS D'ASSEMBLEUR 68000 SUR ATARI ST              *
*                                                                *
*                 par Le Féroce Lapin (from 44E)                 *
*                                                                *
*                         Seconde série                          *
*                                                                *
*                         Cours numéro 3                         *
*****************************************************************

Avant  de continuer à étudier ces cours,  je pense qu'il est grand
temps  pour vous d'étudier toutes les instructions du 68000.  Vous
avez déjà suffisamment de bases pour réaliser de petits programmes
et  surtout  vous  devez  vous  être  habitué  avec le système qui
consiste  à   taper  seulement  2 ou 3 lignes de programmes puis à
visualiser leur fonction avec MONST.

Prenez  donc  l'ouvrage sur le 68000 que vous avez normalement ac-
quis,  et faites le tour de toutes les instructions avec cette mé-
thode.  Observez  bien  les  résultats des opérations de décalages
(ASL,  ASR  etc..) Il ne s'agit pas ici de les connaître par coeur
mais  au  moins  de  savoir qu'elles existent pour pouvoir, le cas
échéant, les  retrouver  facilement,  et surtout d'être capable de
comprendre  ce qu'elles font.  Le nombre d'instructions est faible
mais  chacune  d'elle  réalise  une  opération  bien précise qu'il
convient  de connaître dans ses moindres détails. Il ne suffit pas
de  savoir  que  ROR réalise une rotation vers la droite,  il faut
également  savoir que le bit éjecté d'un coté est remis de l'autre
et, qu'en  plus, il  est recopié dans le bit C du SR.  Il faut re-
marquer  aussi  que ROR #4,D0 est accepté mais pas ROR #9,D0. Dans
ce  cas il faut faire 2 ROR ou MOVE.W #9,D1 puis ROR D1,D0.  C'est
ce  genre  de  petits  trucs qui vous bloqueront plus tard si vous
n'avez pas passé quelques heures à les décortiquer!

LES INCLUSIONS DE FICHIERS

L'un  des plus gros problème de l'ASSEMBLEUR se situe au niveau de
la  taille  des  listings.  Si  en BASIC une ligne suffit pour une
opération parfois très complexe, nous avons vu qu'en ASSEMBLEUR ce
n'était  pas  le  cas.  Par  contre  nous  avons  vu également que
l'écriture  en ASSEMBLEUR consistait à  taper le bon nombre d'ins-
truction  machine, alors  que  le compilateur du BASIC, du C ou du
PASCAL  se  débrouille  à faire une traduction 'qui marche' et qui
n'est donc pas forcément la plus économique au niveau taille et/ou
au niveau vitesse.

En contre partie,  nos sources ASSEMBLEUR font très rapidement des
pages  et des pages là, où un programmeur travaillant avec un lan-
gage  'évolué' n'aurait que quelques dizaines de lignes. Il existe
cependant  quelques  méthodes  permettant  non  pas  d'éviter  ces
multiples pages, mais de réduire sensiblement la taille du listing
sur lequel nous travaillons.

Deux  directives vont principalement nous servir. Attention, ce ne
sont  pas des instructions ASSEMBLEUR, mais des ordres interprétés
par  l'ASSEMBLEUR.  Ce sont donc, dans notre cas, des instructions
'Devpack' et non pas des instructions '68000'.

La  première, INCBIN, permet  d'incorporer  dans  le  programme un
fichier  binaire.  Un fichier binaire cela peut-être une image, de
la digit, des sprites etc... ou bien un morceau de programme qui a
été  assemblé sous forme de fichier binaire. Voici un exemple avec
une image. (listing 1 série 2)

Tout  d'abord  nous  transférons l'adresse de l'image en A6,  nous
sautons  l'en-tête  de  celle-ci pour pointer sur les couleurs qui
sont mises en place avec Xbios(6), nous cherchons ensuite l'adres-
se de l'écran avec la fonction Xbios(3) puis, après avoir sauté la
palette  de couleurs de notre image,  nous transférons cette image
sur  l'écran. Un  écran  fait  32000  octets.  Si nous transférons
long  mot  par  long  mot,  nous  ne  devons transférer que 32000 
divisé  par  4 c'est  à dire 8000 long mots.  Comme nous utilisons
une  boucle  DBF  qui compte jusqu'à 0 compris (la boucle s'arrête
quand  le  compteur  atteint  -1),  il  faut  donc  initialiser le
compteur  à  7999. Ensuite  attente  d'appui sur une touche et bye
bye.

Petit exercice éducatif. Une fois ce programme assemblé, suivez le
sous MONST. Lorsque Xbios(3) aura donné l'adresse de l'écran, pla-
cez  la fenêtre 3 sur cette adresse puis continuez à faire avancer
pas à pas le programme, pour voir la recopie se faire. De temps en
temps tapez sur la touche V afin de voir l'écran au lieu de MONST,
vous pourrez ainsi suivre l'affichage 'en direct'!!!

Autre  petit  exercice:  Le ST possède la particularité d'avoir en
quelque sorte 2 écrans: l'écran sur lequel on travaille (Xbios(2))
et  celui  que  l'on  voit (Xbios(3)).  Dans la plupart des cas il
s'agit  du  même  mais il est tout à fait possible de les placer à
des  endroits  différents  de  la  mémoire et ainsi de préparer un
affichage  dans  xbios2 tout  en  montrant  Xbios3.  Il sera ainsi
possible  d'afficher  rapidement  Xbios2 en  le  transférant  dans
Xbios3, et ainsi, de faire des animations rapides. Essayez donc de
changer  un  peu  le  listing  et de mettre MOVE.W  #2,-(SP)  à la
place  de 3 pour la recherche de l'écran. Débuggez le programme et
constatez!

Mais ceci nous éloigne un peu du sujet qui était l'inclusion. Pour
réaliser ceci nous avons juste fourni un label de repérage qui est
ici  IMAGE  puis l'instruction INCBIN suivi du chemin pour trouver
cette  image. Dans l'exemple c'est une image PI3 mais rien ne vous
empêche  de mettre une image PI2 ou PI1! Nous aurions très bien pu
mettre 2 images l'une à la suite de l'autre,  sans mettre de label
pour  repérer  la  seconde.  Pour  pointer dessus,  sachant qu'une
image DEGAS fait 32066 octets, nous aurions fait:

         LEA       IMAGE,A6
         ADDA.L    #32066,A6
Je rappelle que cela se lit: load effective address IMAGE dans A6 
add address long...

Petit  exercice: faire  un  programme  qui tourne en moyenne réso-
lution, passe en basse, affiche une image basse résolution, attend
un appui sur une touche puis repasse en moyenne.  Sachant utiliser
les  fonctions Xbios et les boucles,  vous devriez être capable de
faire  une  routine  de  sauvegarde  de la palette et une autre de
restitution de celle-ci.

Il  est  tout  à  fait  possible d'inclure ainsi des fichiers très
divers.  Il  existe pourtant plusieurs problèmes.  Tout d'abord la
taille  du programme résultant.  En effet,  notre image DEGAS,  de
part sa taille, a grossi notre programme de 32Ko!  Bien sûr il y a
maintenant  l'avantage  de  pouvoir  empêcher  les bidouilleurs de
venir  y   mettre  leur  pieds!  Encore  qu'une  image  DEGAS fait
toujours  la même taille, mais il est possible d'inclure une image
compactée   (en  mettant bien sûr une routine de décompactage dans
notre programme!).

Autre  problème,  le temps! En effet,  si l'affichage lui même est
plus  rapide  du fait qu'il n'y a pas à aller chercher l'image sur
la  disquette  puisque  cette  image  est  déjà en mémoire avec le
programme, c'est à l'assemblage que ça pédale!!! Il faut donc pas-
ser  par des mécanismes de travail bien ordonnés.  Mettre en place
un  disque virtuel (de préférence résistant au Reset) recopier les
images dedans et ensuite commencer à  travailler. Vous comprendrez
bien  vite pourquoi les possesseurs de  520 ont tout intérêt à les
faire gonfler à un méga minimum et pourquoi un lecteur externe ou 
mieux un disque dur devient vite indispensable!!!

Après  avoir vu le mécanisme d'inclusion de fichiers,  nous allons
nous  intéresser  maintenant aux inclusions de listings. Il est en
effet  possible  de  prendre un bout de listing,  de le sauver sur
disquette et de demander à l'assembleur de l'inclure lors de l'as-
semblage.  Là  aussi,  perte  de temps à l'assemblage mais gain de
temps  très  appréciable  lors  de  la  création de programme. Par
exemple votre routine de sauvegarde de palette:  faites avec soin,
 elle  marche  bien  et  en fait vous en avez besoin dans tous vos
programmes.  Il  faut  donc  à  chaque  fois la retaper et surtout
consommer  quelques  dizaines de lignes avec cette routine.  Perte
de  temps,  possibilité  d'erreur  de  frappe  et allongement bien
inutile  du listing.  Lorsque vous commencerez à circuler dans  10
ou   20  pages  de sources à la recherche d'une variable vous com-
mencerez  à comprendre de quoi je parle, en sachant en plus que 20
pages, c'est un tout petit source assembleur...

Voyons concrètement un exemple, avec la sauvegarde de palette.

SAUVE_PALETTE
         MOVEM.L   D0-D4/A0-A4,-(SP)
         LEA       ANC_PAL,A4            ptn sur le lieu de 
sauvegarde
         MOVE.W    #15,D4                16 couleurs

.ICI     MOVE.W    #-1,-(SP)             demande de couleurs
         MOVE.W    D4,-(SP)              numéro de la couleur
         MOVE.W    #7,-(SP)              Setcolor()
         TRAP      #14                   Xbios
         ADDQ.L    #6,SP
         MOVE.W    D0,(A4)+              sauvegarde de la couleur
         DBF       D4,.ICI               et on passe à la suivante
         MOVEM.L   (SP)+,D0-D4/A0-A4
         RTS
ANC_PAL  DC.L      0,0,0,0,0,0,0,0
REMET_PALETTE
         MOVEM.L   D0-D4/A0-A4,-(SP)
         LEA       ANC_PAL,A4            ptn sur le lieu de 
sauvegarde
         MOVE.W    #15,D4                16 couleurs

.ICI     MOVE.W    (A4)+,-(SP)           demande de couleurs
         MOVE.W    D4,-(SP)              numéro de la couleur
         MOVE.W    #7,-(SP)              Setcolor()
         TRAP      #14                   Xbios
         ADDQ.L    #6,SP
         DBF       D4,.ICI               et on passe à la suivante
         MOVEM.L   (SP)+,D0-D4/A0-A4
         RTS

Voici  donc  2 routines.  Tout d'abord sachant qu'un appel à Xbios
ne  sauve pas les registres D0-D3 et A0-A3 nous utilisons D4 et A4
pour  le  compteur  de couleur et l'adresse de sauvegarde,  ce qui
explique  aussi  la  sauvegarde  sur  la  pile  au  début des deux
routines. La  première sauvegarde la palette et la met à l'adresse
ANC_PAL.  Nous  constatons que cette adresse se trouve entre les 2
routines.  En effet plusieurs solutions s'offrent à nous.  D'abord
la  plus  économique  en  taille c'est de mettre cette réservation
pour  la palette dans la section BSS de notre programme en faisant
ANC_PAL  DS.W  16 Nous avons déjà vu que la section BSS n'occupait
pas  de  place sur la disquette.  Cependant nous avons réalisé ces
routines  avec  en  tête l'idée de les inclure,  afin de gagner en
facilité  de programmation. En plaçant cette réservation entre les
routines,  elle  fera  partie  intégrante  du  fichier.  Il  n'est
cependant  pas  possible  de  la  mettre  en  DS  nous  somme donc
contraint  de  la  mettre  en  DC.  Le  danger  serait  que  notre
programme  essaye  de lire cette partie.  Faire un BSR ANC_PAL par
exemple serait fatal mais nous sommes assez sérieux pour ne pas le
faire, donc pas de problème...

Une  fois tapé ce petit listing, sauvez le par exemple sous le nom
SAUV_PAL.S. Ensuite  modifiez le programme du listing 1 (celui que
nous  venons de voir avec l'image incluse).  Juste après la fin du
programme  par  MOVE.W   #0,-(SP),   TRAP   #1  (donc  juste avant
l'inclusion de l'image), mettez

         INCLUDE   "A:\SAUV_PAL.S"

Ne  mettez  pas d'étiquette sur le bord gauche puisque la première
étiquette  c'est  SAUVE_PALETTE et qu'elle est dans notre routine.
Ensuite  au tout début du programme, mettez BSR SAUVE_PALETTE et à
la fin juste avant de quitter, mettez BSR REMET_PALETTE. Au niveau
taille, votre listing est donc simplement augmenté de 3  lignes:

 (BSR SAUVE_PALETTE, BSR REMET_PALETTE et INCLUDE "A:\SAUV_PAL.S")

et pourtant ce sont 24 lignes qui sont ajoutées!!!

Nous  sommes  donc en train de nous créer une bibliothèque.  C'est
une très grande partie du travail du programmeur en assembleur car
de nombreuses choses reviennent souvent: initialisation par sauve-
garde de la palette, passage en basse résolution et passage en Su-
perviseur,  restitution  en  faisant  l'inverse,  décompactage des
images etc

De  même, si  vous êtes en train de réaliser un gros programme, en
cours de développement ce sont des pages entières qui peuvent être
incluses  diminuant  d'autant la taille du listing et y permettant
des déplacements nettement plus aisés.

Voyons  tout  de  suite  un  autre bloc qui devra maintenant faire
partie  de notre bibliothèque. Jusqu'à présent nous avons tapé pas
mal  de petits programmes,  sans nous soucier de la place mémoire.
Il  est temps d'y penser afin de commencer à prendre conscience du
fait  qu'il  faut  programmer  proprement.   Imaginons  que  notre
programme  soit en mémoire mais qu'en même temps il y ait d'autres
programmes  dans cette mémoire.  Il est bien évident que chacun ne
doit  s'approprier  que  la place mémoire dont il a  besoin,  afin
d'en  laisser le plus possible pour ces voisins. Pour cela il faut
savoir  que lors de l'assemblage sous forme de fichier exécutable,
il y  a génération d'une en-tête.  Grâce à  cette en-tête, au lan-
cement  de  notre programme il va y avoir création de ce qu'on ap-
pelle  la  page  de base.  En voici un descriptif. Si vous désirez
obtenir un maximum de renseignements sur les en-têtes de programme
ou  les pages de base,  reportez vous aux chapitres correspondants
dans  la Bible ou le Livre du Développeur.  Excellent chapitre sur
ce  sujet  dans la doc officielle ATARI (document sur la structure
des programmes).

* Page de base

Adresse         Description
$00      Adresse de début de cette page de base
$04      Adresse de fin de la mémoire libre
$08      Adresse du début de la section TEXTE
$0C      Taille de la section TEXTE
$10      Adresse de début de la section DATA
$14      Taille de la section DATA
$18      Adresse de début de la section BSS
$1C      Taille de la section BSS

Nous allons donc piocher ces informations, et en déduire la taille
qui    devrait   suffire   pour   notre   programme.   Connaissant
l'emplacement  de  la  zone  d'implantation  du  programme,   nous
utiliserons  la  fonction 74 du  GEMDOS  (fonction Mshrink)    qui
permet,  en  donnant  la  taille  désirée et l'adresse de fin,  de
rétrécir  une zone mémoire. En effet, au lancement notre programme
a  pris  toute  la place disponible, nous devons donc la rétrécir.
Notre  programme a également besoin d'une pile. Au lieu de prendre
celle  qui est déjà en place, nous allons lui substituer la notre,
dont nous pourrons régler la taille à loisir.

* ROUTINE DE DEMARRAGE DES PROGRAMMES

         MOVE.L    A7,A5                 prélève ptn de pile pour 
prendre
                                         * les paramètres
         LEA.L     PILE,A7               impose notre pile
         MOVE.L    4(A5),A5              adresse de la page de base 
de
                                         * l'ancienne pile
         MOVE.L    12(A5),D0             taille de la section texte
         ADD.L     20(A5),D0             + taille section data
         ADD.L     28(A5),D0             + taille section bss
         ADD.L     #$100,D0              + longueur de la page de 
base    
                                         *(256 bytes)

* Appel à la fonction MShrink() du GEMDOS (Memory Shrink)

         MOVE.L    D0,-(SP)
         MOVE.L    A5,-(SP)
         MOVE.W    #0,-(SP)
         MOVE.W    #74,-(SP)             M_shrink()
         TRAP      #1
         LEA       12(A7),A7

Voilà,  après  cette opération, notre programme n'utilise plus que
la  place  mémoire  dont  il  a besoin.  Il ne faut pas oublier de
définir la pile dans la section BSS par ceci:

         DS.L      256
PILE     DS.L      1

J'en  vois  qui  sont  surpris  par  cette réservation!!! Dans les
exemples fournis avec DEVPACK il est marqué  "stack go backwards",
que je traduis librement par "moi j'avance la pile recule, comment
veux tu ..." Un peu de sérieux. Nous avons vu que l'utilisation de
la pile se faisait en décrémentant celle ci:

(move.w        #12,-(sp) par exemple).

Il  faut  donc réserver de la place AVANT l'étiquette.  Pour cette
raison  nous  notons  le label et, au-dessus, la taille réellement
réservée pour la pile.

Quelle  taille  choisir?  Cela dépend de vous!  Si votre programme
est  plein  de subroutines s'appelant mutuellement et sauvant tous
les registres à chaque fois, il faut prévoir assez gros.

Tapez  le  programme suivant.  Il est évident que vous devez avoir
tapé au préalable la routine de démarrage de programmes qui est un
peu plus haut dans ce cours. Elle est ici incluse au début. Pas de
branchement  à  y  faire.  Si, une fois assemblé, vous débuggez ce
programme, vous  verrez au début la routine de start.  Dorénavant,
cette  routine  sera  toujours présente au début de nos programmes
mais  ne  sera jamais intégralement recopiée,  un INCLUDE est bien
plus  commode! Note:  Il semble que DEVAPCK se mélange parfois les
pinceaux   lorsque  les inclusions sont nombreuses et font appel à
des  fichiers  contenus  dans des dossiers.  De même il existe des
problèmes  avec  les  inclusions sur disque B lorsque celui-ci est
pris  comme lecteur A dans lequel on met le disque B. (je n'ai par
contre pas rencontré de problème avec mon lecteur externe). 

         INCLUDE   "A:\START.S"
         MOVE.W    #$AAAA,BIDULE
         BSR       TRUCMUCHE
         MOVE.W    BIDULE,D6
         MOVE.W    #0,-(SP)
         TRAP      #1
*-----------------------------------------------*
TRUCMUCHE MOVEM.L  D0-D7/A0-A6,-(SP)
         BSR       MACHIN
         MOVEM.L   (SP)+,D0-D7/A0-A6
         RTS

MACHIN   MOVEM.L   D0-D7/A0-A6,-(SP)
         MOVE.L    #$12345678,D0
         MOVEM.L   (SP)+,D0-D7/A0-A6
         RTS

         SECTION BSS
        
BIDULE   DS.W      1
         DS.B      124
PILE     DS.L      1
         END

Ce  programme  est  bien  sur  bidon.  J'espère cependant que vous
l'avez scrupuleusement tapé. Lancez le... paf  4 bombes!!!!!

Observons  le  à la loupe afin de comprendre pourquoi...  Le start
est  bien  mis  en place et lorsque nous débuggons il est effectué
sans  incident.  On  place  ensuite $AAAA dans la variable BIDULE.
Activons  la fenêtre 3 et pointons sur BIDULE (il suffit pour cela
de faire Alternate A et de taper le nom du label en majuscule donc
ici  BIDULE).  Avançons  pas  à  pas,   nous  voyons  bien  BIDULE
recevoir  AAAA.  Continuons à avancer: nous sautons dans TRUCMUCHE
avec  au passage sauvegarde dans la pile de l'adresse de retour (à
ce  propos  vous  pouvez  faire  pointer la fenêtre 3  sur PILE et
regarder  au  dessus  l'empilage des données). Ensuite nous allons
sauter  dans  MACHIN mais là, stop!!!!!!! Suivez attentivement les
explications. Juste  avant d'exécuter le BSR MACHIN, faites scrol-
ler  la  fenêtre 1 avec  la touche 'flèche vers le bas'.  En effet
d'après  la taille du listing et celle de la fenêtre vous ne devez
pas  voir  BIDULE.  Descendez donc de  7 lignes.   Normalement, la
première  ligne de la fenêtre doit maintenant être BSR MACHIN avec
la petite flèche en face indiquant que c'est cette instruction qui
va  être  exécutée.  En  bas  de la fenêtre vous devez voir BIDULE
avec en face DC.W  $AAAA puisque c'est ce que nous y avons déposé.
 En  dessous  des  ORI.B#0,D0 .  En  effet  nous  sommes  dans une
fenêtre  qui cherche à nous montrer le désassemblage de ce qu'il y
a  dans le tube.  Or à cette endroit il y a 0 dans le tube et cela
correspond  à ORI.B #0,d0;  ce qui explique ces instructions. Mais
ces  ORI.B correspondent à quoi ?  où sont-ils situés? eh bien, il
sont dans notre pile puisque celle-ci est constituée d'un bloc de 
124 octets  entre  BIDULE  et PILE.  Alors maintenant scrutez très
attentivement  BIDULE et avancez le programme d'un pas. Nous voici
maintenant sur la ligne MOVEM.L de la subroutine MACHIN. Exécutons
cette ligne...

Stupeur,  BIDULE  est  écrasé, de même que le RTS de la subroutine
MACHIN  qui  est  juste  au  dessus!!!  Et  maintenant,   si  nous
continuons,  après  le  second MOVEM le 68000 ne va pas tomber sur
le RTS puisque celui-ci vient d'être écrasé, et notre programme va
planter! Pourquoi ? eh bien, parce que nous avons essayé d'empiler
128 octets  (MOVEM de trucmuche=15 registres donc 15*4=60  octets,
idem pour le MOVEM de machin,  auquel il faut ajouter l'adresse de
retour pour trucmuche et celle de machin, total 128 octets!) alors
que nous n'avions prévu qu'une pile de 124 octets.

Pour que ce programme marche sans problème, il faut donc mettre au
minimum une pile de 128 octets. Faites très attention à ça, car si
nous  avions  mis  une  pile  de  124  octets,  le programme ne se
serait  pas planté car il n'y aurait pas eu écrasement du RTS mais
il  y  aurait  eu écrasement de BIDULE et je suis certain que vous
auriez  cherché bien longtemps en vous disant " mais qu'est ce qui
peut  bien écraser BIDULE  " surtout que cet écrasement survient à
un moment où, justement, on n'utilise pas BIDULE!!!

Gardez  donc  toujours  présent  à  l'esprit  le  principe du tube
mémoire,  sans oublier qu'il est plein d'instructions, de contenus
de variables et que rien n'empêche de les écraser!

Encore  une  petite  remarque  sur  le Start.   Il est tout à fait
possible  de  tester  D0 en  retour du Mshrink().  Si celui-ci est
négatif, c'est  qu'il y  a eu erreur. Si vous savez que systémati-
quement vous mettez le label BYE_BYE en face du GEMDOS(0) qui ter-
mine votre programme,  vous pouvez rajouter à la fin du start:

         TST.W     D0
         BMI       BYE_BYE

Une  dernière  précision  sur  les inclusions.  Il existe d'autres
moyens  de réaliser de telles choses,  par exemple d'assembler les
morceaux  puis  de  les  lier  avec un LINKER.  Cette solution est
intéressante  lorsque les programmes commencent à prendre des pro-
portions gigantesques.

Sinon, il  s'agit  plus d'embêtements qu'autre chose!!!!   Même si
certains puristes préfèrent linker:

j'édite, j'assemble, je  quitte, je linke, je lance, ça plante, je
débugge, j'édite etc...) 

je  préfère, quant  à moi, la méthode de l' "include". Elle permet
éventuellement  d'avoir  accès directement au source. Par exemple,
si  votre routine de sauvegarde palette plante,  placer le curseur
de  GENST  sur  la ligne INCLUDE  "A:\SAUV_PAL.S"  puis choisissez
l'option  Insert  File  dans  le  menu fichier.  Votre routine est
maintenant  sous  vos  yeux. Une  fois  que  ce bloc est au point,
délimitez le avec F1 et F2 puis sauver le avec F3, hop le tour est
joué!

Fin  du  cours  sur  les  inclusions!  Commencez à fabriquez votre
bibliothèque  et  n'hésitez pas à en faire des copies de sécurité,
et  méfiez  vous  des virus!  Sur cette disquette il y a  IMUN.PRG
Vous  le mettez en dossier Auto sur votre disquette de boot, et il
vérifié  toutes  les  disquettes  que  vous  introduisez  dans  le
lecteur!