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 :

  1. 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.
  2. Pour la fonction main, des infos lui sont attribuées : globale, type function, calcul de la taille.
  3. On trouve des alignements différents, de 2 ou 4, et j'ignore encore pourquoi.
  4. 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 :)
  5. 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"
  6. 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 !