[delphi/asm] les conventions d'appel

Si vous avez entendu parlé de STDCALL, PASCAL, REGISTER, CDECL et SAFECALL, alors vous êtes pile dans le sujet. Le mot "pile" est à double tranchant... vous allez voir.

Le sujet risque de ne pas brancher les débutants, car ça ne bousculera pas leurs habitudes sous Delphi. Pour les autres, je conseille de ne pas sauter de paragraphes.

Sur ce, bonne lecture !

Contenu

 I) Introduction
 II) La compilation et la liaison
 III) Les registres
 IV) La pile
 V) Passage des paramètres
 VI) La convention PASCAL
 VII) La convention REGISTER
 VIII) La convention STDCALL
 IX) Le problème lié à PASCAL
 X) Les conventions SAFECALL et CDECL
 XI) Valeur de retour pour les fonctions
 XII) Débordement de pile
 XIII) Conclusion

I) Introduction

Votre application fraîchement produite avec Delphi est stockée sur votre disque dûr. C'est un fichier logique qui n'attend que de s'exécuter. Il a besoin d'un exécuteur, d'un peu de mémoire et de la matière d'oeuvre.

Un double-clic et c'est parti. Que se passe-t-il ? Déjà Windows vérifie le fichier et le charge. Ce qui nous intéresse dedans, c'est le CODE, le reste étant un espace de description du fichier, de données initialisées ou pas, et de ressources. Le CODE est une suite de zéros et de uns qui correspondent par petits groupes de taille variable à des instructions. Ces dernières permettent de commander le micro-processeur de manière à obtenir l'effet souhaité : afficher un texte, effectuer un calcul...

Le problème est que ces actions requièrent souvent un paramétrage. Le résultat est au final modifié. Mais comment faire pour dire au micro-processeur (le moteur de notre PC) quels sont ces paramètres si précieux ? Patience...

Les fichiers EXE, SCR, COM peuvent s'exécuter par un double-clic.
Les bibliothèques DLL sont des modules non-exécutables mais qui contiennent du CODE exécutable.
Les drivers VXD et DRV sont particuliers et chargés par le système d'exploitation (Windows...).
Des fichiers plugins ou addons sont généralement de type DLL.

II) La compilation et la liaison

Il faut avoir à l'idée que la mémoire n'est qu'une longue suite de zéros et de uns . Mais pour éviter les conflits, les applications se réservent des zones pour elles. Quand je dis mémoire, je parle des barrettes de RAM toutes noires placées en slot sur la carte mère.

A ce propos, on appelle mode protégé l'environnement dans lequel les accès mémoire sont controlés. Il évite que des applications viennent perturber le fonctionnement d'autres applications, notamment celui du système d'exploitation (OS, operating system). Si un mauvais accès se produit, le processeur râle et c'est l'exception. L'OS termine l'application pour des questions de sécurité (éviter un reboot par exemple). C'est pour cela que le déroulement d'une application est bloqué quand s'affiche un EDivByZero, EPriviliege, EAccessViolation, EAbort... etc.

Que se passe-t-il quand vous écrivez :

var i : integer;
begin  i:50;

Le CODE dit tout simplement de placer la valeur 50 à un emplacement précis de la mémoire, symbolisé par le nom de variable. Ce lieu est repéré par l' adresse . Il ne faut pas confondre l'adresse avec la valeur (localisée par l'adresse). Fameux problème du contenant et du contenu.

L'emplacement et la taille du lieu sont tout simplement écrits en dûr dans le fichier EXE. Mais comme au développement de votre application sous Delphi vous ne pouvez pas connaître quelle sera cette adresse au moment de la compilation, vous utilisez à la place des noms de variable. C'est plus simple, mais surtout plus explicite.

Quand vous demandez à Delphi de compiler, il se créé des fichiers DCU. C'est un fragment de CODE non-exécutable de votre application où les adresses ne sont pas encore résolues. Cela veut dire que le fichier sait qu'il faut copier la valeur 50, mais il ne sait pas encore où. Ce fichier DCU fonctionne sur le même principe que les fichiers OBJ, mais comportent en plus des informations de débogage.

Vient ensuite la liaison. On assemble tous les DCU ensemble, on résoud les adresses et le tout devient application EXE-32 bits. Les DCU ont été réécrits et on sait faire quoi, où et comment !

III) Les registres

