Intégrer de l'assembleur PPC dans un programme C ...
Un article de GuruMed.
Sommaire |
Organisation d'un programme en assembleur, étude de cas
Par Corto
Recommandation : Cet article suppose que vous ayez des bases en assembleur, notamment PPC :) Pour ma part, je ne connais pas la signification de tous les mnémoniques mais leur nom répond à une certaine logique. L'important ici est d'avoir des notions sur la syntaxe assembleur et sur l'organisation d'un source. Il a d'autre part été uniquement réalisé et testé sur MorphOS.
L'utilisation de l'assembleur peut paraître superflu aujourd'hui, car les applications sur le Pegasos sont toutes quasiment instantanées, mais la curiosité est notre moteur :) Et puis il paraît que les compilateurs C (dont GCC) ne sont pas forcément très optimisés pour le PowerPC. Donc autant savoir accélérer les routines qui en ont besoin.
Intrigué par l'assembleur PPC, j'ai tenté d'utiliser l'outil gratuit PASM de Frank Wille, mais la syntaxe est propre à chaque assembleur et je n'ai pas trouvé d'exemples pour celui-ci. Le plus utilisé reste sans doute PowerASM de Haage & Partner. Il n'y avait plus qu'à se procurer des exemples. Et qui est mieux placé que VBCC pour générer du code PASM ? :)
Génération assembleur
J'ai donc essayé de comprendre avec un petit exemple en C m1.c transformé par la suite en assembleur :
#include
int main(int argc, char **argv)
{
printf("Hello PPC world\n");
return 0;
}
Chaque compilateur C laisse la possibilité d'obtenir le code assembleur généré pour être ensuite assemblé, par GAS pour GCC ou par PASM pour VBCC. La compilation d'un programme C est en effet réalisée en 2 opérations principales.
Voici ce que donne le code généré par GCC (avec la commande "gcc -S m1.c") :
.file "m1.c" gcc2_compiled.: .section ".rodata" .align 2 .LC0: .string "Hello PPC world\n" .section ".text" .align 2 .globl main .type main,@function main: stwu 1,-32(1) mflr 0 stw 31,28(1) stw 0,36(1) mr 31,1 stw 3,8(31) stw 4,12(31) lis 9,.LC0@ha la 3,.LC0@l(9) crxor 6,6,6 bl printf li 3,0 b .L6 .L6: lwz 11,0(1) lwz 0,4(11) mtlr 0 lwz 31,-4(11) mr 1,11 blr .Lfe1: .size main,.Lfe1-main .ident "GCC: (GNU) 2.95.3 20020615 (experimental/emm)"
Ce code peut être assemblé par GAS, mais on peut garder GCC comme frontend : "gcc -o m1gcc m1.s". On obtient alors l'exécutable qu'on aurait obtenu directement avec "gcc -o m1gcc m1.c".
Du côté de VBCC (taper la commande "vc -S m1.c"), on obtient m1.asm :
.file "m1.c" #vsc elf .text .sdreg 13 .global main .align 4 main: mflr 11 stw 11,4(1) stwu 1,-16(1) stw 3,8(1) stw 4,12(1) lis 3,.l2@ha addi 3,3,.l2@l bl __v0printf mr 4,3 li 3,0 .l1: addi 1,1,16 lwz 11,4(1) mtlr 11 blr .type main,@function .size main,$-main # stacksize=16+?? .align 2 .section .rodata .align 2 .type .l2,@object .size .l2,17 .l2: .byte 72 .byte 101 .byte 108 .byte 108 .byte 111 .byte 32 .byte 80 .byte 80 .byte 67 .byte 32 .byte 119 .byte 111 .byte 114 .byte 108 .byte 100 .byte 10 .byte 0 .globl __v0printf
Là encore on peut obtenir un exécutable avec la commande "vc -o m1vc m1.o". L'utilisation de PASM directement est suffisante pour créer le fichier objet mais pas l'exécutable, il manque le code de la fonction vprintf. On obtiendrait un avertissement via une boîte de dialogue lors de l'exécution.
Analyse première
Quelque soit le source généré (par GCC ou VBCC), ça fleure bon l'assembleur PPC mais la syntaxe n'est pas du tout la même. Peut-on quand même obtenir des similitudes ? Des informations ? C'est ce que nous allons voir.
Et voilà ce qu'on constate :
- On reconnaît des déclarations semblables et un label "main" avec le code de la fonction. On distingue une section text pour du code et data pour les données.
Pour le code PASM, d'après la doc, le .text est bien l'équivalent d'un ".section text" avec des options particulières. - Pour la fonction main, des infos lui sont attribuées : globale, type function, calcul de la taille.
- On trouve des alignements différents, de 2 ou 4, et j'ignore encore pourquoi.
- Un commentaire s'effectue après un dièse (#), un essai sous gcc montre que c'est pareil. Ne pas utiliser le point virgule ! Je m'aperçois que la doc de PASM m'apparaît déjà sous un autre angle et qu'il est bon de la relire :)
- La chaîne de caractères est contenue dans la partie data du programme. Pour PASM, d'après la doc (encore elle :), on peut remplacer à la manière de gcc par : .string "Hello PPC world\n"
- L'appel à printf (et sans doute à toute fonction) est un peu obscur avec ses lis et addi.
Bien, on commence à ne plus trouver ces sources si différents. Comme l'origine de l'article était la recherche d'infos pour programmer avec pasm, on laisse tomber gcc, mais continuez de faire des essais à titre personnel.
Où celà nous main ?
On va modifier légèrement m1.c pour que le printf reçoive en deuxième argument la variable argc : printf("Hello PPC world : %d\n", argc); Le nouveau programme m2.c donne du code très peu différent ; voyons ça :
# main de m1 main: mflr 11 stw 11,4(1) stwu 1,-16(1) stw 3,8(1) stw 4,12(1) lis 3,.l2@ha addi 3,3,.l2@l bl __v0printf mr 4,3 li 3,0 # main de m2 main: mflr 11 stw 11,4(1) stwu 1,-32(1) # changement relatif à la taille de la pile stw 14,16(1) mr 14,3 stw 4,12(1) mr 4,14 lis 3,.l2@ha addi 3,3,.l2@l bl printf mr 5,3 li 3,0
On remarque deux choses : comme le printf se compléxifie (support d'un paramètre), vbcc utilise la vraie commande printf au lieu de l'allégée "maison" vprintf. La stacksize est passée à 32 et on devine que des sauvegardes de registres (14 et 4) se font dans la pile. Le paramètre argc serait dans r4 au moment de l'appel à printf. Et à la fin du main, on devine aisément un retour de 0 via r3.
Il est important d'effectuer de si petites modifications pour vraiment isoler les différences dans le code assembleur. Alors continuons ...
Par où on double ?
Et bien par une fonction destinée uniquement à remplir cette tâche. On modifie donc m2.c (ce qui nous donnera m3.c, logique :) en lui ajoutant une fonction qui retourne le double d'un entier passé en paramètre. On n'utilise pas la nouvelle commande, juste pour constater la différence de code généré. Comme on a dit, il faut aller doucement.
#include
int doubleint(int a)
{
return (a*2);
}
int main(int argc, char **argv)
{
printf("Hello PPC world : %d\n", argc);
return 0;
}
Dans m3.asm, voilà le code inséré après .text pour le code de la nouvelle fonction :
.sdreg 13 .global doubleint .align 4 doubleint: stwu 1,-16(1) slwi 10,3,1 mr 3,10 .l1: addi 1,1,16 blr .type doubleint,@function .size doubleint,$-doubleint # stacksize=16 #vsc elf
Encore une fois, que pouvons-nous tirer de ce code nouveau ?
On constate qu'en fin de chaque fonction un label réalise quelques opérations avant le retour blr. On essaiera d'en savoir plus par la suite, mais ça a rapport avec la pile.
On retient autrement :
- que le passage de paramètre passe par r3 ... et que le retour de valeur se fait aussi par r3. Il faudra qu'on ajoute des paramètres pour voir s'ils sont passés logiquement par r4, r5, etc.
- que VBCC utilise un simple décalage pour la multiplication par 2, alors que gcc génère :
stw 3,8(31) lwz 0,8(31) # lecture dans r0 du paramètre mr 9,0 # r9 = r0 add 0,9,0 # r0 = r0 + r9
Maintenant, appelons notre fonction dans le paramètre de printf qui devient dans un nouveau fichier m4.c :
printf("Hello PPC world : %d\n", doubleint(argc));
Le code de la fonction main devient :
main: mflr 11 stw 11,4(1) stwu 1,-32(1) stw 14,16(1) mr 14,3 stw 4,12(1) mr 3,14 bl doubleint mr 4,3 lis 3,.l3@ha addi 3,3,.l3@l bl printf mr 5,3 li 3,0 .l2: lwz 14,16(1) addi 1,1,32 lwz 11,4(1) mtlr 11 blr
On a bien la valeur de argc dans r3, puis l'appel à doubleint(), puis copie de r3 dans r4 qui est le deuxième argument à l'appel de printf.
Parallèlement, un petit test m'a conduit à réaliser ma première optimisation (que d'émotions !), en remplaçant : "slwi 10,3,1" et "mr 3,10" par "slwi 3,3,1".
Ce n'est pas grand chose, je l'avoue :) Et l'optimiseur réalise sans doute ça avec les options adéquates.
On y est !
Précédemment, on avait isolé le code de la fonction doubleint. En le copiant dans un fichier doubleint.asm et en l'assemblant indépendament (pasm doubleint.asm), on obtient un premier objet. Le deuxième est obtenu en conservant uniquement la fonction main de m4.c dans un fichier m5.c que l'on compile avec "vc -c m5.c". Les deux objets sont liées avec "vc -o m5vbcc m5.o doubleint.o". Et ça marche !
Le but est atteint : on sait désormais comment compiler des fonctions indépendantes en assembleur pour qu'elles puissent être utilisées dans un projet VBCC ... ou GCC puisque ça fonctionne aussi !
