none
Adresse d'une propriété membre d'une classe RRS feed

  • Question

  • Bonjour,

    J'ai une question qui peut paraître basique, je cherche à accéder à une propriété membre d'une classe depuis du code assembleur.

    Je me demande par exemple, si la propriété est déclarée en tout premier dans la déclaration de la classe, cette propriété a la même adresse que l'instance de l'objet décrit par cette classe. Mais si la propriété est déclarée, par exemple, après le constructeur ? A-t-elle comme adresse: adresse de l'instance de l'objet + pointeur vers le constructeur ?

    Pour faire plus simple, dans le code d'exemple ci-dessous (32 bits), si la propriété b_init est déclarée après le constructeur, quelle sera son adresse ?

    Code d'exemple:

    Fichier asmtest.cpp

    // asmtest.cpp : Defines the entry point for the console application.
    //
    
    #include "stdafx.h"
    
    
    class A
    {
    	public:
    		bool b_init;
    		void *ptr;
    
    		A();
    
    		int plus10(int);
    };
    
    
    int _tmain(int argc, _TCHAR* argv[])
    {
    	A *a1 = new A();
    
    	if(a1->b_init)
    		printf("a1->b_init = true, a1->ptr = 0x%8p, a1->plus10(0) = %i\n", a1->ptr, a1->plus10(0));
    	else
    	{
    		printf("a1->b_init = false;");
    		return 0;
    	}
    
    	A *a2 = new A();
    
    	if(a2->b_init)
    		printf("a2->b_init = true, a2->ptr = 0x%8p, a2->plus10(10) = %i\n", a2->ptr, a2->plus10(10));
    	else
    	{
    		printf("a2->b_init = false;");
    		return 0;
    	}
    
    	printf("Appuyez sur une touche pour quitter...");
    	do
    	{
    	} while(!_kbhit());
    
    	fflush(stdin);
    
    	delete a1;
    	delete a2;
    
    	return 0;
    }

    Fichier test.asm

    .386
    .model flat
    option casemap:none
    
    	;A::A()
    	public	??0A@@QAE@XZ
    
    	;int A::test_a(int)
    	public	?plus10@A@@QAEHH@Z
    
    .data
    	p_this	dd 00000000h
    
    .code
    	;A::A()
    	??0A@@QAE@XZ:
    	push ebp		; prologue du constructeur (convention thiscall)
    	mov ebp, esp 
    	; pas besoin d'espace sur la pile pour les variables locales
    	;sub esp, 0
    	push eax
    
    	mov [p_this], ecx
    
    	mov eax, 1
    	mov [ecx], eax
    	mov eax, [p_this]
    	mov [ecx + 4], eax
    
    	pop eax
    	mov esp, ebp		; epilogue du constructeur
    	pop ebp
    	ret
    
    	;int A::plus10(int)
    	?plus10@A@@QAEHH@Z:
    	push ebp		; prologue de la methode (convention thiscall)
    	mov ebp, esp
    	; pas besoin d'espace sur la pile pour les variables locales
    	;sub esp, 0
    
    	mov eax, [ebp + 8]
    	add eax, 10
    
    	mov     esp, ebp	; epilogue de la methode
    	pop     ebp
    	ret 4 		; la methode ne prend qu'un argument de type int (4 octets)
    END

    Créer un nouveau projet Win32 (avec l'entête précompilé et inclure conio.h dans le stdafx.h), ajouter les 2 fichiers ci dessus, cocher la règle de génération personnalisée masm, puis, depuis l'explorateur de solutions, clic droit sur test.asm, pour toutes les configurations:

    - Général: Type d'élément: Microsoft Macro Assembler

    -Microsoft Macro Assembler: Avancé: Utiliser les gestionnaires d'exceptions sécurisés (je crois avoir bien traduit, sur mon VS2010 Pro SP1 - en anglais -: 'Use Safe Exception Handlers')

    Le code assembleur ne marchera pas si le projet est compilé en 64 bits (la convention thiscall n'est pas utilisée en x64, c'est l'ABI, mais comme ce n'est qu'un code de test...).

    Merci d'avance pour votre aide. Je suis plutôt curieux...

    [EDIT]

    J'oubliais une question secondaire: p_this (dans test.asm) est partagée par toutes les instances d'objets, celle-ci est donc écrasée à chaque instanciation d'un nouvel objet (car elle est initialisée dans le constructeur), ce qui pourrait causer des effets de bord si elle était utilisée dans le reste du code asm. L'allocation dynamique (malloc ou calloc) dans le constructeur accompagnée de la libération (avec free) dans le destructeur est-elle le seul moyen d'éviter ce problème ?

    [/EDIT]

    PS: Désolé pour la coloration erronée du code asm...


    L'erreur est humaine, mais pour un vrai désastre il faut un ordinateur




    mardi 8 octobre 2013 13:38

Réponses

Toutes les réponses

  • Bonjour r62dany

    J'ai trouve ces articles :

    http://www.codeproject.com/Articles/11694/How-to-invoke-C-member-operations-from-inline-asse

    http://msdn.microsoft.com/en-us/library/fabdxz08.aspx

    N'est pas sûr que ça vas fonctionner aussi avec assembler qui n'est pas inline, mais il faut essayer. Ou, peut-être faire le fichier ASM comme inline.

    Cordialement,


    Aurel BERA, MSFT
    MSDN Community Support. LE CONTENU EST FOURNI "TEL QUEL" SANS GARANTIE D'AUCUNE SORTE, EXPLICITE OU IMPLICITE.
    S'il vous plaît n'oubliez pas de "Marquer comme réponse" les réponses qui ont résolu votre problème. C'est une voie commune pour reconnaître ceux qui vous ont aidé, et rend plus facile pour les autres visiteurs de trouver plus tard la résolution.

    • Marqué comme réponse r62dany mercredi 9 octobre 2013 09:57
    mercredi 9 octobre 2013 08:39
  • Bonjour,

    Merci de votre aide.

    A priori, d'après le lien vers codeproject.com , en convention thiscall, ecx contien un pointeur vers this, ou plus exactement, directement un pointeur vers la vtable. Je teste ceci cet après-midi, et je vous tiens informé.

    Le second lien ne fonctionnera pas, il ne concerne que l'assembleur inline, celui-ci est compilé en même temps que le code c++ et le compilateur connait les variables du code c++, permettant d'assembler le code assembleur inline. Ce n'est pas le cas lorsqu'on écrit son code asm dans un fichier .asm, assemblé par masm, masm ne connait pas les variables du code C/C++, et ne pourrait donc pas assembler, à moins de les décrire également dans le .asm, si possible. Pour mon code d'exemple, masm génère un fichier objet, le compilateur aussi, mais ce sont deux fichiers bien distincts. Ensuite, le linker lie ces deux fichiers objets entre eux et aux bibliothèques standards pour générer le fichier exécutable.

    En tout cas, je pense avoir la réponse à ma première question avec votre premier lien.

    Quant à ma question secondaire concernant p_this, je pense que seule l'allocation dynamique me permettrait d'éviter les effets de bord présents le code d'exemple.

    Encore merci pour votre aide.


    L'erreur est humaine, mais pour un vrai désastre il faut un ordinateur

    mercredi 9 octobre 2013 09:57
  • L'exemple sur CodePlex utilise toujours inline asm.

    Voir cet exemple.

    Cordialement,



    Aurel BERA, MSFT
    MSDN Community Support. LE CONTENU EST FOURNI "TEL QUEL" SANS GARANTIE D'AUCUNE SORTE, EXPLICITE OU IMPLICITE.
    S'il vous plaît n'oubliez pas de "Marquer comme réponse" les réponses qui ont résolu votre problème. C'est une voie commune pour reconnaître ceux qui vous ont aidé, et rend plus facile pour les autres visiteurs de trouver plus tard la résolution.


    • Modifié Aurel Bera mercredi 9 octobre 2013 10:32
    mercredi 9 octobre 2013 10:32
  • J'ai dû mal m'exprimer, accéder à une fonction C depuis du code masm ne pose pas de soucis particulier (je le faisait déjà avec tasm et turbo C il y a quelques temps).

    Ce qui me posait problème était d'accéder à une propriété d'une classe C++ depuis du code masm, résolu grâce à votre premier lien.

    Pour cette version du compilateur, en convention thiscall, ecx contient un pointeur vers this. Les pointeurs vers les propriétés se suivent dans l'ordre de leur déclaration.

    Puisqu'un pointeur fait 4 octets en 32 bits, on a:

    - si b_init est déclarée en premier (comme dans le fichier cpp de mon premier post), b_init est accessible à [ecx + 0]

    -si b_init est déclarée après ptr, c'est ptr qui est accessible à [ecx + 0], et b_init est accessible à [ecx + 4], et ce, même si b_init est déclarée à la fin de la classe, puisque la classe A ne comporte que 2 propriétés.

    En assembleur inline (donc assemblé par le compilateur), on peut accéder aux propriétés membres de la classe A directement. Par exemple, mov eax, b_init placerait dans le registre eax la valeur de la propriété b_init.

    Ici, le code assembleur n'est pas inline, il est assemblé par masm dans un fichier objet distinct. masm ne sait par conséquent pas ce qu'est b_init et puisque b_init n'est pas une méthode, le compilateur ne génère pas de symbole dans le fichier objet pour celle-ci.

    Quant à la question de p_this (pointeur vers this, inutile fonctionellement, c'était pour l'exemple), pourquoi faire compliqué: autant en faire une propriété de la classe A (nommée ptr dans le code ci-dessous), plus de problème avec çà.

    J'ai un peu modifié le code d'exemple. Après tout, une zone mémoire partagée par toutes les instances peut être intéressante. J'en ai fait un compteur d'instances, accessible depuis toutes les instances d'objets de classe A.

    asmtest.cpp

    // asmtest.cpp : Defines the entry point for the console application.
    //
    
    #include "stdafx.h"
    
    class A
    {
    	public:
    		void *ptr;
    		bool b_init;
    		int *instances;
    
    		A();
    		~A();
    		int plus10(int);
    };
    
    int _tmain(int argc, _TCHAR* argv[])
    {
    	A *a1 = new A();
    
    	if(a1->b_init)
    	{
    		printf("Nombre d'objets de classe A: %i\n", *a1->instances);
    		printf("a1->b_init = true, a1->ptr = 0x%8p, a1->plus10(0) = %i\n", a1->ptr, a1->plus10(0));
    	}
    	else
    	{
    		printf("a1->b_init = false;");
    		return 0;
    	}
    
    
    	A *a2 = new A();
    
    	if(a2->b_init)
    	{
    		printf("Nombre d'objets de classe A: %i\n", *a2->instances);
    		printf("a2->b_init = true, a2->ptr = 0x%8p, a2->plus10(10) = %i\n", a2->ptr, a2->plus10(10));
    	}
    	else
    	{
    		printf("a2->b_init = false;");
    		return 0;
    	}
    
    	delete a1;
    	printf("Nombre d'objets de classe A: %i\n", *a2->instances);
    	delete a2;
    
    	printf("Appuyez sur une touche pour quitter...");
    	do
    	{
    	} while(!_kbhit());
    
    	fflush(stdin);
    
    	return 0;
    }
    
    

    test.asm

    .386
    .model flat
    option casemap:none
    
    	;A::A()
    	public	??0A@@QAE@XZ
    
    	;A::~A()
    	public	??1A@@QAE@XZ
    
    	;int A::plus10(int)
    	public	?plus10@A@@QAEHH@Z
    
    .data
    		instances	dd			00000000h
    
    .code
    	;A::A()
    	??0A@@QAE@XZ:
    		push ebp						; prologue du constructeur (convention thiscall)
    		mov ebp, esp 
    		; pas besoin d'espace sur la pile pour les variables locales
    		;sub esp, 0
    		push eax
    
    		mov [ecx], ecx ; ptr = ecx
    
    		mov eax, 1
    		mov [ecx + 4], eax ; b_init = 1
    
    		inc [instances] ; *instances += 1
    		mov eax, offset instances ; eax = &instances
    		mov [ecx + 8], eax	; inst = eax
    
    		pop eax
    		mov esp, ebp ; epilogue du constructeur
    		pop ebp
    		ret
    
    	;A::~A()
    	??1A@@QAE@XZ:
    		push ebp ; prologue du destructeur (convention thiscall)
    		mov ebp, esp
    		; pas besoin d'espace sur la pile pour les variables locales
    		;sub esp, 0
    
    		dec [instances] ; *instances -= 1
    
    		mov esp, ebp ; epilogue du destructeur
    		pop ebp
    		ret
    
    	;int A::plus10(int)
    	?plus10@A@@QAEHH@Z:
    		push ebp ; prologue de la methode (convention thiscall)
    		mov ebp, esp
    		; pas besoin d'espace sur la pile pour les variables locales
    		;sub esp, 0
    
    		mov eax, [ebp + 8]
    		add eax, 10
    
    		mov esp, ebp ; epilogue de la methode
    		pop ebp
    		ret 4 ; la methode ne prend qu'un argument de type int (4 octets)
    END

    Bien sûr, ce code n'est pas portable. En l'état, si l'on tentait de le compiler tel quel avec un autre compilateur (par ex GCC) et un autre assembleur (par ex, pour un processeur ARM). Déjà, le standard C++ ne définit pas comment les compilateurs doivent décorer les noms, GCC et MSVC les décorent donc de manières différentes. Les conventions peuvent être différentes, et l'assembleur ARM est différent de l'assembleur Intel x86.

    Ici, c'était juste par curiosité.

    Pour moi, le sujet est résolu, encore merci, Aurel, pour votre aide, et à bientôt.


    L'erreur est humaine, mais pour un vrai désastre il faut un ordinateur

    mercredi 9 octobre 2013 15:08
  • Heu, c'est très très approximatif comme mécanisme.

    Le layout d'une classe est fonction d'une multitude de paramètre, même pour la même configuration d'un même compilateur. (présence ou pas d'héritage multiple, d'héritage multiple, de keyword non portable ...)


    Paul Bacelar, Ex - MVP VC++

    lundi 25 novembre 2013 14:11
    Modérateur
  • Bonjour,

    Bien sûr, c'est une approche plutôt approximative, je pense que c'est aussi peu portable.

    Ce mécanisme nécessite de compiler avec le même compilateur que celui qui a généré les symboles utilisés dans le code asm, et de ne rien changer dans la classe une fois celle-ci terminée, à moins de tout revérifier.

    En revanche, çà peut être utile si l'on a besoin de maîtriser complètement certaines parties de l'implémentation de la classe et quand ce besoin est prioritaire par rapport à la portabilité et à la durée de développement. Si on a besoin d'appeler une méthode ou une fonction depuis de l'asm, on peut aussi utiliser la macro prédéfinie __FUNCDNAME__ propre aux compilateurs Microsoft.

    Sinon, partir sur une base en langage C: créer une structure contenant les propriétés voulues ainsi que les pointeurs vers les fonctions convenablement castés, et réaliser l'implémentation des fonctions voulues en asm. Ceci nécessiterait d'écrire le code des constructeurs et des destructeurs, de les appeler manuellement, pas question de patrons, de surcharge, ou d'héritage, qui n'existent pas en C.

    Dans ce cas, la convention d'appel étant définie, on peut anticiper sur les noms décorés et s'en servir dans le code asm, mais serait-ce réellement "propre" ?

    J'ai trouvé cet article sur Wikipédia, lequel contient un lien externe vers ce pdf d'Agner Fog sur les conventions d'appel, celui-ci devient plutôt intéressant à partir de la page 24 (Chapitre 8, "Name mangling"). Bien sûr, rien ne garanti que Microsoft continuera à utiliser le schéma de décoration des noms qui y est décrit, si celui-ci est 100% correct.

    Pour résumer, à un compilateur donné, à une situation donnée, pour pouvoir savoir où est située une propriété membre d'une classe C++ en mémoire, il faut pouvoir connaître le nom décoré de cette propriété (en générant par ex volontairement un erreur de linker la concernant) pour pouvoir s'en servir en asm.

    Mais ceci n'est pas portable, et peut être plutôt aléatoire (surtout si on utilise en plus du code auto-modifiable ...), en prenant un maximum de précautions, hors projets professionnels, ç'est plutôt intéressant à explorer. Qu'en dites-vous ?


    L'erreur est humaine, mais pour un vrai désastre il faut un ordinateur

    mardi 26 novembre 2013 14:18