Dans tous les cas, je traite des Intel x86.

Le processeur contient 8 mémoires spécifiques, appelées registres. On y accède en utilisant la programmation assembleur entre les balises asm...end. Delphi les utilise à tour de bras, mais vous les cache bien.

EAX  un accumulateur générique pour stocker des nombres et renvoyer le résultat des fonctions numériques entières
EBX  un entier pour fixer une adresse et pouvoir faire des décalages
ECX  permet de créer des boucles FOR, REPEAT, WHILE
EDX  un stockage de destination pour les données, qui sert aussi à stocker des entiers 64 bits par un couple EDX:EAX
ESI  un index de source
EDI  un index de destination
EBP  un pointeur de base pour la mémoire et le contrôle des appels internes
ESP  le pointeur de pile qui servira à repérer les paramètres passés, toujours copié dans EBP par Delphi

EIP n'est pas un registre mais désigne l'adresse en cours de l'instruction du CODE de l'application. C'est ce qui est affiché dans les messages d'exception. Il permet de localiser les instructions fautives de manière à pouvoir les corriger si possible.

Ce qui est dit plus haut n'est que convention d'usage. On peut accumuler ou boucler dans EDX si on veut... le problème est qu'il ne faut pas se perdre dans les notations, ni modifier abusivement les registres.

Il n'y a cependant que 6 registres utiles. En effet, EBP et ESP servent à la gestion des entrées et sorties de votre programme quand vous appelez une procédure ou une fonction (principe du retour à l'appelant). Il ne faut pas les modifier abusivement, sous peine de créer de graves erreurs. Dans la plupart des applications, ESP est très souvent copié dans EBP.

Sur les 6 registres restants, on distingue 2 catégories.

1) EBX, ESI et EDI qui doivent être sauvegardés au sein d'une procédure lorsqu'ils font l'objet d'une modification. Ils permettent de localiser les données source et de destination quand vous faîtes des appels internes, ou externes sur des DLL (nommés API).

2) EAX, ECX et EDX sont modifiables à volonté. Ce sont de la compote informatique pour l'exécution des instructions. Comme la copie directe d'une variable à une variable n'existe pas (l'opcode n'est pas géré par le processeur), on fait une copie intermédiaire dans l'un de ces registres. Après un API, ils sont toujours modifiés. Donc si vous avez stocké des valeurs dedans et que vous en avez besoin après l'appel, il faut les préserver avant. Pour cela, on les stocke dans des variables ou on les pousse sur la pile (voir ci-après).

Les registres sont accessibles à différents niveaux : DWord, Word ou Byte (respectivement 32, 16 et 8 bits). Prenons l'exemple de EAX qui peut stocker un nombre 32 bits (4 octets).

<--- EAX ---->               <-AX->             
00  00  00  00       00  00  00  00       00  00  00  00

E signifie Extended (étendu)
H signifie High (partie haute)
L signifie Low (partie basse)

Par analogie, on a pareil avec les autres registres, sauf pour ESI et EDI qui se décomposent uniquement en SI et DI (16 bits tous les deux).

IV) La pile

On vient de voir la mémoire RAM et les registres. Vient la dernière technique de mémorisation : la pile. C'est pour cela que je disais qu'on était pile dans le sujet ;)

C'est une partie de la mémoire RAM qui en en relation avec les registres. Elle fonctionne sur le principe : j'empile et je dépile. C'est à dire : si j'ai des données à y stocker, je met tout sur le dessus. Je peux lire toutes les données qui sont contenues, mais je ne peux pas effacer les éléments du dessous tant que ceux du dessus sont toujours là.

Prenons l'exemple de mon cageot de bouteilles. Je peux voir mes différents vins délicieux à travers les barreaux. Je pose dessus mon Chinon rouge. Ah zut, j'ai besoin des rosés tout en bas. J'ai la possibilité de sortir le cageot au risque de tout faire tomber. NON !! J'enlève les cageots du dessus (je les mets dans la RAM ou les registres), j'isole mon rosé, et je replace tout dans le bon ordre.

Pour empiler, je fais un PUSH.
Pour dépiler, je fais un POP.

