Sam86
  Présentation  /   Téléchargement  /   Manuel  /   Tutoriel : "Assembleur et Amorce"


Programmer en assembleur
(et écrire un secteur d'amorce)


Intro

    On vous a toujours dit qu'un ordinateur ça fonctionne avec des "1" et des "0", que l'on compte en binaire, que l'on agence les bits en octet, que ça utilise des portes logiques ET, OU, OU-EXCLUSIF et peut-être même l'astuce du complément à 2... bla bla bla.

    En voila des choses bien intéréssantes ! Mais avec tout ça avez-vous vraiment compris comment ça marche :-\ ?

    Voici le programme :


Ce document est librement distribuable à condition que cela soit dans son intégralité, ce paragraphe en faisant partie. Si vous constatez une faute d'orthographe ou, horreur, une erreur ou si vous avez une suggestion, merci de me contacter pour en faire profiter le plus grand nombre.

Première Version : 25/6/03
Rectif. orthographe : 5/12/03
Intégration site web Sam86 : 12/01/04
Jean-Michel MORANI
ICQ 195876409
jean-michel.morani@esial.uhp-nancy.fr

1. A l'intérieur du processeur

    Un processeur est composé de registres (petites mémoires), d'une Unité Arithmetique et Logique (UAL ou ALU), etc... Sur chacun de ces éléments il y a une ou plusieurs lignes de contrôle. Elle permet par exemple d'indiquer à un registre qu'il doit prendre la valeur d'un bus, ou à l'UAL si elle doit faire une addition, une multiplication, une opération booléene ou encore un décalage, etc...
    En pilotant ces lignes on controle ce que fait le processeur, c'est ce qu'on appelle des micro-instructions. Mais ces lignes sont très nombreuses et donc le nombre de micro-instructions aussi, or certaines d'entre elles sont sans intêret. C'est pourquoi on ne commande pas directement un processeur avec des micro-instructions mais avec des instructions (plus compactes) qui sont converties par un décodeur d'instructions en (séquences de) micro-instructions.
    Bref, en voilà bien une explication hyper-hyper-simplifiée du fonctionnement d'un processeur et pourtant pour programmer un processeur il n'est même pas besoin de savoir tout cela.

2. La famille des 80x86

    8086/8088...80286, 80386, 80486, Pentium (80586), PII/PIII/P4 (80686)

    Il faut savoir qu'Intel a toujours pratiqué la compatibilité ascendante*. C'est à dire qu'un programme qui fonctionnait sur un 8086 fonctionne sur un P4 (en un peu plus vite). L'inverse n'est pas forcement vrai car Intel a rajouté des instructions au fil des générations.
    Ne nous perdons pas dans des méandres historiques. Dans ce tutoriel, et sauf mention contraire, toutes les instructions utlisées sont compatibles avec les i386 et postérieurs, c'est à dire la famille IA-32 (Intel Architecture 32 bits).

    *sauf l'instruction "mov cs, ax" qui a disparu avec le 286. Une instruction qu'utilisait entre autres le célèbre virus ping-pong.

3. Autour du processeur

    Le processeur communique avec l'exterieur via des ports, une ligne d'interruption et aussi par la mémoire à laquelle il accède directement.

    Par exemple :
    Le processeur (exécutant un programme) effectue des écritures sur certains ports pour configurer le contrôleur du clavier. Puis il continue d'exécuter le programme. On appuye sur une touche. Cela envoye un signal à la ligne d'interruption, ce qui a pour effet d'interrompre l'exécution du programme en cours pour lancer un sous programme prédéfini à l'avance. Celui ci va demander au processeur de lire le port du clavier. Et voila, vous avez lu au clavier. Ensuite vous pourrez écrire en mémoire vidéo pour que le caractère s'affiche à l'écran.
    Ici encore, l'explication est assez simpliste puisqu'en réalité il y a multiplexage des lignes d'interruption par le 8259A.

    La mémoire est découpée en segment qui peuvent contenir soit du code, soit des données. L'offset (en français le déplacement) est l'adresse d'une donnée par rapport au début d'un segment. L'ensemble Segment:Offset désigne un emplacement mémoire.

4. L'Architecture IA-32

    4.1 Les registres de segment

      Ils sont au nombre de six : CS, DS, ES, GS, FS, SS tous 16 bits.
      • CS - Code Segment, contient le code du programme en cours d'execution
      • DS - Data Segment, contient des données que l'on utilise dans le programme.
      • ES - Extra Segment, segment supplémentaire pour travailler sur des données
      • FS,GS - même principe que ES. Ils ont été rajouter à partir de i386 (donc bricolage).
      • SS - Stack Segment. Nous verrons le fonctionnement de la pile un peu plus loin
      Le couple CS:IP contient l'adresse de l'instruction suivante (IP : Instruction Pointer).

    4.2 Les registres généraux

      Pour commencer citons les quatres registres 32 bits de base.
      • EAX - Accumulateur utilisé pour stocker les données que l'on manipule
      • EBX - Souvent utilisé pour pointer des données dans DS
      • ECX - Compteur utilisé surtout dans les boucles
      • EDX - Pointeur pour les E/S (écriture sur port)

      Bien sur les significations sont données à titre indicatif et rien ne vous empêche de faire systématiquement des additions entre EBX et ECX. Sachez tout de même que certaines instructions utilisant EAX sont optimisées.

      Pourquoi toujours utiliser des registres 32 bits ? Parfois 16 suffisent amplement.

      AX, BX, CX et DX sont des registres 16 bits. En fait, ce sont des sous registres de EAX, EBX, ECX et EDX. Donc attention : si en modifiant AX, vous modifiez aussi les 16 bits de poids faible de EAX.
      Ces registres sont en fait les registres historiques des x86. Les E_X (qui signifient Extended _X) ont été rajoutés à partir du i386.

      Pourquoi toujours utiliser des registres 16 ou 32 bits ? Parfois 8 suffisent amplement.

      AL, BL, CL et DL sont des registres 8 bits. En fait ce sont des sous registres de AX, BX, CX et DX (mais vous l'aviez compris). Pareil : si en modifiant AX, vous modifiez aussi les 8 bits de poids faible (Low) de AX.
      AH, BH, CH et DH sont des registres 8 bits. En modifiant ces registres vous modifiez AX, vous modifiez les 8 bits de poids fort (High) de AX.
       
      Note : L'accès à ces registres est plus lent que l'accès aux registres 16 bits.

      Et avec ça qu'est ce que je vous mets ?

      ESI, EDI, ESP, EBP sont des registres 32 bits (comme le préfixe E l'indique).

      • ESI Source Index, pour pointer une données dans DS
      • EDI Destination Index, pour pointer une données dans ES
      • ESP Stack Pointer, pointe sur le sommet de la pile
      • EBP Base Pointer, pointe sur la Base de la pile

      [Les registres]
      Un petit dessin pour éclaircir tout ça.

    4.3 Le mot d'état machine (MSW)

      Encore un petit effort, c'est le dernier registre que nous allons voir mais il est hyper important !
      Après chaque opération arithmétique il est mis à jour. Par exemple si le résultat d'une soustraction est nul, ou s'il y a une retenue, alors des bits (ou indicateurs) de ce mot d'état machine seront mis à jour (indicateurs ou drapeaux, en anglais flags)

      Vous voulez tester l'égalité de deux registres. Faites une soustraction, puis faites un saut conditionnel en fonction de l'indicateur Z (zero). (Nous reverrons cela plus loin).

      [Machine Status Word]
      Machine Status Word (MSW)

5. le B.A.BA

    Ahhh ! Enfin un nouveau chapitre. Ohhh ! Nous allons enfin commencer à programmer.

    5.1 - MOV

      	mov	ax, 10		; Ici c'est du commentaire
      	mov	bx, 10h		; Après un point virgule le texte 
      	mov	cx, [123h]	; est ignoré. En assembleur les
      	mov	dx, bx		; commentaires sont très vivement recommandés.
      

      Que fait ce programme ?

      Il met 10 dans AX. Il met 10h dans BX (h=héxadécimal). Il met le contenu du mot à l'adresse DS:123h dans CX et le contenu de BX dans DX.

      A votre avis que fait "mov ax, [es:di+3]" ?

      Bien sûr vous ne pouvez pas écrire "mov bl, dx" car les opérandes destination et source ne sont pas de même dimension.

    5.2 - INC et DEC

      Ces instructions servent à incrémenter ou décrémenter une opérande.

      	inc	ax		; ax = ax + 1
      	dec	[123h]		; [123h] = [123h] - 1
      

      Certains programmes d'assemblage vous permette d'écrire "inc ax, bx, al" ce qui signifie "inc ax" puis "inc bx" puis "inc al".

    5.3 - ADD, ADC, SUB, SBB, AND, OR, XOR et CMP

      • ADD - addition
      • ADC - addition pour les nombres signés (en complément à 2)
      • SUB - soustraction
      • SBB - soustraction pour les nombres signés (en complément à 2)
      • AND - ET logique bit à bit
      • OR - OU logique bit à bit
      • XOR - OU-EXCLUSIF logique bit à bit
      • CMP - soustraction. Attention ne renvoie pas de résultat
      L'instruction CMP sert à comparer deux éléments. Son seul intérêt, puisqu'elle ne renvoie pas de résultat, est de mettre à jour les indicateurs du MSW. Elle sera utile plus tard quand nous verrons les sauts conditionnels.

      	add	ax, 10		; ax = ax + 10
      	add	ebx, [123h]	; ebx = ebx + [123h]
      

      Notez que le résultat va dans la première opérande. Les indicateurs du mot d'état machine sont mis à jour.

    5.4 - JMP

      	mov	ax, 10
      	jmp 	saut
      	add	ax, 10		; Cette ligne n'est jamais éxécutée.
      saut:
      	add	ebx, [123h]
      
      Saut est ce qu'on appelle une étiquette. Elle représente une adresse dans le programme. En faisant "JMP Saut", vous allez directement à l'instruction qui suit l'étiquette.

    5.4 - Jcc : les sauts conditionnels

      • JA - Saute si au-dessus (CF=0 and ZF=0)
      • JAE - Saute si au-dessus ou Égal (CF=0)
      • JB - identique à JC
      • JBE - Saute si (CF=1 or ZF=1)
      • JC - Saute si l'indicateur de retenue est actif (CF=1)
      • JE - Saute si égal (ZF=1)
      • JG - Saute si plus Grand (ZF=0 and SF=OF)
      • JGE - Saute si plus Grand ou Égal (SF=OF)
      • JL - Saute si plus petit (SF<>OF)
      • JLE - Saute si plus petit ou Égal (ZF=1 or SF<>OF)
      • JO - Saute si dépassement de capacité (OF=1)
      • JP - identique à JPE
      • JPE - Saute si pair(PF=1)
      • JPO - Saute si impair (PF=0)
      • JS - Saute si signé (SF=1)
      • JZ - Saute si nul (ou JE) (ZF =1)

      Vous pouvez nier les conditions ci-dessus en intercalant un 'N' après le J. (par exemple JNB)

      • JCXZ - Saute si CX = 0
      • JECXZ - Saute si ECX = 0
      Exemple 1:
      	mov	cx, 10		; cx = 10
      boucle:
      	jcxz	fin		; Si cx = 0 alors fin
      	...
      	dec	cx		; cx = cx - 1
      	jmp	boucle
      fin:
      	...
      
      
      Exemple 2:
      	mov	ax, 0		; ax = 0
      boucle:
      	cmp	ax, 10		; ax - 10 ?
      	jz	fin		; Si = 0 alors fin
      	...
      	inc	ax		; ax = ax + 1
      	jmp	boucle
      fin:
      	...
      

      Vous remarquerez qu'il est plus simple d'utiliser CX pour les boucles, mais vous pouvez toujours contourner les recommandations.

    5.5 - PUSH, POP, Bref la pile !

      Elle vous créera bien des ennuis mais vous ne pourrez plus vous en passer : la pile (stack). C'est assez délicat à expliquer (et donc je pense aussi à comprendre), mais vous verrez c'est très pratique.

      La pile est un endroit en mémoire, dans le segment mémoire SS, où l'on stocke temporairement des données, ce qui évite de créer des variables (surtout quand on ne sait pas exactement combien il en faudra).

      Imaginez que tous vos registres sont occupés (et que vous ne voulez pas créer de variable). Que faire ! Empilez des données, utilisez les registres, puis récupérez ces données plus tard.

      Exemple :
      	push	ax		; ax -> Pile
      	...
      	pop	bx		; bx <- Pile
      

      Oho, cela ne parait pas si compliqué que ça !

      Mais voyons, pour mieux comprendre, un équivalent :

      	; push	ax
      	sub	sp, 2   	; parceque AX prends 2 octets
      	mov	[ss:sp], ax
      
      	...
      	; pop	bx
      	mov	bx, [ss:sp]
      	add	sp, 2		
      

      Le registre SP pointe sur le dernier élément empilé. Notez bien que SP decroit quand on empile. La pile doit toujours être initialisé avant de l'utiliser. Autrement vous êtes suceptible d'écrire n'importe où en mémoire. Parfois les OS s'en occupe. Parfois :

      	mov	ax, cs  	; "mov ss, cs" n'existe pas
      	mov	ss, es
      	jmp	saut1
      
      	db	10 dup ("PILE ")
      
      saut1:  mov     sp, offset saut1 ; une petite subtilité
      	...
      

      Notez aussi l'existence de PUSHF/POPF qui permette d'empiler/de dépiler le mot d'état machine, ainsi que PUSHA et POPA qui empile/dépile tous les registres d'un coup.

    5.6 - CALL et RET/RETF - les appels

      	jmp	debut
      routine:
      	inc	ax
      	dec	di
      	ret
      
      debut:	
      	xor	ax, ax		; ax = 0
      	call	routine
      

      Lorsque l'on fait un call, IP (ou CS:IP selon les cas) est empilé puis on fais un saut à l'étiquette indiquée. A l'exécution de RET respectivement RETF, on dépile IP respectivement CS:IP.

      N'oubliez pas désormais de mettre un RET à la fin de vos programme pour rendre la main à celui qui vous a appellé.

    5.7 - INT

      Je ne détaillerai pas ici les mécanismes précis des appels à interruptions logicielles. Sachez tout de même qu'avant l'avènement des librairies dynamiques de fonctions (.DLL, .OVL, etc...), tous les appels systèmes fonctionnaient ainsi.

      Dans AH, on mets un numéro de fonction. Dans les autres registres les paramètres, puis on appelle une interruption, par exemple pour les appels au BIOS Graphique, la 10h.

      Exemple :
      
      	mov	ah, 0eh 	; fonction "écrire un caractère"
      	mov	al, 'A' 	; le dit caractère
      	mov	bx, 1Eh 	; en Jaune (E) sur du bleu (1)
      	int	10h     	; appel au BIOS Graphique
      	ret
      

      Vous trouverez facilement sur internet ou dans une bible PC, la liste des fonctions.

      Il existe encore de nombreuses instructions mais restons en là pour le B.A.BA.

6. Votre premier programme

    Il est temps de passer à la pratique. Pour cela munissez vous d'un éditeur de texte (edit, bloc-notes, vi, etc...) et d'un assembleur (sam86, a86, tasm, masm, nasm, fasm). J'ai personnellement une préférence pour Sam86 (que vous comprendrez dans la partie 7).

    Sam86, a86 génèrent par défaut des programmes ".com" qui tourne sous DOS (org 100h). (Tasm génére des .exe, donc avec une entête)

    • Tapez ceci dans un éditeur :
      	mov	ax, cs
      	mov	ds, ax
      
      	mov	si, offset msg  ; Avec nasm => "mov si, msg"
      
      boucle:
      	mov	al, [si]	; [ds:si] => al
      	inc	si      	; si = si + 1	
      	cmp	al, 0   	; Si AL = 0
      	je	fin     	; Alors fin
      	mov	ah, 0eh 	; Sinon ecrire le caractère
      	mov	bx, 07h 	; en gris sur noir
      	int	10h
      	jmp	boucle
      fin:	ret
      
      msg	db	'Hello World !!!',0
      
      
    • Enregistrez le avec une extension .asm
    • Compilez le ("sam mon_prgm.asm", "nasm mon_prgm.asm -o mon_prgm.com")
    • Lancez le !

    Oups, c'est vrai nous n'avons pas encore vu les DB, DW et DD, mais vous avez compris qu'il s'agissait d'intégrer des données au programme (avec une étiquette). C'est une variable quoi !

    Mettons cela sous forme de procédure.

    	jmp	debut
    
    ecrit_:	
    	mov	al, [si]	; [ds:si] => al
    	inc	si      	; si = si + 1	
    	cmp     al, 0   	; Si AL = 0
    	je      fin     	; Alors fin
    	mov     ah, 0eh 	; Sinon ecrire le caractère
    	mov     bx, 07h 	; en gris sur noir
    	int     10h
    	jmp     ecrit_
    fin:	ret
    
    debut:
    	mov	ax, cs
    	mov	ds, ax
    
    	mov	si, offset msg  ; Avec nasm => "mov si, msg"
    	call	ecrit_
    
    	ret
    
    msg	db	'Hello World !!!',0	
    

    Mettons la "procédure" dans un autre fichier.

    Le fichier ecrit.inc :
    ecrit_:	mov	al, [si]	; [ds:si] => al
    	inc	si       	; si = si + 1	
    	cmp     al, 0   	; Si AL = 0
    	je      fin		; Alors fin
    	mov     ah, 0eh 	; Sinon ecrire le caractère
    	mov     bx, 07h 	; en gris sur noir
    	int     10h
    	jmp     ecrit_
    fin:	ret
    
    Votre programme :
    	jmp	debut
    
    	include	"ecrit.inc"
    debut:
    	mov	ax, cs
    	mov	ds, ax
    
    	mov	si, offset msg  ; Avec nasm => "mov si, msg"
    	call	ecrit_
    
    	ret
    
    msg	db	'Hello World !!!',0	
    

    Et rajoutons un macro pour la route. (syntaxe pour Sam86 uniquement)

    fichier 'ecrit.inc' :
    MACRO ecrit msg
    	mov	si, offset {db msg, 10, 13, 0}
    	call	ecrit_
    ENDM
    
    ecrit_:
    	lodsb
    	cmp     al, 0
    	je      fin
    	mov     ah, 0eh
    	mov     bx, 07h
    	int     10h
    	jmp     fin_ecrit
    fin_ecrit:
    	ret
    
    Votre programme :
    	jmp	debut
    
    	include "ecrit.inc"     ; recopie le fichier ecrit.inc ici
    
    debut:	mov	ax, cs
    	mov	ds, ax          ; DS = CS
    
    	ecrit 	"Hello World !!!"
    
    	ecrit 	"Coucou"
    
    	ret
    

    Ca ne parait plus très simple, mais en fait ça vous simplifiera la suite des opérations. Regardez votre programme, on dirait presque un langage évolué !
    "offset {db msg, 10, 13, 0}" est transformé en "offset xxx", et plus loin Sam86 insère de façon opportune "xxx db msg, 10, 13, 0".

    Note : Ici on ne passe pas de paramètre aux routines, qui elles même ne renvoient pas de valeurs. En générale on utilise la pile, mais sachez qu'il existe plusieurs conventions d'appel (Calling convention) (C, Pascal, etc...).

7. Votre première amorce

    Le but de ce chapitre est de faire un petit programme que l'on transferera sur l'amorce d'une disquette. Il s'executera lorsque vous tenterez de démarrer dessus. Mais avant de faire cela il va falloir créer un outil qui nous manque.

    7.1 Transferer du code vers l'amorce - tea.asm

      C'est l'occasion pour vous de lire du code (c'est à dire la meilleur manière de progresser).
      Si vous utilisez linux, utilisez "dd if=/tmp/amorce.bin of=/dev/fd0" au lieu de tea.

      Que va faire ce programme :

      • Ouvrir un fichier "amorce.bin"
      • Lire les 512 premiers octets vers un tampon
      • Ecrire ce tampon vers l'amorce de la disquette
      le fichier "tea.asm" :
      ; TEA -> Tranfert ficher En Amorce
      
      ; Avant d'exécuter ce programme insérez une disquette ne contenant aucune donnée 
      ; importante dans le lecteur A. Celle-ci ne sera plus lisible par un OS standard.
      ; Vous pourrez la récuperrer en la formatant.
      ; Attention avant de faire quoi que ce soit, soyez sûr de comprendre ce que vous 
      ; allez faire car vous allez écrire en amorce d'un disque ! Je décline...
      ; Ne pas modifier des lignes au hasard, sous peine de perdre des données du hdd. 
      
      
      	mov     ax, cs  ; DS <- CS
      	mov     ds, ax
      
      	jmp     debut
      
      ecrit:
      	lodsb
      	cmp     al, 0
      	je      @f
      	mov     ah, 0eh
      	mov     bx, 07h
      	int     10h
      	jmp     ecrit
      @@:	ret
      
      
      msg1	db	'TEA -> Tranfert ficher En Amorce', 10, 13, 0
      msg2	db	'Impossible d''ouvrir amorce.bin', 10, 13, 0
      msg3	db	'Erreur lors de l''ecriture en amorce', 10, 13, 0
      msg4	db	'Ok', 10, 13, 0
      nom_fichier db	'amorce.bin'
      fichier	dw	0
      
      
      debut:  
      	mov	si, offset msg1
      	call	ecrit
      
      	mov	ah, 3dh                 ; ouverture d'un fichier
      	mov	al, 0                   ; en mode lecture seule
      	mov	dx, offset nom_fichier
      	int	21h
      
      	jnc	@f                      ; On teste si ça c'est bien passé.
      
      	mov	si, offset msg2         ; Bah non
      	call	ecrit                   ; Donc message d'erreur
      	ret                             ; et ciao
      
      @@:	mov	[fichier], ax
      
      	mov	ah, 3fh                 ; Lire le fichier
      	mov	bx, [fichier]
      	mov	cx, 512                 ; 512 octets
      	mov	dx, offset tampon
      	int	21h                     ; Appel MS-DOS
      
      
      	mov	ax, cs
      	mov	ds, ax                  ; es <- cs
      	mov	bx, offset tampon
      
      	mov	ah, 03h                 ; Ecrire
      	mov	al, 1                   ; 1 secteur
      	mov	ch, 0                   ; sur la piste 0
      	mov	cl, 1                   ; le numéro 1
      	mov	dh, 0                   ; face 0
      	mov	dl, 0                   ; sur le lecteur A
      	int	13h                     ; int. disque
      
      	jnc	@f                      ; On teste si ça c'est bien passé.
      
      	mov	si, offset msg3         ; Bah non
      	call	ecrit                   ; Donc message d'erreur
      	ret                             ; et ciao
      
      @@:	mov	si, offset msg4
      	call	ecrit
      	ret
      
      tampon	db	512 dup (0)
      

    7.2 L'amorce - amorce.asm

      Que va faire notre amorce ?

      • Afficher un message
      • Attendre qu'on appuye sur une touche
      • Redémarrer la machine (à chaud)
      le fichier "amorce.asm" :
      	org	7C00h		; prgm d'amorce
      
      	mov	ax, cs
      	mov	ds, ax
      
      	mov	si, offset msg	
      
      boucle:
      	lodsb           	; AL = [DS:SI], Si = Si + 1
      	cmp     al, 0   	; Si AL = 0
      	je      fin     	; Alors fin
      
      	mov     ah, 0eh 	; Sinon ecrire le caractère
      	mov     bx, 07h 	; en gris sur noir
      	int     10h	
      	jmp     boucle	
      fin:    
      
      	mov	ah, 0		; Lire un caractère
      	int	16h		; Int. clavier
      
      	int	19h		; On continue le retente de démarrer
      
      msg	db	'Hello World !!!',10,13,'Appuyez sur une touche !',0
      	
      
      	org	7DFEh		; Avec nasm : "times 512-($-$$)-2 db 0"
      	dw	0AA55h		; Signature
      
      Note : Avec Sam86, utilisez l'option -boot et ne copiez pas les lignes vertes.

    7.3 Action !

      Pour l'exécuter :

      • Créez le fameux fichier tea.asm
      • Compilez le !
      • Créez le fameux fichier amorce.asm
        (avec Sam : sam -boot amorce, avec Nasm : nasm amorce.asm -f bin -o amorce.bin" et n'oubliez pas d'enlever directives "offset".)
      • Compilez le !
      • Si vous avez utilisé a86, supprimez tous les octets nuls qui sont avant le code (il y en a 7C00h).
      • Mettez une disquette vide dans le lecteur A.
      • Exécutez tea.com
      • Redémarrez sur la disquette

      Eventuellement il faudra changer des options dans le BIOS pour que le système cherche à démarrer sur la disquette.

      Quand vous aurez finit de vous amuser, vous pourrez reformater la disquette et consulter d'autres tutoriels pour approfondir vos connaissances.

Bonne chance !
Jean-Michel MORANI


Ce document est librement distribuable à condition que cela soit dans son intégralité, ce paragraphe en faisant partie. Si vous constatez une faute d'orthographe ou, horreur, une erreur ou si vous avez une suggestion, merci de me contacter pour en faire profiter le plus grand nombre.
Merci Marco.

Première Version : 25/6/03
Rectif. orthographe : 5/12/03
Intégration site web Sam86 : 12/01/04


Site crée par Nayxx