Interfacage Assembleur / C++


Un autre vieux tuto : comment accéder à vos structures C++ à partir de l'assembleur.

C'était mon tout premier tuto :) Une pièce d'histoire...

Welcome pour mon premier tuto :)

Nous allons apprendre à faire un truc trop cool, de la POO en assembleur.
Mais bon on va y aller doucement, on va commencer par interfacer asm et C.

Pour commencer, quel est l'intérêt ? Tout simplement d'avoir accès à toutes les possibilités de l'assembleur tout en gardant un contrôle total sur le code généré (puisque justement c'est nous qui le générons ^^ ).
Evidement on pourrait simplement mettre asm{ /* code */ } mais le compilo rajoute ses propres trucs, et si on a une application très spéciale ça peut être embêtant ( je pense tout particulièrement à ScalP pour ceux qui connaissent, où j'ai été obligé de faire ça ): on n'a pas de contrôle sur les registres utilisés ainsi que sur l'utilisation de la pile, à laquelle on peut vouloir accéder.
En outre ce n'est absolument pas portable, chaque compilo a sa propre syntaxe.
De toute façon pour les indécis j'ai un argument-choc : ça fait méga g33k!
convaincu ? ^^

Alors allons-y...

Interfacer assembleur et C

Appeler de l'assembleur en C

Pour ceux qui ne connaissent absolument rien à l'assembleur, un chti tuto est disponible [url="http://www.coder-studio.com/?page=tutoriaux_aff&code=asm_1"]ici[/url], merci Funto !

Disons qu'on a ce programme :

// main.cint asm_carre (int);
int main( int nagrs, char ** vargs){
 int a = 4;
 int b = asm_carre(a);
 printf("D'après notre fonction assembleur, le carré de %d vaut %d\n",a,b);
 return 0;
}

avec int asm_carre (int) une fonction que l'on va implémenter en assembleur.
On écrit le prototype dans le .c pour que le compilateur connaisse la fonction.
Bien évidemment ça ne compilera pas dans l'état, personne ne connaît où se trouve l'implémentation de asm_carre.
Mais où mettre cette implémentation ? simple, dans un .asm... gogogo !

; asm_carre.asm
[BITS 32]
segment .data ; données initialisées: rien pour l'instant.
segment .bss  ; données non initialisées, toujours rien.
segment .text ; ah ! on entre dans le CS ( Code Segment )
 global _asm_carre ; on va exporter ce symbole
_asm_carre:
 enter 0,0         ; on n'utilise aucune variable, uniquement des registes.
 push ebx          ; La valeur de ebx ne DOIT PAS être modifiée par la fonction !! alors on le met temporairement sur la pile ( même si dans ce cas précis c'est inutile )
 mov eax,[ebp+8]   ; le paramètre de la fonction ce trouve à cet endroit.
 mul eax           ; eax = a*a
 pop ebx           ; on restaure ebx
 leave             ;
 ret               ; retour à la fonction appelante

héé partez pas :lol:
Pour les quatre premières lignes, si vous ne comprenez pas refaites un petit tour sur le tuto de Funto.
bon.
pourquoi global? pour dire à NASM que cette fonction pourra être appelée d'un programme externe.
pourquoi _asm_carre et non pas asm_carre ? C'est une convention d'écriture. Chaque fois que vous voudrez créer une fonction asm devant être appelée à partir du C, il faudra préfixer le label par _

ensuite place à l'implémentation.
Attention, rappelez vous que la pile de processeur commence à DROITE et s'aggrandit vers la gauche, donc quand on pushe une variable, elle se met le plus à gauche possible. Du coup ce qui se trouve en ebp+4 a été mis sur la pile AVANT ce qui se trouve en ebp+0

enter 0,0 : ne sert pas à grand chose ici.

push ebx : ici cela n'est pas réellement utile non plus puisque ebx, on n'y touche pas; mais c'est une question de bonnes habitudes car il est interdit pour une fonction de modifier ebx ( quand on l'interface avec du C/C++ tout du moins, en asm pur vous faites ce que vous voulez )
C'est embêtant ça ! déjà que des registes on n'en a pas beaucoup... alors du coup on le pushe, et on le popera juste avant de redonner la main au C.
à vrai dire.. il n'y a pas que ebx, mais aussi esi,edi,ebp,cs,ds,ss et es ... au pire : pusha / popa.
]

