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 : Intégrer de l'assembleur PowerPC dans un programme C
(Article écrit par Mathias Parnaudeau et extrait de GuruMed.net - novembre 2004)
|
|
Organisation d'un programme en assembleur, étude de cas
Recommandation : cet article suppose que vous ayez des bases en assembleur, notamment PowerPC.
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 superflue aujourd'hui, car les applications sur les
machines MorphOS comme 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 PowerPC, j'ai tenté d'utiliser l'outil gratuit
pasm de Frank Wille (maintenant nommé
vasm, 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 deux
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 interface de travail : "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
Quel que soit le source généré (par GCC ou vbcc), ça fleure bon l'assembleur PowerPC 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 documentation, le ".text" est bien l'équivalent d'un ".section text" avec
des options particulières.
- Pour la fonction "main", des informations lui sont attribuées : globale, type function, calcul de la taille.
- On trouve des alignements différents, de deux ou quatre, 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 documentation 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 documentation
(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'informations pour programmer avec pasm, on laisse tomber GCC, mais continuez de faire des essais à
titre personnel.
Où cela 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 complexifie (gestion d'un paramètre), vbcc utilise la
vraie commande "printf" au lieu de l'allégée maison "vprintf". La taille de la pile 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 ?
Eh 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épendamment (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és 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 !
|