Quand vous empilez ou dépilez, toute la mémoire n'est pas déplacée : ce serait une perte de temps scandaleuse. On modifie tout simplement ESP (et/ou EBP) qui désignent l'emplacement de l'élément du dessus.

Quand vous écrivez begin, Delphi pousse/mémorise EBP et copie ESP dans EBP.
Quand vous écrivez end, Delphi tire/restaure EBP.

Ceci n'est pas toujours vrai (voir plus loin avec la convention Register).

V) Passage des paramètres

Le langage que connaît le processeur n'est que zéros et uns. Il est très limité. On ne peut donc pas dire en une seule instruction "met dans variable le résultat de (34+Variable)/2". De même, on ne peut pas dire directement "appelle fonction(param1,param2)".

Imaginons que nous avons :
function Empilage(Param1,Param2,Param3:integer):integer;

Pour appeller la fonction, nous allons tout d'abord stocker les paramètres : Param1, Param2 puis Param3. Tout est prêt et on déclenche le CODE de la fonction Empilage.

Oui mais non... ! Pourquoi n'avons nous pas stocké Param3, Param2 puis Param1. Et puis, que se passe-t-il si nous avons 80 paramètres ?? Dans tous les cas, il faudra bien passer tous les paramètres, le tout étant de ne pas être en manque de mémoire.

Il est impossible de passer les paramètres par la RAM. Le processeur n'ira pas chercher tout seul si on ne le lui dit pas où. Tout doit se faire avec les registres et la pile. Logique : les registres sont liés au processeur, la pile aussi via EBP et ESP.

On peut mettre une partie des paramètres dans les registres, le reste dans la pile. Ou alors tout mettre directement dans la pile.

Dans la suite des exemples, nous allons prendre le code source suivant pour support :

program Convention;
{$HINTS Off}
  function Empilage(Param1,Param2,Param3,Param4:integer):integer;
  begin
    //On n'utilise aucun paramètre. C'est juste pour voir
    //comment ils sont transmis en fonction des conventions
    Result: =0;
  end;
var MonResultat : integer;
begin  MonResultat:Empilage(1,2,3,4);

Cliquez dans la marge au niveau de la ligne MonResultat:=... afin qu'elle devienne colorée. On vient de définir un point d'arrêt . C'est-à-dire que Delphi stoppera l'exécution de l'application quand cette dernière arrivera à l'instruction surlignée.

VI) La convention PASCAL

C'est une spécificité de Delphi. Par défaut, quand on écrit rien derrière une fonction, la convention choisie est PASCAL. Mais lorsque l'Optimisation de la compilation est activée (toujours le cas, car on a $O+ par défaut), la convention par défaut est REGISTER. Une fois encore, REGISTER n'est pas le standard de Windows qui fonctionne en STDCALL (Standard Call).

Pour désactiver l'optimisation, passez par les options du projet ou (conseillé) rajoutez la ligne suivante sous $Hints :
{$O-}

Remplaçons la ligne 3 par celle-ci :
function Empilage(Param1,Param2,Param3,Param4:integer):integer; pascal;

Exécutons l'application. Le raccourci est la touche F9. Delphi doit montrer une petite flèche verte dans la marge. Ouvrons la Fenêtre CPU en utilisant le menu Voir. Testez dans le sous-menu Fenêtre de débogage si vous ne trouvez pas. Nous visualisons ceci :

PUSH $1
PUSH $2
PUSH $3
PUSH $4
CALL Empilage

Delphi vient d'utiliser la pile pour transmettre les paramètres. L'élément sur le dessus de la pile est $4, car il a été empilé en dernier.

== > La convention PASCAL utilise la pile, empile de la gauche vers la droite, et nettoie la pile automatiquement.

Que veut dire nettoyer la pile ? En ayant empilé des données, nous avons modifié l'aspect de la pile. Elle n'est plus comme elle était, car l'élément supérieur a changé. Il faut donc dépiler nos quatres arguments.

Reprenez la fenêtre CPU et remontez plus haut avec l'ascenseur principal. Nous voyons ceci :

Convention.dpr.6: begin
 PUSH EBP
 MOV EBP, ESP
==dpr.9: Result:=0;==
 XOR EAX, EAX

dpr.10: end;

 POP EBP
 RET $10

