Quelques renseignements sur les Boot Blocs (ou BB) par Christophe LARATTE Cette petite documentation a pour but d'éclairer la lanterne des novices en matière de BB et celle des programmeurs qui désirent mettre leur empreinte sur les BBs. Elle peut être librement copiée tant quelle reste inchangée. Si vous avez des choses à ajouter, ou si vous désirez plus d'infos, écrivez-moi à : Christophe LARATTE 6 allée Marcel Pagnol 87410 Le Palais sur Vienne FRANCE ________________________ Un BB c'est quoi ? ~~~~~~~~~~~~~~~~~~ Comme son nom l'indique, un BB est un bloc, ou plutôt deux blocs : les deux premiers blocs (ou secteurs) d'une disquette. Il faut cependant savoir qu'une disquette normalement formattée est constituée de : en anglais - 2 faces 2 sides - 80 pistes par face = 160 pistes 80 tracks - 11 secteurs par piste = 1760 secteurs 11 sectors (blocks) et chaque secteur contient 512 octets 512 bytes soit un total de 901120 octets ou 880 Ko. Comme son nom l'indique aussi, un BB sert pour le boot. A chaque reset (ce qui correspond au boot), l'Amiga attend une disquette. Dès que vous en insérez une, il va lire le BB. Ainsi il vérifie que la disquette est lisible (c'est à dire quelle a été formattée, ou qu'il ne s'agit pas d'une disquette PC ou autre). Une fois le BB lu, il vérifie que ce dernier est intact en vérifiant la checksum (somme de contrôle, voir plus loin). Si c'est le cas, il exécute le programme qui se trouve sur le BB et qui dépend de chaque BB. Voici le format d'un BB (donné en hexadécimal (le $ signifie écriture hexadécimale), en mots longs (1 mot long = 4 octets)) : $444F5300 qui signifie 'DOS' accompagné d'un 0. $xxxxxxxx où xxxxxxxx peut prendre n'importe quelle valeur. C'est là que se situe la checksum. Pour que l'on puisse booter sur une disquette, il faut que la valeur de la checksum soit correcte : elle doit annuler la somme de tous les autres mots longs. Il y une méthode pour la calculer à la fin de la doc. $00000370 cette valeur doit normalement correspondre à la position du bloc racine (root block) sur la disquette, c'est à dire le 880ème bloc. Ce bloc est utilisé pour stocker le nom de la disquette et d'autres informations sur la disquette. C'est aussi à partir de lui que commence le catalogue de la disquette (d'où son nom : racine). En réalité (et pour le file system standard), cette valeur ne sert à rien. $xxxxxxxx ici prend place un programme de 1012 octets max. Il . sert normalement à amorcer l'initialisation de tout . le système. Mais dans 1012 octets, on peut ajouter . à l'initialisation standard, un virus, un antivirus $xxxxxxxx ou un petit programme permettant de régler quelques paramètres de l'Amiga (comme le filtre audio). Il doit être en position independent car il est chargé n'importe où (voyez le manuel de votre assembleur). Mais attention, les BBs sont aussi les rampes de lancement de la plupart des jeux. Un programme spécial, logé dans les 1012 octets, prend le contrôle de l'Amiga, au lieu de faire l'initialisation normale. Il permet ainsi de charger un jeu sans passer par l'AmigaDOS, en dirigeant directement le hardware (le matériel, c'est à dire les processeurs de l'Amiga). C'est très pratique, car on peut ainsi charger une disquette qui ne corresponde pas au formattage standard... Conclusion : si un jeu est auto-boot (il se charge sans passer par le système : pas de Workbench, pas de fenêtre CLI, affichage du répertoire de la disquette impossible (read/write error)), il ne faut pas écraser son BB. Ainsi si vous voulez vérifier ce jeu avec un antivirus, ne répondez pas oui aux questions du genre 'BB inconnu, voulez-vous le remplacer par un BB standard', sans avoir au préalable sauvegardé votre BB grâce à un programme comme, heu, je sais pas moi, par exemple... BootBlock Manager... pour n'en citer qu'un :-) Du concret, du concret... ~~~~~~~~~~~~~~~~~~~~~~~~~ Voici la description du BB standard (version 1.3) dont je vous parlais plus haut : * Source du BB standard (version 1.3) * Par Commodore _LVOFindResident = -$60 RT_INIT = $16 section BB,CODE dc.l $444F5300 ; ou dc.b 'DOS',0 dc.l $C0200F19 ; cette valeur de checksum n'est ; valable que si tous les autres ; octets après le programme sont ; à zéro. dc.l $00000370 ; R.A.S. lea DosName(pc),a1 ; l'adresse du nom en A1 jsr _LVOFindResident(a6) ; on appelle une fonction d'exec ; qui recherche la chaine (dont ; l'adresse est en A1) parmi les ; noms des modules résidents. ; Si la fonction trouve un ; module avec le même nom, elle ; renvoie un pointeur (=adresse) ; en d0 sur la structure du ; module résident. Sinon d0=0. tst.l d0 ; Teste le contenu de d0. beq.s NoDos ; si d0=0 alors il n'y a pas le ; module demandé. On va en NoDos ; si d0 ne contient pas 0, on ; continue normalement. move.l d0,a0 ; on met le pointeur en a0. move.l RT_INIT(a0),a0 ; dans une structure Resident ; Tag (RT) et à l'offset de ; valeur RT_INIT, correspond un ; pointeur sur un programme d' ; initialisation du module. Ici ; il s'agit du pointeur sur le ; programme d'initialisation de ; la dos.library. moveq #0,d0 ; met 0 en d0. End rts ; on quitte. NoDos moveq #-1,d0 ; met -1 en d0 bra.s End ; on saute à la fin du programme DosName dc.b "dos.library",0 ; nom à chercher. END Remarque : si vous voulez assembler ce programme et l'installer avec BootBlock Manager sur un BB, il faut enlever les trois mots longs du début. BBM se chargera de les remettre grâce à l'option Calculate Checksum. Si vous tenez vraiment à laisser les trois mots longs, il faut mettre la préférence "Offset in executable". Comme vous avez pu le constater, il peut se produire deux cas : - FindResident ne trouve pas de module dont le nom est "dos.library". Le programme renvoie -1 (dans d0) à l'Amiga. Celui-ci affichera le guru #30000001.xxxxxxxx qui signifie : erreur en provenance de Strap (le programme qui s'occupe du boot), le programme du boot bloc a renvoyé le code d'erreur xxxxxxxx. Puis, l'Amiga recommencera l'initialisation. - FindResident trouve le module. Le programme renvoie 0 (dans d0) et le pointeur sur le programme d'initialisation de la dos.library (en a0) à l'Amiga. Ce dernier peut poursuivre l'initialisation complète du système. Pour les plus curieux, voici comment Commodore parle de d0 et a0 : (extrait du Rom Kernel Reference Manuel : libraries and devices) "Le programme du boot doit renvoyer des valeurs dans deux registres: D0 et A0. D0 est un code d'erreur -- s'il n'est pas à zéro alors une alerte système sera générée, et le système rebootera." "Si D0 est à zéro alors A0 doit contenir l'adresse de départ où l'on doit sauter." Revenons à notre programme : il n'est pas sensé marcher tel quel. Car il fait appel à une fonction de l'exec.library sans avoir mis le pointeur d'exec en A6 (grâce à : move.l 4,a6). Alors, pourquoi ça marche ? Tout simplement, parce que Strap (le programme qui lance le programme du boot) met toujours le pointeur d'exec en A6. Il fait même plus, il met le pointeur d'une structure IO request du trackdisk.device, ouverte, en A1. Commodore ajoute : "Le programme du boot peut utiliser librement le IO request autant qu'il veut (le programme peut effacer le contenu de A1, mais pas celui du IO request)." Ca sert à quoi une structure IO request ? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Une structure IO request est une structure (si si!) qui permet de dialoguer (IO = Input/Output = Entrée/Sortie) avec un device, ici le trackdisk.device. Une structure est constituée par un ensemble de données qui permettent de diriger le device. Pour le trackdisk, il faut une structure du type IOExtTD ou IOSTD (celle du boot est IOSTD). Ce qui donne en assembleur : offsets 00 dc.l 0 ; *ln_Succ \ \ 04 dc.l 0 ; *ln_Pred \ \ 08 dc.b 0 ; ln_Type structure Node \ 09 dc.b 0 ; ln_Pri / structure Message 10 dc.l 0 ; *ln_Name / / 14 dc.l 0 ; *mn_ReplyPort / 18 dc.w 0 ; mn_Length / 20 dc.l 0 ; *io_device privé 24 dc.l 0 ; *io_Unit privé 28 dc.w 0 ; io_Command 30 dc.b 0 ; io_Flags 31 dc.b 0 ; io_Error 32 dc.l 0 ; io_Actual 36 dc.l 0 ; io_Length 40 dc.l 0 ; io_Data 44 dc.l 0 ; io_Offset 48 dc.l 0 ; iotd_Count \ 52 dc.l 0 ; iotd_SecLabel / spécifique au IOExtTD Remarques : le * signifie pointeur ou adresse. la structure Message et la structure Node sont des sous-structures. Parmi toutes ces données, voici celles qui nous intéressent pour le moment (pour les autres, référez-vous aux RKMs) : offsets 28 dc.w 0 ; io_Command 36 dc.l 0 ; io_Length 40 dc.l 0 ; io_Data 44 dc.l 0 ; io_Offset io_Command permet de déterminer la commande que doit exécuter le trackdisk. Par exemple, 2 correspond à la commande CMD_READ, c'est à dire la lecture, 9 correspond à TD_MOTOR, elle permet de controler le moteur du lecteur de disquette. Les trois autres données ne sont pas toujours prises en compte. Elles dépendent de la commande. Par exemple, avec CMD_READ : io_Length permet de préciser le nombre de blocs à lire. La valeur doit être donnée en octets et doit être un multiple de 512 (car il y a 512 octets par bloc). io_Data est un pointeur sur l'endroit dans la mémoire où l'on doit mettre les blocs lus. io_Offset permet de déterminer à partir de quel bloc on doit lire. Là aussi, la valeur doit être donnée en octets et doit être un multiple de 512. Exemple en assembleur : * Lecture et exécution d'un programme CMD_READ = 2 io_Command = 28 io_Data = 36 io_Length = 40 io_Offset = 44 _LVOAllocMem = -198 _LVODoIO = -456 _LVOFreeMem = -210 ; a1 contient l'adresse du IO request lors du boot move.l a1,a5 ; on sauve a1 en a5 move.l #10*512,d0 ; on réserve de la move.l #$10002,d1 ; mémoire en CHIP jsr _LVOAllocMem(a6) move.l d0,a3 ; a3 = *mémoire allouée move.w #CMD_READ,io_Command(a5) ; on veut lire move.l #10*512,io_Length(a5) ; 10 secteurs move.l #20*512,io_Offset(a5) ; à partir du 20ème move.l a3,io_Data(a5) ; dans la mémoire allouée move.l a5,a1 jsr _LVODoIO(a6) ; on envoie la demande movem.l d0-a6,-(sp) ; sauvegarde des registres jsr (a3) ; on exécute le programme ; chargé dans la mémoire ; allouée movem.l (sp)+,d0-a6 ; restitution des registres move.l a3,a1 ; on libère la mémoire move.l #10*512,d0 ; allouée jsr _LVOFreeMem(a6) ; ... ici prend place la routine d'initialisation normale ; vue plus haut END Remarque : ATTENTION, les transferts de disquette à mémoire ou de mémoire à disquette ne peuvent se faire qu'avec la mémoire de type CHIP. (d'où move.l #$10002,d1 pour l'AllocMem). Revenons à nos données de la structure IO request. Avec la commande TD_MOTOR, seul la valeur de io_Length est prise en compte. Elle permet de choisir entre l'activation et la désactivation du moteur. Si io_Length contient 1, alors le moteur sera activé, si io_Length contient 0, alors il sera désactivé. Il y a d'autres commandes qui permettent de piloter le lecteur de disquette, comme CMD_WRITE, par exemple, qui fonctionne comme CMD_READ sauf qu'il s'agit d'écriture au lieu de lecture. Il y a deux autres données qui sont assez intéressantes : io_Actual et io_Error. La première permet au trackdisk de retourner une valeur à un programme quand celui-ci à fait appel à certaines commandes. La seconde sert pour toutes les commandes : elle contient 0 si tout c'est bien passé, sinon elle contient un code d'erreur. Jetez un coup d'oeil sur le module Strap, ci-dessous, pour avoir des exemples. Si vous voulez, vous pouvez tester vos programmes, destinés à se trouver sur un BB, dans les conditions du reset grâce au source SimulBB.s qui se trouve sur la disquette de BBM. Allons encore plus loin dans Strap... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Je vous ai caché quelques choses à propos de Strap. J'ai dit qu'il mettait le pointeur d'exec en A6 et le pointeur sur une structure IO request en A1. Mais il fait plus : il met l'adresse du BB en A4. Attention, Commodore ne précise pas le contenu de A4, ce qui signifie que dans d'autres version de l'Amiga autre que la 1.3, ça peut changer. Grâce à cette adresse vous pouvez gagner jusqu'à... une instruction ! Exemple : Start dc.l 0,0,0 ; correspond aux 3 premiers ; mots longs ... lea Datas(pc),a0 move.b Phrase-Datas(a0),d0 Datas dc.l 0,0,0,0,0,0,0 Phrase dc.b 100 peut être remplacé par : Start dc.l 0,0,0 ; correspond aux 3 premiers ; mots longs ... move.b Phrase-Start(a4),d0 Datas dc.l 0,0,0,0,0,0,0 Phrase dc.b 100 C'est pas très utile, mais bon... Histoire de prouver que les registres sont bien initialisés tel que je l'ai dit, voici une partie du module Strap (version 1.3) désassemblé : L'adresse mémoire de RomTagStrap correspond à $FE83E0 * Désassemblage de Strap * Par Christophe LARATTE * Le 28/02/92 MsgPort = $5C IORequest = $2C ExecBase = 0 ln_Name = 10 mp_Flags = 14 mp_SigBit = 15 mp_SigTask = 16 mp_MsgList = 20 lh_Head = 0 mn_ReplyPort = 14 io_Command = 28 io_Error = 31 io_Actual = 32 io_Data = 36 io_Length = 40 io_Offset = 44 CMD_CLEAR = 5 CMD_READ = 2 TD_CHANGENUM = 13 TD_CHANGESTATE = 14 TD_MOTOR = 9 DMACON = $DFF096 _LVOAlert = -$06C _LVOAllocMem = -$0C6 _LVOAllocSignal = -$14A _LVOCloseDevice = -$1C2 _LVOCloseLibrary = -$19E _LVODoIO = -$1C8 _LVOFindTask = -$126 _LVOFreeMem = -$0D2 _LVOFreeSignal = -$150 _LVOOpenDevice = -$1BC _LVOOpenLibrary = -$228 RomTagStrap dc.w $4AFC ; ceci est une structure dc.l RomTagStrap ; resident (RomTag) dc.l EndSkip dc.b 1 dc.b $22 ; version 34 dc.b 0 ; type : inconnu dc.b $C4 ; priorité : -60 dc.l StrapName dc.l Identification dc.l StrapCode ; pointeur sur le code StrapName dc.b "strap",0 Identification dc.b "strap 34.4 (9 Dec 1987)",$D,$A,0 EndSkip dc.w 0 Dos0 dc.b "DOS",0 TrackdiskName dc.b "trackdisk.device",0 dc.b 0,0,0 RombootName dc.b "romboot.library",0 StrapCode movem.l d2/d3/a2-a5,-(sp) ; sauvegarde certains registres moveq #0,d3 sub.l a4,a4 ; a3 pointe sur RTS au cas lea Return,a3 ; où sa plante (voir ci-dessous) move.l #-1,a2 ; ATTENTION a2 va servir de flag link a5,#-$7E ; réserve de la place dans sub.l #$7E,a5 ; la pile move.l a6,ExecBase(a5) ; sauvegarde a6 (= *exec) move.l d3,4(a5) move.l #1160,d0 ; réserve de la mémoire en move.l #$10002,d1 ; CHIP jsr _LVOAllocMem(a6) tst.l d0 bne.s AllocOK movem.l d7/a5/a6,-(sp) ; Si pas de mémoire alors move.l #$30010000,d7 ; GURU #30010000 c.a.d move.l 4.w,a6 ; "GURU de BootStrap : mémoire jsr _LVOAlert(a6) ; insuffisante" movem.l (sp)+,d7/a5/a6 bra NoMem AllocOK move.l d0,a4 lea StrapName,a0 move.l a0,IORequest+ln_Name(a5) move.l a0,MsgPort+ln_Name(a5) sub.l a1,a1 jsr _LVOFindTask(a6) move.l d0,MsgPort+mp_SigTask(a5) move.b #0,MsgPort+mp_Flags(a5) lea MsgPort+mp_MsgList+lh_Head(a5),a0 move.l a0,(a0) addq.l #4,(a0) clr.l 4(a0) move.l a0,8(a0) moveq #-1,d0 ; Alloue un signal jsr _LVOAllocSignal(a6) move.b d0,MsgPort+mp_SigBit(a5) bpl.s SignalOK movem.l d7/a5/a6,-(sp) ; Si pas de signal alors move.l #$30070000,d7 ; GURU #30070000 c.a.d move.l 4.w,a6 ; "GURU de BootStrap : pas jsr _LVOAlert(a6) ; de signal" movem.l (sp)+,d7/a5/a6 bra NoSignal SignalOK lea MsgPort(a5),a0 move.l a0,IORequest+mn_ReplyPort(a5) lea TrackdiskName(pc),a0 ; lea IORequest(a5),a1 ; ouvre le trackdisk.device moveq #0,d0 ; moveq #0,d1 ; jsr _LVOOpenDevice(a6) ; tst.l d0 beq.s OpenTrackdiskOK movem.l d7/a5/a6,-(sp) ; Si pas trackdisk alors move.l #$30048014,d7 ; GURU #30048014 c.a.d move.l 4.w,a6 ; "GURU de BootStrap : erreur jsr _LVOAlert(a6) ; d'ouverture du trackdisk.device" movem.l (sp)+,d7/a5/a6 bra NoTrackdisk OpenTrackdiskOK cmp.l #0,a2 bne.s SansErreur lea RombootName(pc),a1 moveq #0,d0 jsr _LVOOpenLibrary(a6) tst.l d0 beq.s SansErreur move.l a6,-(sp) move.l d0,a6 jsr -$1E(a6) ; faute d'infos sur la librairie move.l a6,a1 ; romboot je me tais move.l (sp)+,a6 jsr _LVOCloseLibrary(a6) SansErreur move.w #$100,DMACON ; désactive le DMA Bitplane (écran) lea IORequest(a5),a1 move.w #CMD_CLEAR,io_Command(a1) ; met le buffer de piste jsr _LVODoIO(a6) ; invalide pour forcer la lecture tst.l d0 ; lors du prochain CMD_READ bne NoCommand lea IORequest(a5),a1 move.w #TD_CHANGENUM,io_Command(a1) ; jsr _LVODoIO(a6) ; récupère le compteur de tst.l d0 ; changement de disquettes bne NoCommand ; et le met en d2 move.l IORequest+io_Actual(a5),d2 ; lea IORequest(a5),a1 move.w #CMD_READ,io_Command(a1) ; lit le BB move.l #1024,io_Length(a1) ; long de 2 blocs move.l a4,io_Data(a1) ; en A4 move.l #0,io_Offset(a1) ; (les 2 premiers blocs) jsr _LVODoIO(a6) tst.l d0 bne.s NoRead move.l (a4),d0 ; test si c'est une disquette cmp.l Dos0(pc),d0 ; DOS,0 bne.s NoRead move.l a4,a0 ; move.w #$FF,d1 ; moveq #0,d0 ; vérifie que la checksum CalculChecksum ; est correcte add.l (a0)+,d0 ; bcc.s Retenue ; addq.l #1,d0 ; Retenue dbf d1,CalculChecksum ; not.l d0 ; bne.s NoRead lea IORequest(a5),a1 ; IORequest en A1 jsr 12(a4) ; exécution du code du boot tst.l d0 beq.s DosOK ; vérifie que D0 égal 0 move.l d0,-(sp) ; sinon move.l sp,a1 ; . movem.l d7/a5/a6,-(sp) ; . move.l #$30000001,d7 ; GURU #30000001 : lea (a1),a5 ; "Le programme du boot a renvoyé move.l 4.w,a6 ; une erreur" jsr _LVOAlert(a6) movem.l (sp)+,d7/a5/a6 addq.l #4,sp bra End DosOK move.l a0,-(sp) ; la routine en $FE8C9C sert à jsr $FE8C9C ; ajouter le lecteur DF0: dans la move.l (sp)+,a0 ; liste des lecteurs reconnus par move.l a0,a3 ; le système bra End NoRead add.l #1,a2 ; modifie le flag (=drapeau) cmp.l #0,a2 ; si 1er essai ble OpenTrackdiskOK ; on re-essaye move.l 4(a5),d0 ; sinon, on affiche la main bne.s DiskOK ; car pas de disquette bsr DisplayScreen DiskOK move.w #$8100,DMACON ; active le DMA Bitplane lea IORequest(a5),a1 move.w #TD_MOTOR,io_Command(a1) ; on arrète le moteur clr.l io_Length(a1) jsr _LVODoIO(a6) tst.l d0 bne.s NoCommand Wait lea IORequest(a5),a1 ; récupère le compteur move.w #TD_CHANGENUM,io_Command(a1) ; de changement de jsr _LVODoIO(a6) ; disquette tst.l d0 bne.s NoCommand cmp.l IORequest+io_Actual(a5),d2 ; si compteur toujours beq.s Wait ; à zéro on attend Wait2 lea IORequest(a5),a1 move.w #TD_CHANGESTATE,io_Command(a1) ; vérifie jsr _LVODoIO(a6) ; que l'insertion tst.l d0 ; d'une disquette bne.s NoCommand ; a bien eu lieu tst.l IORequest+io_Actual(a5) ; Si io_Actual=0 alors bne.s Wait2 ; il y a une disquette bra OpenTrackdiskOK Modif lea IORequest(a5),a1 ; exécute un move.w #TD_CHANGENUM,io_Command(a1) ; TD_CHANGENUM jsr _LVODoIO(a6) ; avant de sauter bra.s NoRead ; à NoRead NoCommand cmp.b #$1D,IORequest+io_Error(a5) ; examine l'erreur beq.s Modif ; Si io_Error=$1D alors pea 0 ; il y a eu un changement move.w IORequest+io_Command(a5),2(sp) ; de disquette pea 0 ; sinon GURU move.b IORequest+io_Error(a5),3(sp) ; avec à la place de move.l sp,a1 ; l'adresse dans le GURU movem.l d7/a5/a6,-(sp) ; le numéro de l'erreur move.l #$30068014,d7 lea (a1),a5 move.l 4.w,a6 ; GURU #30068014 : jsr _LVOAlert(a6) ; "erreur d'entrée/sortie movem.l (sp)+,d7/a5/a6 ; avec le trackdisk" addq.l #8,sp End bsr $FE8984 ; aucune idée du but de cette routine move.w #$8100,DMACON ; réactive le DMA Bitplane lea IORequest(a5),a1 jsr _LVOCloseDevice(a6) ; ferme le trackdisk NoTrackdisk moveq #0,d0 move.b 15(a5),d0 ; ? jsr _LVOFreeSignal(a6) ; libère le signal NoSignal move.l a4,a1 move.l #$1160,d0 jsr _LVOFreeMem(a6) ; libère la mémoire NoMem add.l #$7E,a5 unlk a5 move.l a3,a0 ; movem.l (sp)+,d2/d3/a2-a5 ; on saute sur le programme jmp (a0) ; d'initialisation Return rts ; ... données DisplayScreen ; ... je n'ai pas inclus la routine d'affichage de la main, ; car elle dépasse le cadre du BB Conclusion : Les registres sont bien initialisés comme je l'ai dit. Vous noterez que la structure IORequest est rattachée à une structure MsgPort. Ca permet au trackdisk de "réveiller" Strap une fois qu'il a accompli sa mission (la fonction DoIO "endort" la tâche). Pour finir voici une routine qui permet de calculer la checksum. ; Routine de calcul de checksum de BB ; en entrée : ; a0 : adresse du BB de 1024 octets move.l a0,a1 move.l #0,4(a0) ; on nettoie la place de la checksum move.l #255,d1 ; nombre de mots longs a additionner moveq #0,d0 ; on fait les additions en d0 Checksum1 add.l (a0)+,d0 bcc.s Checksum2 ; on prend en compte les retenues addq.l #1,d0 Checksum2 dbf d1,Checksum1 not.l d0 move.l d0,4(a1) ; on met la checksum à sa place Voila, c'est fini. Si ce que j'ai dit ne vous suffit pas, si vous avez trouvé des bétises dans la doc ou si vous avez simplement des trucs à ajouter, n'hésitez pas à m'écrire. Amigalement votre, Christophe LARATTE