C ++ vtables. Bagian 1 (dasar-dasar + beberapa Inheritance)

Halo semuanya! Terjemahan artikel disiapkan khusus untuk siswa kursus "Pengembang C ++" Apakah menarik untuk dikembangkan ke arah ini? Datang online 13 Desember pukul 20.00 waktu Moskow. ke kelas master "Berlatih menggunakan Kerangka Tes Google" !



Pada artikel ini, kita akan melihat bagaimana clang mengimplementasikan vtables (tabel metode virtual) dan RTTI (identifikasi tipe runtime). Pada bagian pertama, kita mulai dengan kelas dasar, dan kemudian melihat pewarisan berganda dan virtual.


Harap dicatat bahwa dalam artikel ini kita harus menggali representasi biner yang dihasilkan untuk berbagai bagian kode kita menggunakan gdb. Ini adalah level yang cukup rendah, tetapi saya akan melakukan semua kerja keras untuk Anda. Saya tidak berpikir bahwa sebagian besar posting di masa depan akan menjelaskan detail dari level rendah seperti itu.


Penafian : semua yang ditulis di sini tergantung pada implementasinya, dapat berubah dalam versi apa pun di masa mendatang, jadi Anda tidak harus bergantung padanya. Kami menganggap ini hanya untuk tujuan pendidikan.


luar biasa, maka mari kita mulai.


Bagian 1 - vtables - Dasar-dasar


Mari kita lihat kode berikut:


#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 memiliki ukuran 1 byte, karena di kelas C ++ tidak dapat memiliki ukuran nol. Namun, ini tidak penting sekarang.


VirtualClass adalah 8 byte pada mesin 64-bit. Mengapa Karena di dalamnya ada pointer tersembunyi yang menunjuk ke sebuah vtable. vtables adalah tabel terjemahan statis yang dibuat untuk setiap kelas virtual. Artikel ini berbicara tentang konten mereka dan bagaimana mereka digunakan.


Untuk mendapatkan pemahaman lebih dalam tentang seperti apa vtables, mari kita lihat kode berikut dengan gdb untuk mengetahui bagaimana memori dialokasikan:


 #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>} 

Inilah yang kami pelajari dari hal di atas:
- Terlepas dari kenyataan bahwa kelas tidak memiliki anggota data, ada pointer tersembunyi ke vtable;
- vtable untuk p1 dan p2 adalah sama. vtables adalah data statis untuk setiap jenis;
- d1 dan d2 mewarisi vtable-pointer dari Parent, yang menunjuk ke vtable Berasal;
- Semua vtables menunjukkan offset 16 (0x10) byte di vtable. Kami juga akan membahas ini nanti.


Mari kita lanjutkan sesi gdb kami untuk melihat isi dari vtables. Saya akan menggunakan perintah x, yang menampilkan memori di layar. Kita akan menghasilkan 300 byte dalam heksadesimal, dimulai dengan 0x400b40. Kenapa tepatnya alamat ini? Karena kami melihat di atas bahwa penunjuk vtable menunjuk ke 0x400b50, dan simbol untuk alamat ini adalah 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 ... 

Catatan: kita melihat karakter yang tidak dihiasi (demangled). Jika Anda benar-benar tertarik, _ZTV adalah awalan untuk vtable, _ZTS adalah awalan untuk tipe string (nama), dan _ZTI untuk typeinfo.



Berikut adalah struktur vtable Parent :


AlamatnyaNilaiIsi
0x400ba80x0top_offset (lebih lanjut tentang ini nanti)
0x400bb00x400b78Pointer untuk mengetik info untuk Induk (juga bagian dari dump memori di atas)
0x400bb80x400aa0Pointer to Parent :: Foo () (1) . _vptr Poin induk di sini.
0x400bc00x400a90Pointer to Parent :: FooNotOverridden () (2)

Berikut adalah struktur yang vtable Derived dari vtable Derived :


AlamatnyaNilaiIsi
0x400b400x0top_offset (lebih lanjut tentang ini nanti)
0x400b480x400b90Pointer untuk mengetikkan info untuk Berasal (juga bagian dari dump memori di atas)
0x400b500x400a80Pointer ke Turunkan :: Foo () (3) ., _ Vptr Turunkan poin di sini.
0x400b580x400a90Pointer to Parent :: FooNotOverridden () (sama seperti 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 

Ingat bahwa pointer vtable di Derived menunjuk ke offset +16 byte di vtable? Pointer ketiga adalah alamat dari pointer metode pertama. Ingin metode ketiga? Tidak masalah - tambahkan 2 sizeof (void ) ke pointer vtable. Ingin rekam typeinfo? pergi ke pointer di depannya.


Pindah - bagaimana dengan struktur catatan typeinfo?


Parent :


AlamatnyaNilaiIsi
0x400b780x602090Kelas pembantu untuk metode type_info (1)
0x400b800x400b69String yang mewakili nama tipe (2)
0x400b880x00 berarti tidak ada entri typeo orangtua

Dan di sini adalah entri typeinfo Derived :


AlamatnyaNilaiIsi
0x400b900x602210Kelas pembantu untuk metode type_info (3)
0x400b980x400b60String yang mewakili nama tipe (4)
0x400ba00x400b78Pointer ke entri Induk 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" 

Jika Anda ingin tahu lebih banyak tentang __si_class_type_info, Anda dapat menemukan beberapa informasi di sini dan juga di sini .


Ini melelahkan keahlian saya dengan gdb dan juga melengkapi bagian ini. Saya menyarankan bahwa beberapa orang menemukan ini terlalu rendah, atau mungkin hanya tidak memiliki nilai praktis. Jika demikian, saya akan merekomendasikan melewatkan bagian 2 dan 3, langsung ke bagian 4 .


Bagian 2 - Multiple Inheritance


Dunia hierarki warisan tunggal lebih mudah bagi kompilator. Seperti yang kita lihat di bagian pertama, setiap kelas anak memperluas vtable induk dengan menambahkan entri untuk setiap metode virtual baru.


Mari kita lihat multiple inheritance, yang memperumit situasi, bahkan ketika inheritance diimplementasikan hanya murni dari interface.


Mari kita lihat cuplikan kode berikut:


 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; }; 