En ayant utilisé précédemment l'instruction CALL, on s'est déplacé dans le CODE à un autre endroit. Mais comme toute fonction a une fin, il faut revenir là où on était afin de poursuivre l'exécution du programme. C'est à cela que sert l'instruction RET. Mais en étant en convention PASCAL, c'est à la fonction de nettoyer la pile. Le chiffre $10 en hexadécimal permet d'effectuer cette opération.

Pourquoi $10 ? Eh bien, cette valeur vaut en décimal 16. Or nous avons passé 4 entiers de 32 bits en paramètre. Signalons que 32 bits font 4 octets. Ainsi, 4 fois 4 octets en paramètre, ça fait 16 octets en trop sur la pile. Le RET effectue le nettoyage en modifiant le registre ESP. On est revenu sur le haut de la pile.

Magique !

Mais j'ai une colle pour vous... pensez-vous réellement que la valeur $4 est sur le haut de la pile, c'est-à-dire à l'adresse [ESP * ? On vient de dire que RET permet de revenir là d'où on vient. Et elle est où cette adresse d'où on vient ? Elle a tout simplement été empilée par l'instruction CALL... Et c'est même encore plus subtile, car c'est l'adresse de l'instruction suivante qui a été empilée, car on veut poursuivre l'exécution.

Encore mieux... regardez le CODE correspondant à begin. On empile encore EBP par dessus. Ce qui veut dire que la pile ressemble à ça :

