Suivez-nous sur X
|
|
|
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
|
|
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
|
|
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
|
|
A propos d'Obligement
|
|
David Brunet
|
|
|
|
Programmation : C - interfacer le C et l'assembleur (1re partie)
(Article écrit par Cédric Beust et extrait d'A-News (Amiga News) - janvier 1990)
|
|
Loin de moi l'idée de raviver l'éternelle querelle qui oppose les inconditionnels de l'un et l'autre.
Au contraire, j'espère montrer dans les lignes qui suivent qu'il est non seulement possible de les
interfacer simplement sur l'Amiga, mais que cela peut apporter un gain en efficacité non négligeable.
Les protagonistes
Quelques précisions sur les produits dont je parle tout d'abord. J'utilise le Lattice 5.02 et Devpac 2.
Ceci dit, n'importe quel compilateur C devrait faire l'affaire car l'appel des fonctions dans ce
langage se fait de manière standard. Si votre compilateur ne les respecte pas, c'est que c'est un
mauvais compilateur et il est urgent d'en changer ! En ce qui concerne l'assembleur, vous pouvez
également utiliser le MetaComCo ou n'importe quel autre qui dispose des directives XREF et XDEF.
Une option pour nommer le module courant ne serait pas non plus inutile, mais j'en reparlerai plus tard.
L'intérêt
Quel peut être l'intérêt d'utiliser simultanément les deux langages ? Il y pour cela plusieurs raisons.
Chacun a des avantages et des inconvénients.
Pour le C
- Avantages : rapidité de mise au point, entrées/sorties faciles avec printf, putc, etc.
- Inconvénients : oblige parfois à des initialisations lourdes pour respecter le multitâche.
Pour l'assembleur
- Avantages : rapidité d'exécution, accès facile aux registres système.
- Inconvénients : relecture difficile, bogues nombreux à l'écriture, mise au point fastidieuse.
Ceci est très loin d'être une liste exhaustive et les différences ne sont en fait pas
aussi marquées que je le laisse apparaître dans ce tableau, mais cela va me servir
à illustrer mon propos. L'intérêt est donc de ne conserver de chaque langage le côté qui
lui est agréable, en laissant de côté les inconvénients.
Fixons-nous un but à atteindre afin que tout cela ne soit pas trop théorique. Il vous
est sûrement arrivé d'avoir à écrire une petite routine qui a besoin d'afficher du
texte dans la fenêtre courante. L'écrire en C semble être une bonne idée mais la
seule utilisation de "printf" ou "putc" ajoute facilement 2 ko
au code produit. Il est assez irritant d'avoir un exécutable de 5 ko
alors que les opérations que vous faites ne devraient en théorie pas dépasser 1 ko
(exemple : la routine "avail.c" livrée avec le Lattice).
Il serait donc intéressant
de disposer d'une routine d'impression qui serait très simple (impression de chaînes
pour commencer) et qui prendrait très peu de place. Je vous propose donc de créer votre
propre bibliothèque C (un .lib) qui contiendrait votre routine d'impression sur écran,
la routine en question n'excéderait pas 150 octets... J'irai même plus loin en vous
proposant une nouvelle routine "sprintf" qui vous permettra de faire des sorties formatées
(c'est-à-dire avec %d, %s, etc.) pour une taille du même ordre. Par la même occasion,
nous apprendrons à nous passer du startup c.o. Alléchant, n'est-ce pas ?
Assez bavardé, nous avons du pain sur la planche ! Quelques notions sont indispensables pour commencer.
Généralités
Comment le C passe-t-il ses paramètres ? Il utilise pour ce faire une méthode universelle à
tous les compilateurs : le passage par la pile. Lorsque le compilateur rencontre une instruction :
...il pousse sur la pile d, c, b et enfin a, après quoi, il fait un saut à la routine "_fonction"
et dès son retour, il rétablit la pile en l'incrémentant de la taille des paramètres passés.
Le compilateur pourrait donc produire un code comme celui-ci :
Pourquoi 16 ? Parce que j'ai supposé que chaque paramètre était représenté sur quatre
octets. Ce n'est nullement indispensable (il faut malgré tout que la taille soit un nombre
pair d'octets) mais c'est beaucoup plus simple pour les calculs.
Plaçons-nous maintenant à la place de la fonction appelée. Comment peut-elle récupérer ses
paramètres ? C'est très simple, il suffit d'aller les chercher au bon endroit sur la pile.
Mais n'oubliez pas qu'une nouvelle adresse s'est empilée après l'appel : celle du retour.
Le premier paramètre ne se trouve donc pas en 0(a7) mais en 4(a7). Reprenons l'exemple
précédent : la routine "_fonction" agirait probablement de la façon suivante :
En fait, ce modèle n'est pas tout à fait exact. Il faut absolument garder intacts les registres
d2-d7 et a2-a6 (d0, d1, a0 et a1 peuvent être utilisés et détruits). En toute rigueur,
il faudait sauver les registres que nous utilisons sur la pile, et décaler donc les
accès d'autant. Dans cet exemple, nous aurions à sauvegarder d2 et d3, soit une taille de
8 octets :
Je laisse de côté l'instruction "link" qui est utilisée pour créer des variables locales,
et ajoute un nouveau décalage dans l'adressage.
Comprenez-vous pourquoi les paramètres sont poussés dans l'ordre inverse d'apparition ?
L'explication est simple : le C autorise à ses fonctions d'avoir un nombre variable
de paramètres. Songez à "printf" : le nombre de paramètres est directement lié à la
chaîne de formatage. Par exemple, une chaîne "Résultat: %d = %d" imposera clairement
un "printf" à trois paramètres. Si la chaîne était poussée la première,
la fonction appelée serait incapable de la retrouver dans la pile.
Exemple : soit l'instruction printf("%d %d",a,b). Voyons comment se présentent
les piles en utilisant un mode de passage différent :
Dans le premier exemple, comment la fonction saura-t-elle que la chaîne de formatage se
trouve en 12(a7) ? En utilisant la deuxième méthode, elle sait avec certitude
qu'elle doit regarder en premier lieu en 4(a7). Puis elle en déduira que deux
paramètres suivent et ira donc les chercher en 8(a7) et 12(a7) (elle connaît
naturellement leur taille).
Finissons cette introduction en regardant le problème inverse : comment appeler une fonction
C à partir de l'assembleur ? Voici comment procéder :
1. Définir à l'aide de XREF les fonctions C à appeler, et que l'assembleur
doit donc ignorer. C'est la tâche de l'éditeur de liens de la trouver.
2. Respecter les conventions d'appel décrites ci-dessus.
3. Ne pas oublier de lier avec la bibliothèque qui contient les fonctions utilisées.
Un petit exemple : utilisation de printf (encore lui !) :
Pour faire marcher cet exemple, utiliser "blink" de la façon suivante :
blink lib:c.o,a.o to a lib lib:lc.lib
|
Pas de problèmes, ça marche mais vous avez vu la taille ? Environ 6000 octets. Ça
fait beaucoup pour une routine d'affichage, vous ne trouvez pas ? Ceci est dû au
fait que "printf" est une fonction très complexe qui autorise des formatages
très divers. De plus, elle fait appel à des symboles définis dans c.o
(d'où l'obligation d'utiliser le nom "_main") et des routines autres que
"printf" dans lc.lib.
|