----------------------------------------------------------------
| adresse ||| ebp - 4  | ebp + 0  |    ebp + 4     |  ebp + 8  |
----------------------------------------------------------------
| contenu |||   ???    |    ebp   | adresse retour | parametre |
----------------------------------------------------------------

mov eax,[ebp+8] : strico sensu, met ce qui se trouve en ebp+8 ( sur la pile ) dans eax. Mais qu'est-ce qui se trouve donc en ebx+8 ? ohhh, notre paramètre ;)

mul eax,eax : on multiplie eax par lui même ...

pop ebx: voilà, comme ça le standard est respecté et on risque pas de se retrouver avec erreurs incompréhensibles.

leave: symétrique de enter

ret : pope l'adresse de la fonction appelante et y va: ici, on était dans le main, ligne 2...

et voilà nous revoilà dans notre programme C avec le carré du paramètre dans eax. cool, ça sert à quoi? hé bien justement, la valeur de retour d'une fonction DOIT être dans eax, c'est la règle. Et du coup, tout est bon, b=a^2.

En fait on pouvait faire plus simple, c'est là tout l'avantage de l'assembleur :

_asm_carre:
 mov eax,[esp+4]
 mul eax
 ret

fini ! La rule of thumb, c'est 1 : vous ne modifiez pas les registres cités plus haut et 2 : valeur de retour dans eax.
[nota : eax c'est pour les DWORDS. Pour un WORD c'est ax, pour un QWORD, edx:eax]
remarquez que je n'ai pas utilisé ebp (base pointer) mais esp ( stack pointer ) car il n'y a pas la commande enter, dont l'un des rôle est justement de faire mov ebp,esp.

Bon , compilons ! OK c'est bon ça marche... Argheul nan, la compilation se fait sans ennuis mais erreur au linkage : l'éditeur de liens ne sait pas où trouver l'implémentation de asm_carre. Et pour cause ! il n'a pas été assemblé :niii:
Qu'à cela ne tienne :

C:\>nasm.exe -o asmlib.o -f coff asm_carre.asm

( si vous n'êtes pas sous windows utilisez -f elf )
Voilà vous avez un merveilleux .o ! Il n'y a plus qu'à demander au compilo de linker avec , et magie !! ça plante :D
[NOTAs :

  • on peut utiliser aussi, nasm -f win32 asm_carre.asm , ca donnera asm_carre.obj, à linker de même manière.
  • Pour éviter de retourner dans la console à chaque modif on peut aussi faire ça automatiquement : dans VC6, Project->Settings-> onglet pre-link step-> nouveau-> entrez cette ligne...

]

( bon en fait non ça plante pas mais c'est parce que vous avez de la chance )
La raison est simple, la convention d'appel utilisée par le compilo C++ est le _stdcall ( params empilés de gauche à droite, dépilés dans la fonction elle même ) et il y a de fortes chances pour que vous ayez écrit votre fonction assembleur en _cdecl ( params empilés de droite à gauche, dépilés par le programme appelant ). Aurement dit, gros plantage assuré s'il n'y a pas la même convention des deux côtés (heureusement ici c'est du C, pas du C++, donc pas de plantage en fait) .
pour spécifer la convention :

  • sous VC++ : _cdecl int fonction ( int ); // respectivement _stdcall
  • sous GCC : int fonction ( int ) __attribute__((cdecl)); // respectivement stdcall

Yeah ! Là ça marche vraiment, et on sait pourquoi :)

Appeler de l'asm, qui appelle du C

Bon, on ne va pas s'arrêter en si bon chemin ... maintenant qu'on sait écrire des fonctions hyper puissantes et polyvalentes en assembleur ( *hum* ), on aura sûrement besoin, à un moment ou à un autre, d'appeller une fonction qui elle est implémentée dans le prog C, ou même dans les librairies standard ... genre strcmp !

Pour ça c'est très simple, il suffit de déclarer le nom de la fonction en extern au début du segment .text et le linker s'occupera de tout.