[ESP + $00 * Registre EBP sauvegardé
[ESP + $04 * EIP de l'instruction de retour
[ESP + $08 * 4
[ESP + $0C * 3
[ESP + $10 * 2
[ESP + $14 * 1

Nous avons donc empilé 6 fois 4 octets, soit 24 octets ($18). Pourquoi ne fait-on pas RET $18 alors ?

Tout d'abord, avant le RET, on fait une restauration subliminale avec POP. Ca fait déjà 4 octets en moins. Ensuite, RET dépile le dessus pour son propre usage. Ce qui nous fait bien seulement $10 octets à dépiler à la fin.

C'est quand même un sacré fardeau d'utiliser un appel pour simplement mettre 0 dans EAX...

Dernière chose. Comme Delphi copie ESP dans EBP, tous les arguments sont accessibles en faisant simplement [EBP+Offset * , où Offset est une valeur entière en octets. C'est à cela que sert EBP : être un pointeur de base !

VII) La convention REGISTER

C'est la convention par défaut utilisée pour les procédures et fonctions quand l'optimisation est activée. Remplaçons le mot PASCAL par REGISTER et effectuons à nouveau le petit Travail Pratique. Attention !! Cette fois nous avons 5 paramètres.

program Convention;
{$HINTS Off}
  function Empilage(Param1,Param2,Param3,Param4,Param5:integer):integer; REGISTER;
  begin
    Result: =0;
  end;
var MonResultat : integer;
begin  MonResultat:Empilage(1,2,3,4,5);

La fenêtre CPU nous montre ceci :

PUSH $00000004
PUSH $00000005
MOV ECX, $00000003
MOV EDX, $00000002
MOV EAX, $00000001
CALL Empilage

Le résultat est nettement différent.

==> La convention REGISTER travaille de la gauche vers la droite, place les 3 premiers paramètres dans des registres préférentiels, empile les autres paramètres si présents, et nettoie la pile automatiquement.

On voit que Delphi utilise des registres dans un ordre préférentiel qui est : EAX, EDX puis ECX. Ceci conforte toujours l'idée que seuls ces 3 registres sont conventionnellement modifiables à volonté. Ici nous avons 5 paramètres mais seulement 3 registres (les autres sont occupés pour autre chose). La seule possibilité est de passer par la pile pour mémoriser les 2 derniers paramètres. On aurait pu empiler 77 paramètres s'il y en avait eu 80 en tout.

Remontons plus haut dans la fenêtre CPU pour atteindre notre fonction Empilage. Nous visualisons :

PUSH EBP
MOV EBP, ESP
XOR EAX, EAX
POP EBP
RET $0008

Nous n'avons pas $10, car seulement 2 seul de nos 5 paramètres ont attérit dans la pile. Les 3 autres restent dans les registres et seront écrasés en fonction du contexte informatique. Comme au paragraphe précédent, nous n'avons pas à nous soucier des (dés)empilages automatiques liés à CALL et RET.

Pour montrer un autre effet de la pile, remplacez par cette nouvelle ligne. Je compte faire attérir 2 octets sur la pile, et non 8.
function Empilage(Param1,Param2,Param3:integer; Param4,Param5:byte):integer; register;

Va-t-on avoir RET $2 ? Eh bien non... car dans tous les cas, un entier 32 bits est empilé. Cela ne pose pas de problème : il suffit de dépiler un Integer 32 bits, mais de ne considérer que AL (la partie ultra-basse de 1 octet), c'est-à-dire les 8 bits les plus à droite dans la représentation binaire du nombre.

Intéressons nous maintenant sur ce qui se passe si nous avons que 3 arguments :
function Empilage(Param1,Param2,Param3:integer):integer; register;

Vous avez vu ? Il ne reste que ceci :

XOR EAX, EAX
RET

Comme nous n'avons pas au minimum 4 paramètres, il est inutile de paramétrer la pile afin de la restaurer à la fin. Ce serait des instructions inutiles qui ralentisseraient le programme. N'ayant rien empilé, nous faisons un simple retour RET sans toucher à la pile (excepté pour les besoins de RET).

Il est sympa Delphi quand l'optimisation est activée {$O+}. Mais regardez les "beautés" liées à {$O-}. Ca parle tout seul...

VIII) La convention STDCALL

C'est la convention PASCAL mais prise cette fois de la droite vers la gauche. Considérons 4 paramètres:
function Empilage(Param1,Param2,Param3,Param4:integer):integer; stdcall;

Nous visualisons dans la fenêtre CPU :

PUSH $4
PUSH $3
PUSH $2
PUSH $1
CALL Empilage

== > La convention STDCALL utilise la pile, empile de la droite vers la gauche, et nettoie la pile automatiquement.

En regardant plus haut, nous retrouvons bien RET $10, car nous avons empilé sur la pile 4 paramètres de 4 octets (16 octets).

Windows fonctionne avec STDCALL. Est-ce pratique ? Sûrement mais c'est surtout plus mnémotechnique que PASCAL !

Une fois empilés, la fonction Empilage va chercher les paramètres en faisant [ESP+Offset * . Delphi fait [EBP+Offset] puisque ESP est copié dans EBP. Avec STDCALL, l'intérêt est que le premier paramètre est accessible avec l'Offset le plus petit, le dernier étant situé à l'Offset le plus grand. L' offset est un décalage permettant d'accéder au n-nième élément à partir du haut de la pile donné par ESP.

IX) Le problème lié à PASCAL

STDCALL est la norme utilisée dans Windows pour toutes les applications et DLL. Que se soit Kernel, User32, GDI, ou je ne sais quoi, c'est toujours STDCALL. D'ailleurs, en regardant Windows.pas, vous ne trouverez aucune déclaration utilisant REGISTER ou PASCAL.

Pourquoi Delphi fait contre-nature ? Pour ce qui est de PASCAL, c'est de la compatibilité avec les débuts du Pascal. En revanche, pour REGISTER, la raison est sensiblement un gain de performance niveau vitesse. Empiler pour dépiler dans les registres ne sert à rien. Autant mettre directement dans les registres qui offrent (en plus !) une plus grande vitesse d'accès pour le micro-processeur.

Le problème lié à STDCALL et PASCAL est celui des appels. Si vous omettez de spécifier la convention STDCALL en déclarant une procédure externe, Delphi va le prendre comme un PASCAL ou un REGISTER. Et du coup, la DLL ne va pas être contente. Elle est programmée pour fonctionner en STDCALL, pas pour autre chose. Si vous inversez l'ordre des paramètres, vous vous placez dans un objectif de conflit, qui se traduit en temps normal par un EAccessViolation à l'adresse xxxx:xxxx.

Comme une grande partie des programmeurs font du C++, la norme "naturelle" est STDCALL. Si vous souhaitez développer des DLL et les rendre réutilisables pour d'autres personnes et d'autres langages de programmation, utilisez STDCALL. La raison est celle-ci :
      - Eh man, j'ai reçu ta DLL mais j'ai pas la liste des paramètres à fournir.
      - Ah ouais pardon... Le premier c'est un DWord de l'adresse source, le second c'est pour la destination
      - OK, merci
Comme le gars fonctionne toujours en STDCALL, il empile la destination puis la source. Et là, c'est le drame ! Il vient de copier la destination dans la source, car Pascal empile dans l'autre sens. Ce qui est normalement Source est pointé sur la Destination, et inversement.
      - Dis... tu m'aurais pas codé de la m***e par hasard ?
Ca le fiche très mal...

Peut-être certains se demandent comment une DLL peut récupérer la pile et les registres, alors que c'est un fichier bien différent de notre application et qui peut fonctionner avec une multitude d'applications. Comment peut-elle être compatible ? Comme il a été dit bien plus haut, la DLL n'est pas en soi exécutable mais contient du CODE exécutable. Par quel bout la prendre ? Et puis, elle ne contient que des fragments de code et non un CODE pour une application complète. Le CODE de la DLL est chargé comme si c'était quelque chose en plus de notre application. Il suffit ensuite de modifier EIP avec CALL pour lancer l'exécution du code. Une sorte d'assimilation.

X) Les conventions SAFECALL et CDECL

Leur utilisation n'est pas fréquente. SAFECALL a les mêmes caractéristiques que STDCALL et sert dans les interfaçage avec IDispatch.

CDECL est comme STDCALL sauf qu'après avoir fait votre appel avec CALL, vous devez vous-même restaurer la pile. Cela veut dire que la fonction fait un RET 0 et qu'il faut rajouter à ESP ce qu'on n'a pas mis à côté du RET :

PUSH $4
PUSH $3
PUSH $2
PUSH $1
CALL Empilage
ADD ESP, $10

Delphi le fait normalement pour nous. Toujours vérifier avec la fenêtre CPU en cas de doute.

XI) Valeur de retour pour les fonctions

Toutes les fonctions renvoient le résultat dans EAX. Ce peut être le résultat numérique entier d'un calcul ou l'adresse d'une variable dont notre programme devra aller chercher la valeur.

Reprenons notre TP. On pose un point d'arrêt toujours sur la même ligne :

program Convention;
{$APPTYPE Console}
  function Empilage:integer;
  begin
    //Aucun paramètre et on se fiche de la convention
    Result: =50;
  end;
var MonResultat : integer;
begin
  MonResultat:=Empilage;
  WriteLn(MonResultat);
====

On visualise : MOV EBX, EAX

Le résultat de Empilage est extrait de EAX et est stocké dans EBX. Pourquoi n'est-il pas placé dans une variable ? En fait, c'est à cause de l'optimisation. L'astuce pour comprendre est de la désactiver en rajoutant {$O-} en début de fichier. Et là, on voit : MOV [MonResultat * , EAX

La différence de code entre {$O-} et {$O+} montre que l'optimisation est parfois douteuse, mais reste cependant efficace. Un code équivalent en pur ASM parfaitement optimisé est toujours plus rapide, car il contient moins d'instructions compte tenu du fait que le programmeur sait ce qu'il veut, quand où et comment. Parfois, l'humain prend des raccourcis... donc ça va plus vite !

Sa vision est plus claire que le compilateur. Mais l'intérêt vitesse est inversement proportionnel au temps requis pour développer le programme.

XII) Débordement de pile

EStackOverflow est connu... la fameuse imbrication sans fin. Une fois de plus, tout s'explique.

Quand vous faîtes de la récursivité, une procédure ou une fonction s'appelle elle-même. Lorsque ceci se produit, peut-être restait-il du code après l'appel ? A chaque fois que l'auto-appel se fait, il faut mémoriser le travail. Le seul moyen est d'empiler.

Or si la profondeur d'imbrication est trop grande, la pile arrive à saturation. C'est l'exception ! Il faut toujours qu'il y ait une condition de sortie.

XIII) Conclusion

Une fois de plus, pas grand chose d'intéressant à dire en guise de conclusion. Même si vous n'avez pas tout compris en détail, j'espère vous avoir donné une bonne idée de ce que sont les conventions d'appel.

En développant uniquement votre application dans Delphi, aucune difficulté ne se pose pour vous à part bien choisir quelle convention utiliser pour les appels externes. Si vous codez en dûr les appels, ces informations devraient vous être utiles.

Tanguy Altert, http://altert.family.free.fr

Adresse d'origine

Ce document intitulé « [delphi/asm] les conventions d'appel » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous