Vtables C ++. Partie 1 (notions de base + héritage multiple)

Bonjour à tous! La traduction de l'article a été préparée spécialement pour les étudiants du cours "Développeur C ++" . Est-il intéressant de se développer dans cette direction? Connectez-vous le 13 décembre à 20h00, heure de Moscou. à la classe de maître "Pratiquez l'utilisation de Google Test Framework" !



Dans cet article, nous verrons comment clang implémente vtables (tables de méthodes virtuelles) et RTTI (identification de type d'exécution). Dans la première partie, nous commençons par les classes de base, puis examinons l'héritage multiple et virtuel.


Veuillez noter que dans cet article, nous devons creuser dans la représentation binaire générée pour diverses parties de notre code à l'aide de gdb. C'est un niveau assez bas, mais je ferai tout le travail dur pour vous. Je ne pense pas que la plupart des futurs articles décrivent les détails d'un niveau aussi bas.


Avis de non - responsabilité : tout ce qui est écrit ici dépend de la mise en œuvre, peut changer dans toute version future, vous ne devez donc pas vous y fier. Nous considérons cela uniquement à des fins éducatives.


excellent, alors commençons.


Partie 1 - vtables - Bases


Regardons le code suivant:


#include <iostream> using namespace std; class NonVirtualClass { public: void foo() {} }; class VirtualClass { public: virtual void foo() {} }; int main() { cout << "Size of NonVirtualClass: " << sizeof(NonVirtualClass) << endl; cout << "Size of VirtualClass: " << sizeof(VirtualClass) << endl; } 

 $ #    main.cpp $ clang++ main.cpp && ./a.out Size of NonVirtualClass: 1 Size of VirtualClass: 8 

NonVirtualClass a une taille de 1 octet, car en C ++, les classes ne peuvent pas avoir une taille nulle. Cependant, ce n'est pas important maintenant.


VirtualClass est de 8 octets sur une machine 64 bits. Pourquoi? Parce qu'à l'intérieur, il y a un pointeur caché pointant vers une table virtuelle. vtables sont des tables de traduction statiques créées pour chaque classe virtuelle. Cet article parle de leur contenu et de leur utilisation.


Pour mieux comprendre à quoi ressemble vtables, regardons le code suivant avec gdb pour savoir comment la mémoire est allouée:


 #include <iostream> class Parent { public: virtual void Foo() {} virtual void FooNotOverridden() {} }; class Derived : public Parent { public: void Foo() override {} }; int main() { Parent p1, p2; Derived d1, d2; std::cout << "done" << std::endl; } 

 $ #         ,  gdb $ clang++ -std=c++14 -stdlib=libc++ -g main.cpp && gdb ./a.out ... (gdb) #  gdb  -  C++ (gdb) set print asm-demangle on (gdb) set print demangle on (gdb) #     main (gdb) b main Breakpoint 1 at 0x4009ac: file main.cpp, line 15. (gdb) run Starting program: /home/shmike/cpp/a.out Breakpoint 1, main () at main.cpp:15 15 Parent p1, p2; (gdb) #     (gdb) n 16 Derived d1, d2; (gdb) #     (gdb) n 18 std::cout << "done" << std::endl; (gdb) #  p1, p2, d1, d2 -     ,    (gdb) p p1 $1 = {_vptr$Parent = 0x400bb8 <vtable for Parent+16>} (gdb) p p2 $2 = {_vptr$Parent = 0x400bb8 <vtable for Parent+16>} (gdb) p d1 $3 = {<Parent> = {_vptr$Parent = 0x400b50 <vtable for Derived+16>}, <No data fields>} (gdb) p d2 $4 = {<Parent> = {_vptr$Parent = 0x400b50 <vtable for Derived+16>}, <No data fields>} 

Voici ce que nous avons appris de ce qui précède:
- Malgré le fait que les classes n'ont pas de membres de données, il existe un pointeur caché vers vtable;
- vtable pour p1 et p2 est le même. les vtables sont des données statiques pour chaque type;
- d1 et d2 héritent du vtable-pointer de Parent, qui pointe vers vtable Derived;
- Toutes les vtables indiquent un décalage de 16 (0x10) octets dans la vtable. Nous en discuterons également plus tard.


Continuons notre session gdb pour voir le contenu de vtables. J'utiliserai la commande x, qui affiche la mémoire à l'écran. Nous allons sortir 300 octets en hexadécimal, en commençant par 0x400b40. Pourquoi exactement cette adresse? Parce que nous avons vu ci-dessus que le pointeur vtable pointe vers 0x400b50, et le symbole de cette adresse est vtable for Derived+16 (16 == 0x10) .


 (gdb) x/300xb 0x400b40 0x400b40 <vtable for Derived>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b48 <vtable for Derived+8>: 0x90 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400b50 <vtable for Derived+16>: 0x80 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b58 <vtable for Derived+24>: 0x90 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b60 <typeinfo name for Derived>: 0x37 0x44 0x65 0x72 0x69 0x76 0x65 0x64 0x400b68 <typeinfo name for Derived+8>: 0x00 0x36 0x50 0x61 0x72 0x65 0x6e 0x74 0x400b70 <typeinfo name for Parent+7>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b78 <typeinfo for Parent>: 0x90 0x20 0x60 0x00 0x00 0x00 0x00 0x00 0x400b80 <typeinfo for Parent+8>: 0x69 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400b88: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b90 <typeinfo for Derived>: 0x10 0x22 0x60 0x00 0x00 0x00 0x00 0x00 0x400b98 <typeinfo for Derived+8>: 0x60 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400ba0 <typeinfo for Derived+16>: 0x78 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400ba8 <vtable for Parent>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400bb0 <vtable for Parent+8>: 0x78 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400bb8 <vtable for Parent+16>: 0xa0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400bc0 <vtable for Parent+24>: 0x90 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 ... 

Remarque: nous examinons les personnages décornés (démêlés). Si vous êtes vraiment intéressé, _ZTV est le préfixe de vtable, _ZTS est le préfixe de la chaîne de type (nom) et _ZTI pour typeinfo.



Voici la structure vtable Parent :


L'adresseValeurTable des matières
0x400ba80x0top_offset (plus à ce sujet plus tard)
0x400bb00x400b78Pointeur vers typeinfo pour le parent (fait également partie du vidage de mémoire ci-dessus)
0x400bb80x400aa0Pointeur vers Parent :: Foo () (1) . _vptr Parent pointe ici.
0x400bc00x400a90Pointeur vers le parent :: FooNotOverridden () (2)

Voici la structure vtable Derived :


L'adresseValeurTable des matières
0x400b400x0top_offset (plus à ce sujet plus tard)
0x400b480x400b90Pointeur vers typeinfo pour Derived (fait également partie du vidage de mémoire ci-dessus)
0x400b500x400a80Pointeur vers Derived :: Foo () (3) ., _ Vptr Points dérivés ici.
0x400b580x400a90Pointeur vers Parent :: FooNotOverridden () (identique à Parent)

1:


 (gdb) # ,        0x400aa0 (gdb) info symbol 0x400aa0 Parent::Foo() in section .text of a.out 

2:


 (gdb) info symbol 0x400a90 Parent::FooNotOverridden() in section .text of a.out 

3:


 (gdb) info symbol 0x400a80 Derived::Foo() in section .text of a.out 

Rappelez-vous que le pointeur vtable dans Derived pointait vers un décalage de +16 octets dans la vtable? Le troisième pointeur est l'adresse du pointeur de la première méthode. Vous voulez une troisième méthode? Pas de problème - ajoutez 2 sizeof (void ) au pointeur vtable. Vous voulez un enregistrement typeinfo? allez au pointeur devant lui.


Passons - qu'en est-il de la structure d'enregistrement typeinfo?


Parent :


L'adresseValeurTable des matières
0x400b780x602090Classe d'assistance pour les méthodes type_info (1)
0x400b800x400b69Une chaîne représentant le nom du type (2)
0x400b880x00 signifie aucune entrée typeinfo parent

Et voici l'entrée typeinfo Derived :


L'adresseValeurTable des matières
0x400b900x602210Classe d'assistance pour les méthodes type_info (3)
0x400b980x400b60Chaîne représentant le nom du type (4)
0x400ba00x400b78Pointeur vers une entrée parent typeinfo

1:


 (gdb) info symbol 0x602090 vtable for __cxxabiv1::__class_type_info@@CXXABI_1.3 + 16 in section .bss of a.out 

2:


 (gdb) x/s 0x400b69 0x400b69 <typeinfo name for Parent>: "6Parent" 

3:


 (gdb) info symbol 0x602210 vtable for __cxxabiv1::__si_class_type_info@@CXXABI_1.3 + 16 in section .bss of a.out 

4:


 (gdb) x/s 0x400b60 0x400b60 <typeinfo name for Derived>: "7Derived" 

Si vous voulez en savoir plus sur __si_class_type_info, vous pouvez trouver des informations ici et ici .


Cela épuise mes compétences avec gdb et complète également cette partie. Je suggère que certaines personnes trouvent cela trop faible, ou peut-être tout simplement pas de valeur pratique. Si c'est le cas, je recommanderais de sauter les parties 2 et 3, en passant directement à la partie 4 .


Partie 2 - Héritage multiple


Le monde des hiérarchies d'héritage unique est plus facile pour le compilateur. Comme nous l'avons vu dans la première partie, chaque classe enfant étend la table parent en ajoutant des entrées pour chaque nouvelle méthode virtuelle.


Examinons l'héritage multiple, ce qui complique la situation, même lorsque l'héritage est implémenté uniquement à partir d'interfaces.


Regardons l'extrait de code suivant:


 class Mother { public: virtual void MotherMethod() {} int mother_data; }; class Father { public: virtual void FatherMethod() {} int father_data; }; class Child : public Mother, public Father { public: virtual void ChildMethod() {} int child_data; }; 

Structure enfant
_vptr $ Mère
mother_data (+ padding)
_vptr $ Père
père_données
child_data (1)

Notez qu'il existe 2 pointeurs vtable. Intuitivement, je m'attendrais à 1 ou 3 pointeurs (mère, père et enfant). En fait, il est impossible d'avoir un pointeur (plus à ce sujet plus tard), et le compilateur est assez intelligent pour combiner les entrées de l'enfant vtable Child comme une continuation de vtable Mother, économisant ainsi 1 pointeur.


Pourquoi un enfant ne peut-il pas avoir un pointeur vtable pour les trois types? N'oubliez pas qu'un pointeur enfant peut être passé à une fonction qui accepte un pointeur mère ou père, et les deux s'attendent à ce que le pointeur this contienne les données correctes aux bons décalages. Ces fonctions n'ont pas besoin de connaître l'Enfant, et vous ne devez certainement pas supposer que l'Enfant est vraiment ce qui se trouve sous le pointeur Mère / Père avec lequel elles opèrent.


(1) Ce n'est pas pertinent pour ce sujet, mais, néanmoins, il est intéressant que child_data soit effectivement placé dans le remplissage de Father. C'est ce qu'on appelle le rembourrage de la queue et peut faire l'objet d'un futur article.


Voici la structure de vtable :


L'adresseValeurTable des matières
0x4008b80top_offset (plus à ce sujet plus tard)
0x4008c00x400930pointeur vers typeinfo pour enfant
0x4008c80x400800Mother :: MotherMethod (). _vptr $ Mère pointe ici.
0x4008d00x400810Child :: ChildMethod ()
0x4008d8-16top_offset (plus à ce sujet plus tard)
0x4008e00x400930pointeur vers typeinfo pour enfant
0x4008e80x400820Père :: FatherMethod (). _vptr $ Le père pointe ici.

Dans cet exemple, l'instance Child aura le même pointeur lors de la conversion vers le pointeur mère. Mais lors de la conversion vers le pointeur Father, le compilateur calcule le décalage du pointeur this pour pointer vers la partie _vptr $ Father de l'enfant (3e champ de la structure Child, voir le tableau ci-dessus).


En d'autres termes, pour un enfant donné c;: (void ) & c! = (Void ) static_cast <Father *> (& c). Certaines personnes ne s'attendent pas à cela, et peut-être qu'un jour ces informations vous feront gagner du temps pour le débogage.


J'ai trouvé cela utile plus d'une fois. Mais attendez, ce n'est pas tout.


Et si l'enfant décidait de passer outre l'une des méthodes du père? Considérez ce code:


 class Mother { public: virtual void MotherFoo() {} }; class Father { public: virtual void FatherFoo() {} }; class Child : public Mother, public Father { public: void FatherFoo() override {} }; 

La situation devient plus difficile. La fonction peut prendre l'argument Father * et appeler FatherFoo () pour cela. Mais si vous passez l'instance Child, il est prévu d'appeler la méthode Child surchargée avec le pointeur this correct. Cependant, l'appelant ne sait pas qu'il contient vraiment Child. Il a un pointeur sur le décalage enfant, où se trouve l'emplacement du père. Quelqu'un doit compenser le pointeur this, mais comment faire? Quelle magie le compilateur fait-il pour que cela fonctionne?


Avant de répondre à cette question, notez que remplacer une des méthodes mères n'est pas très délicat, car le pointeur this est le même. L'enfant sait quoi lire après vtable Mother, et s'attend à ce que les méthodes Child soient juste après.


Voici la solution: le compilateur crée une méthode thunk qui corrige le pointeur this puis appelle la méthode «real». L'adresse de la méthode de l'adaptateur sera sous le père vtable, tandis que la méthode «réelle» sera sous l'enfant vtable.


Voici le vtable Child :


 0x4008e8 <vtable for Child>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4008f0 <vtable for Child+8>: 0x60 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4008f8 <vtable for Child+16>: 0x00 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400900 <vtable for Child+24>: 0x10 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400908 <vtable for Child+32>: 0xf8 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400910 <vtable for Child+40>: 0x60 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x400918 <vtable for Child+48>: 0x20 0x08 0x40 0x00 0x00 0x00 0x00 0x00 

Qu'est-ce que cela signifie:


L'adresseValeurTable des matières
0x4008e80top_offset (à venir bientôt!)
0x4008f00x400960typeinfo pour enfant
0x4008f80x400800Mère :: MotherFoo ()
0x4009000x400810Enfant :: FatherFoo ()
0x400908-8top_offset
0x4009100x400960typeinfo pour enfant
0x4009180x400820pas d'adaptateur virtuel Child :: FatherFoo ()

Explication: comme nous l'avons vu précédemment, l'enfant a 2 tables - l'une est utilisée pour la mère et l'enfant et l'autre pour le père. Dans vtable Father, FatherFoo () pointe vers un «adaptateur», et dans vtable Child pointe directement vers Child :: FatherFoo ().


Et que contient cet «adaptateur», demandez-vous?


 (gdb) disas /m 0x400820, 0x400850 Dump of assembler code from 0x400820 to 0x400850: 15 void FatherFoo() override {} 0x0000000000400820 <non-virtual thunk to Child::FatherFoo()+0>: push %rbp 0x0000000000400821 <non-virtual thunk to Child::FatherFoo()+1>: mov %rsp,%rbp 0x0000000000400824 <non-virtual thunk to Child::FatherFoo()+4>: sub $0x10,%rsp 0x0000000000400828 <non-virtual thunk to Child::FatherFoo()+8>: mov %rdi,-0x8(%rbp) 0x000000000040082c <non-virtual thunk to Child::FatherFoo()+12>: mov -0x8(%rbp),%rdi 0x0000000000400830 <non-virtual thunk to Child::FatherFoo()+16>: add $0xfffffffffffffff8,%rdi 0x0000000000400837 <non-virtual thunk to Child::FatherFoo()+23>: callq 0x400810 <Child::FatherFoo()> 0x000000000040083c <non-virtual thunk to Child::FatherFoo()+28>: add $0x10,%rsp 0x0000000000400840 <non-virtual thunk to Child::FatherFoo()+32>: pop %rbp 0x0000000000400841 <non-virtual thunk to Child::FatherFoo()+33>: retq 0x0000000000400842: nopw %cs:0x0(%rax,%rax,1) 0x000000000040084c: nopl 0x0(%rax) 

Comme nous l'avons déjà expliqué, il s'agit de décalages et FatherFoo () est appelé. Et combien devrions-nous changer cela pour obtenir Child? top_offset!


Veuillez noter que je trouve personnellement que le nom de thunk non virtuel est extrêmement déroutant car il s'agit d'une entrée de table virtuelle pour une fonction virtuelle. Je ne suis pas sûr que ce ne soit pas virtuel, mais ce n'est que mon avis.




C'est tout pour le moment, dans un futur proche nous allons traduire 3 et 4 parties. Suivez l'actualité!

Source: https://habr.com/ru/post/fr479802/


All Articles