Dans le genre "Programme qui sert à rien" ça donne ça:

// main.c
int _cdecl asm_carre (int);
int main( int nagrs, char ** vargs){
 int a = 4;
 int b = asm_carre(a);
 printf("D'après notre fonction assembleur, le carré de %d vaut %d\n",a,b);
 return 0;
}
int _cdecl carre(int a){return a*a;}

puis:

; asm_carre.asm
[BITS 32]
segment .text
 global _asm_c_carre:
 extern _carre ;remarquez qu'ici aussi on met un _
_asm_c_carre:
 mov eax,[esp+4]   ; on prend le param sur la pile
 push eax      ; on le re-pushe au sommet de la pile
 call _carre      ; on appelle la fonction
 add esp,4         ; on dépile
 ret               ; on retourne au C

Héé oui vous l'aurez compris, ce code atteint le sommum de l'inutilité en appellant la fonction asm_carre qui se contente d'appeler carre(), qui lui est implémenté en C ^^
Attention cependant, cela ne marche pas avec le format COFF, le win32 est impératif ( et souvenez vous que du coup ça donne pas en .o mais un .obj)

Interfacer assembleur et C++

- Here comes the bidouille -

Bien, toutes les bases sont posées, ce que vous ferez de cela dépendra de vos connaissances en assembleur.
Mais maintenant nous sommes en 2006, ya un truc qui est pas mal : le C++.
ça serait bien qu'on puisse interfacer le C++ et l'assembleur de la même manière qu'avec le C... non?

Malheureusement la convention qui nous dit de rajouter un petit _ , c'est une convention C ! et non C++ ...
c'est là que les embrouilles commencent, parce que les éditeurs de compilateurs s'en sont donné à coeur joie pour nous peaufiner des tonnes de conventions aussi différentes et incompréhensibles les unes que les autres...
ça s'appelle le name mangling ( décoration de nom ) et c'est pas du tout, mais alors pas du tout standardisé.
Heureusement, le linker est votre ami !
Ainsi, si vous renommez le main.c précédent en main.cpp, la compilation encore une fois se déroulera sans souci, mais le linker va vous sortir une erreur du genre :

tuto.obj : error LNK2001: unresolved external symbol "int __cdecl asm_carre(int)" (?asm_carre@@YAHH@Z)

Oh magie , un joli nom de label tout bizarroïde apparaît : ?asm_carre@@YAHH@Z
Il suffit d'un petit copier-coller de ce nom pour remplacer tous les _asm_carre par ?asm_carre@@YAHH@Z dans le listing assembleur.
On réassemble, on recompile, ça marche ! *joie*

Pour la compatibilité entre les différents compilateurs, on peut s'amuser à regarder toutes les erreurs de linkage créées par tous ces compilos, ajouter tous ces noms dans la liste des labels extern, et remplacer chaqun de ces labels par la liste des labels demandés par l'ensemble des compilos.....

Assembleur Orienté Objet ?

Oui c'est possible :)
Tout du moins à certaines conditions .....
Admettons que nous ayons une classe CMaClasse avec une fonction membre void Calcule(void).
hé bien jusque là c'est très simple : c'est exactement la même chose qu'avant. On regarde l'erreur du linker, on copie-colle le nom de la fonction.
Ca marche ... si et seulement si ladite classe est un POD ou au mieux un agrégat. Donc : pas de virtuel ! ni fonction ni membre ni quoi que ce soit de virtuel. On a le droit à l'héritage si ça reste un agrégat, mais bon ça devient plus compliqué après et à priori ça dépend du compilo utilisé.
( théoriquement, même avec du virtuel c'est possible mais n'y songez même pas ^^)

Bon , maintenant, admettons que l'on veuille prendre des paramètres. le prototype devient:

void CMaCLasse::Calcule(int a);

sauf que ...
sauf que c'est pas si simple, ce prototye c'est celui que vous écrivez, pas celui qui est utilisé.
le compilo, lui, il comprend ça:

void CMaClasse::Calcule( CMaClasse* this, int a);

c'est d'ailleurs pour ça que vous avez accès aux membres quand vous faites Instance->fonction();, ça tombe pas du ciel ...