Struktur anak
_vptr $ Ibu
mother_data (+ padding)
_vptr $ Ayah
data ayah
child_data (1)

Perhatikan bahwa ada 2 pointer vtable. Secara intuitif, saya akan mengharapkan 1 atau 3 petunjuk (Ibu, Ayah dan Anak). Bahkan, tidak mungkin untuk memiliki satu pointer (lebih lanjut tentang ini nanti), dan kompiler cukup pintar untuk menggabungkan entri anak vtable Anak sebagai kelanjutan dari vtable Ibu, sehingga menghemat 1 pointer.


Mengapa seorang anak tidak dapat memiliki satu pointer vtable untuk ketiga jenis? Ingat bahwa pointer Anak dapat diteruskan ke fungsi yang menerima pointer Ibu atau Ayah, dan keduanya akan mengharapkan pointer ini mengandung data yang benar di offset yang benar. Fungsi-fungsi ini tidak perlu diketahui tentang Child, dan Anda pasti tidak boleh berasumsi bahwa Child benar-benar berada di bawah pointer Ibu / Ayah yang dengannya mereka beroperasi.


(1) Itu tidak relevan dengan topik ini, tetapi, bagaimanapun, menarik bahwa child_data sebenarnya ditempatkan dalam pengisian Ayah. Ini disebut tail padding dan mungkin menjadi subjek posting di masa depan.


Berikut adalah struktur vtable :


AlamatnyaNilaiIsi
0x4008b80top_offset (lebih lanjut tentang ini nanti)
0x4008c00x400930arahkan ke ketik info untuk Anak
0x4008c80x400800Mother :: MotherMethod (). _vptr $ Poin ibu di sini.
0x4008d00x400810Anak :: AnakMetode ()
0x4008d8-16top_offset (lebih lanjut tentang ini nanti)
0x4008e00x400930arahkan ke ketik info untuk Anak
0x4008e80x400820Father :: FatherMethod (). _vptr $ Ayah menunjuk di sini.

Dalam contoh ini, turunan Child akan memiliki pointer yang sama ketika casting ke pointer Mother. Tetapi ketika melakukan casting ke pointer Father, kompiler menghitung offset dari pointer ini untuk menunjuk ke _vptr $ Father part of the Child (bidang ke-3 di struktur Child, lihat tabel di atas).


Dengan kata lain, untuk Anak tertentu c;: (void ) & c! = (Void ) static_cast <Father *> (& c). Beberapa orang tidak mengharapkan ini, dan mungkin suatu hari informasi ini akan menghemat waktu Anda untuk debugging.


Saya menemukan ini berguna lebih dari sekali. Tapi tunggu, itu belum semuanya.


Bagaimana jika Child memutuskan untuk mengganti salah satu metode Ayah? Pertimbangkan kode ini:


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

Situasi semakin sulit. Fungsi dapat mengambil argumen Father * dan memanggil FatherFoo () untuk itu. Tetapi jika Anda melewati instance Child, diharapkan memanggil metode Child yang diganti dengan pointer yang benar ini. Namun, si penelepon tidak tahu bahwa dia benar-benar mengandung Child. Ini memiliki pointer ke offset Anak, di mana lokasi Bapa. Seseorang harus mengimbangi pointer ini, tetapi bagaimana cara melakukannya? Keajaiban apa yang dilakukan kompiler untuk membuat ini bekerja?


Sebelum kita menjawab ini, perhatikan bahwa mengganti salah satu metode Mother tidak terlalu rumit, karena pointer ini sama. Anak tahu apa yang harus dibaca setelah Ibu, dan berharap metode Anak tepat setelah itu.


Inilah solusinya: kompiler membuat metode thunk yang mengoreksi pointer ini dan kemudian memanggil metode "nyata". Alamat metode adaptor akan berada di bawah vtable Father, sedangkan metode "real" akan berada di bawah vtable Child.


Ini adalah 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 

Apa artinya:


AlamatnyaNilaiIsi
0x4008e80top_offset (segera hadir!)
0x4008f00x400960typeinfo untuk anak
0x4008f80x400800Mother :: MotherFoo ()
0x4009000x400810Anak :: FatherFoo ()
0x400908-8top_offset
0x4009100x400960typeinfo untuk anak
0x4009180x400820bukan adaptor virtual Child :: FatherFoo ()

Penjelasan: seperti yang kita lihat sebelumnya, Anak memiliki 2 tabel - satu digunakan untuk Ibu dan Anak, dan yang lainnya untuk Ayah. Dalam vtable Father, FatherFoo () menunjuk ke "adaptor", dan dalam vtable Child menunjuk langsung ke Child :: FatherFoo ().


Dan apa yang ada dalam "adaptor" ini, Anda bertanya?


 (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) 

Seperti yang telah kita bahas, ini offset dan FatherFoo () dipanggil. Dan berapa banyak yang harus kita ubah untuk mendapatkan Anak? top_offset!


Harap dicatat bahwa saya pribadi menemukan nama thunk non-virtual sangat membingungkan karena ini adalah entri tabel virtual untuk fungsi virtual. Saya tidak yakin bahwa ini bukan virtual, tetapi ini hanya pendapat saya.




Itu saja untuk saat ini, dalam waktu dekat kami akan menerjemahkan 3 dan 4 bagian. Ikuti beritanya!

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


All Articles