Une fois dans la partie asm, voilà donc l'état de la pile:

----------------------------------------------------------------------------
| adresse ||| ebp - 4  | ebp + 0  |    ebp + 4     |  ebp + 8  | ebp + 12  |
----------------------------------------------------------------------------
| contenu |||   ???    |    ebp   | adresse retour |    this   | parametre |
----------------------------------------------------------------------------

'a' n'est donc plus en ebp+8 mais en ebp+12 ! à part ça rien ne change.

bon , maintenant que j'ai accès aux paramètres, je veux une valeur de retour: pareil qu'en C ! dans eax ...

[NOTA : dans la source d'exemple , j'accède à this avec mov ecx,[esp+4], mais c'est parce que je n'utilise pas enter et leave.

et dernier point fun : accéder aux membres :)
Le truc sympa c'est qu'on a this...
ce qu'il faudrait c'est donc ça : (ebp+8)->_membre, c'est à dire, en simili-nasm: [ebp+8]._membre
mais comment avoir l'adresse de _membre ? simple, pour un POD, le c++ demande que les membres soient stockés en mémoire de façon contigüe (cela est rarement respecté, voir plus bas) et dans l'ordre de la déclaration.
exemple:

class CMaClasse{
public:
 int a;
private:
 char c;
protected:
 float f;
};
CMaClasse inst;

dans ce cas, la mémoire sera :

0         4   5         9
-------------------------
| * * * * | * | * * * * |
|    a    | c |    f    |
-------------------------

( un * correspond un octet, le 4 signifie qu'on se situe à l'adresse &inst +4 )
( On voit d'ailleurs qu'il est presque toujours préférable de classer ses membres par taille décroissante, ça optimise les accès mémoire)

NOTA:
la plupart des compilos alignent automatiquement sur 4 octets (c'est plus rapide à l'exec), ce qui donne une organisation de la mémoire comme ça:

0         4        8         12
------------------------------
| * * * * | *      | * * * * |
|    a    | c      |    f    |
------------------------------

on peu configurer ça soi même l'alignement mémoire avec la directive pragma.
Pour comprimer au maximum la structure sous VC++ :

#pragma pack(push)
#pragma pack(1)
struct lkjlkj{...};
#pragma pack(pop)

et sous gcc:

struct ljhlkjh{
...
}__attribute__((packed)); // (oui c'est compliqué et alors ? :p)

/NOTA

nous disions donc que nous avions this en ebp+8 et qu'on voulait les membres... admettons que je veuille mettre f dans ebx. je dois alors faire:

mov ecx, [ebp+8] ;ecx=this;
mov eax, [ecx+5] ;eax=*(this+5); on met dans eax les 4 octets( 4 car c'est eaXX) situés à l'adresse this+5

(on peut optimiser avec lea , mais passons)
à partir de là on peut accéder au membres, une bonne idée est d'utiliser les macros de nasm pour y accéder plus aisément en donnant des noms compréhensibles a [ecx+5] ...)

voili voilou !!!
Comment ça ya plus personne avec moi :p

dernière chose: Magie de l'assembleur on peut même appeller des fonctions membres...mais fait connaître le label, pour ça il suffit de faire un appel de ladite fonction quelque part dans le c++ tout en commentant l'implémentation. Le linker va râler, et par là même donner le label qu'il cherche.
Ne pas oublier de pusher this! ...
Maintenant, petit truc sympa et bien utile: quand votre routine asm plante... mettez vous en mode débug dans VC++, relancez...vous verrez s'afficher le desassembly MASM de votre code NASM, avec la ligne à laquelle ça a planté...faites clic droit sur la barre des menus, cliquez sur registres: voilà, vous contrôlez votre machine B-) :p

Bon ben voilà, je verrai ce qu'il y a à ajouter à cela, please feel free de faire des commentaires dans le forum :)
N'oubliez pas d'aller voir les exemples.

Un zip est disponible [url="http://coder-studio.com/tutoriaux/asm/asm_4.zip"]ici[/url], et la discussion se poursuit [url="http://www.coder-studio.com/forum2/viewtopic.php?id=904"]là[/url] !

  1. No comments yet.
(will not be published)

  1. No trackbacks yet.