C ++ vtables। भाग 1 (मूल बातें + एकाधिक वंशानुक्रम)

सभी को नमस्कार! लेख का अनुवाद विशेष रूप से पाठ्यक्रम "C ++ डेवलपर" के छात्रों के लिए तैयार किया गया था। क्या इस दिशा में विकास करना दिलचस्प है? 13 दिसंबर को 20:00 मास्को समय पर ऑनलाइन आओ। मास्टर वर्ग "Google टेस्ट फ्रेमवर्क का उपयोग करके अभ्यास करें" !



इस लेख में, हम देखेंगे कि कैसे क्लेंथ vtables (वर्चुअल मेथड टेबल) और RTTI (रनटाइम टाइप आइडेंटिफिकेशन) को लागू करता है। पहले भाग में, हम आधार कक्षाओं से शुरू करते हैं, और फिर कई और आभासी विरासत को देखते हैं।


कृपया ध्यान दें कि इस लेख में हमें gdb का उपयोग करके अपने कोड के विभिन्न भागों के लिए उत्पन्न द्विआधारी प्रतिनिधित्व में खुदाई करनी है। यह काफी निम्न स्तर है, लेकिन मैं आपके लिए पूरी मेहनत करूंगा। मुझे नहीं लगता कि भविष्य के अधिकांश पोस्ट इतने निचले स्तर के विवरण का वर्णन करेंगे।


डिस्क्लेमर : यहां लिखी गई हर चीज कार्यान्वयन पर निर्भर करती है, भविष्य के किसी भी संस्करण में बदल सकती है, इसलिए आपको इस पर भरोसा नहीं करना चाहिए। हम इसे केवल शैक्षिक उद्देश्यों के लिए मानते हैं।


उत्कृष्ट, तो चलो शुरू हो जाओ।


भाग 1 - vtables - मूल बातें


आइए निम्नलिखित कोड देखें:


#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 का आकार 1 बाइट होता है, क्योंकि C ++ कक्षाओं में शून्य का आकार नहीं हो सकता है। हालाँकि, यह अब महत्वपूर्ण नहीं है।


VirtualClass एक 64-बिट मशीन पर 8 बाइट्स है। क्यों? क्योंकि अंदर एक छिपी हुई पॉइंटर है जो एक वाइबेट की ओर इशारा करती है। vtables प्रत्येक आभासी कक्षा के लिए बनाई गई स्थिर अनुवाद तालिकाएँ हैं। यह लेख उनकी सामग्री के बारे में बात करता है और उनका उपयोग कैसे किया जाता है।


Vtables कैसा दिखता है, इसकी गहरी समझ पाने के लिए, आइए gdb के साथ निम्नलिखित कोड देखें कि कैसे स्मृति आवंटित की गई है:


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

ऊपर से हमने जो सीखा वह यहां है:
- इस तथ्य के बावजूद कि कक्षाओं में डेटा सदस्य नहीं हैं, वहाँ एक छिपी सूचक है, जो vtable है;
- p1 और P2 के लिए विटेबल समान है। vtables प्रत्येक प्रकार के स्थिर डेटा हैं;
- डी 1 और डी 2 को पेरेंट से वाइबेट-पॉइंटर विरासत में मिला है, जो वाइबर्ड डिविएड को इंगित करता है;
- सभी vtables 16 (0x10) बाइट्स ऑफ़सेट को वाइबेट में इंगित करते हैं। इसकी चर्चा हम बाद में भी करेंगे।


चलो vtables की सामग्री को देखने के लिए हमारे gdb सत्र को जारी रखें। मैं x कमांड का उपयोग करूंगा, जो स्क्रीन पर मेमोरी प्रदर्शित करता है। हम हेक्साडेसिमल में 300 बाइट्स का उत्पादन करने जा रहे हैं, जिसकी शुरुआत 0x400b40 से होती है। आखिर यह पता क्यों? क्योंकि हमने ऊपर देखा कि vtable पॉइंटर 0x400b50 की ओर इशारा करता है, और इस एड्रेस का सिंबल 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 ... 

नोट: हम डी-डेकोरेट (ध्वस्त) वर्णों को देखते हैं। यदि आप वास्तव में रुचि रखते हैं, तो _ZTV vtable के लिए उपसर्ग है, _ZTS प्रकार स्ट्रिंग (नाम) के लिए उपसर्ग है, और _ZTI टाइपिनफो के लिए है।



यहाँ vtable Parent संरचना है:


पतामूल्यसामग्री
0x400ba80x0top_offset (इस पर बाद में)
0x400bb00x400b78माता-पिता के लिए टाइपिनफो के लिए सूचक (उपरोक्त मेमोरी डंप का भी हिस्सा)
0x400bb80x400aa0माता-पिता की ओर इशारा :: फू () (1) । _vptr अभिभावक यहां बताते हैं।
0x400bc00x400a90जनक की ओर इशारा :: FooNotOverridden () (2)

यहाँ vtable Derived संरचना है:


पतामूल्यसामग्री
0x400b400x0top_offset (इस पर बाद में)
0x400b480x400b90व्युत्पन्न के लिए टाइप करने के लिए सूचक (ऊपर मेमोरी डंप का भी हिस्सा)
0x400b500x400a80व्युत्पन्न के लिए सूचक :: फू () (3) ।, _ Vptr व्युत्पन्न अंक यहाँ।
0x400b580x400a90माता-पिता की ओर इशारा :: FooNotOverridden () (माता-पिता के समान)

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 

याद रखें कि व्युत्पन्न में vtable सूचक ने vtable में +16 बाइट्स की भरपाई करने के लिए इशारा किया? तीसरा पॉइंटर पहली विधि के पॉइंटर का पता है। तीसरा तरीका चाहिए? कोई समस्या नहीं - 2 साइज़ोफ़ (शून्य ) को वाइबेटर पॉइंटर में जोड़ें। एक टाइपिनो रिकॉर्ड करना चाहते हैं? इसके सामने पॉइंटर पर जाएं।


चल रहा है - टाइपिनफो रिकॉर्ड संरचना के बारे में क्या?


Parent :


पतामूल्यसामग्री
0x400b780x602090Type_info (1) विधियों के लिए हेल्पर वर्ग
0x400b800x400b69प्रकार नाम का एक स्ट्रिंग (2)
0x400b880x00 का मतलब कोई पेरेंट टाइपइनफो एंट्री नहीं है

और यहाँ typeinfo Derived जाता है


पतामूल्यसामग्री
0x400b900x602210Type_info (3) विधियों के लिए हेल्पर वर्ग
0x400b980x400b60स्ट्रिंग प्रकार नाम का प्रतिनिधित्व (4)
0x400ba00x400b78एक टाइपिनफो अभिभावक प्रविष्टि को इंगित करता है

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_class_type_info के बारे में अधिक जानना चाहते हैं, तो आप यहाँ और साथ ही कुछ जानकारी पा सकते हैं।


यह gdb के साथ मेरे कौशल को समाप्त करता है और इस भाग को पूरा भी करता है। मेरा सुझाव है कि कुछ लोगों को यह बहुत कम लगता है, या शायद व्यावहारिक मूल्य का नहीं। यदि हां, तो मैं भाग 2 और 3 को लंघन करने की सलाह दूंगा, सीधे भाग 4 पर जा रहा हूं


भाग 2 - एकाधिक वंशानुक्रम


संकलक के लिए एकल वंशानुक्रम पदानुक्रम की दुनिया आसान है। जैसा कि हमने पहले भाग में देखा था, प्रत्येक बच्चे की कक्षा प्रत्येक नई आभासी पद्धति के लिए प्रविष्टियाँ जोड़कर मूल व्यवहार्य का विस्तार करती है।


आइए कई विरासत को देखें, जो स्थिति को जटिल बनाती है, यहां तक ​​कि जब विरासत को केवल इंटरफेस से पूरी तरह से लागू किया जाता है।


आइए निम्नलिखित कोड स्निपेट देखें:


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

बाल संरचना
_vtr $ माँ
माँ_दाता (+ गद्दी)
_vrr $ पिता
father_data
child_data (1)

ध्यान दें कि 2 व्यवहार्य बिंदु हैं। सहज रूप से, मैं 1 या 3 पॉइंटर्स (माता, पिता और बच्चे) की अपेक्षा करूंगा। वास्तव में, एक पॉइंटर (इस पर बाद में अधिक) होना असंभव है, और कंपाइलर पर्याप्त रूप से स्मार्ट है चाइल्ड वाइबेट चाइल्ड की प्रविष्टियों को वाइब्रेट मदर की निरंतरता के रूप में संयोजित करता है, इस प्रकार 1 पॉइंटर की बचत होती है।


तीनों प्रकारों के लिए एक बच्चे के पास एक व्यवहार्य सूचक क्यों नहीं हो सकता है? याद रखें कि एक चाइल्ड पॉइंटर को एक ऐसे फंक्शन में पास किया जा सकता है जो एक मदर या फादर पॉइंटर को स्वीकार करता है, और दोनों ही इस पॉइंटर से यह उम्मीद करेंगे कि सही ऑउटसेट में सही डेटा हो। इन कार्यों को चाइल्ड के बारे में जानने की आवश्यकता नहीं है, और आपको निश्चित रूप से यह नहीं मानना ​​चाहिए कि चाइल्ड वास्तव में वह है जो मदर / फादर पॉइंटर के अधीन है जिसके साथ काम करते हैं।


(1) यह इस विषय के लिए प्रासंगिक नहीं है, लेकिन, फिर भी, यह दिलचस्प है कि वास्तव में child_data को पिता के भरने में रखा गया है। इसे टेल पैडिंग कहा जाता है और भविष्य की पोस्ट का विषय हो सकता है।


यहाँ vtable संरचना है:


पतामूल्यसामग्री
0x4008b80top_offset (इस पर बाद में)
0x4008c00x400930चाइल्ड के लिए टाइपिनफो का सूचक
0x4008c80x400800माता :: मातृमेध ()। _vptr $ मदर पॉइंट्स यहाँ।
0x4008d00x400810बच्चा :: चाइल्डमैथोड ()
0x4008d8-16top_offset (इस पर बाद में)
0x4008e00x400930चाइल्ड के लिए टाइपिनफो का सूचक
0x4008e80x400820पिता :: FatherMethod ()। _vptr $ पिता यहाँ बताते हैं।

इस उदाहरण में, मदर पॉइंटर को कास्टिंग करते समय बाल उदाहरण में एक ही पॉइंटर होगा। लेकिन जब फादर पॉइंटर को कास्ट करते हैं, तो कंपाइलर इस प्वॉइंट की ऑफसेट की गणना बच्चे के _vptr $ फादर पार्ट (चाइल्ड स्ट्रक्चर में 3rd फील्ड, ऊपर दी गई टेबल देखें) की ओर इशारा करता है।


दूसरे शब्दों में, दिए गए चाइल्ड c के लिए ;: (void ) & c! = (Void ) static_cast <Father *> ((c)। कुछ लोग इसकी उम्मीद नहीं करते हैं, और शायद एक दिन यह जानकारी आपको कुछ समय डिबगिंग से बचाएगा।


मैंने इसे एक से अधिक बार उपयोगी पाया है। लेकिन रुकिए, यह सब नहीं है।


क्या होगा यदि बच्चा पिता विधियों में से एक को ओवरराइड करने का निर्णय लेता है? इस कोड पर विचार करें:


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

स्थिति कठिन होती जा रही है। समारोह तर्क पिता * को ले सकता है और इसके लिए फादरफू () को बुला सकता है। लेकिन अगर आप चाइल्ड इंस्टेंस पास करते हैं, तो इस पॉइंटर को सही करने के साथ ओवरराइड चाइल्ड मेथड को कॉल करना अपेक्षित है। हालांकि, फोन करने वाले को पता नहीं है कि उसके पास वास्तव में चाइल्ड है। इसमें चाइल्ड ऑफसेट के लिए एक पॉइंटर है, जहां पिता का स्थान है। किसी को इस सूचक को ऑफसेट करना है, लेकिन यह कैसे करना है? संकलक इस काम को करने के लिए क्या जादू करता है?


इससे पहले कि हम इसका उत्तर दें, ध्यान दें कि माता के तरीकों में से एक को ओवरराइड करना बहुत मुश्किल नहीं है, क्योंकि यह सूचक समान है। बच्चे को पता है कि व्यवहार्य माँ के बाद क्या पढ़ना है, और उम्मीद करता है कि बाल विधियाँ उसके बाद सही होंगी।


यहां समाधान है: कंपाइलर एक थंक विधि बनाता है जो इस पॉइंटर को सही करता है और फिर "वास्तविक" विधि को कॉल करता है। एडेप्टर विधि का पता व्यवहार्य पिता के अधीन होगा, जबकि "वास्तविक" विधि व्यवहार्य चाइल्ड के तहत होगी।


यहाँ 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 

इसका क्या मतलब है:


पतामूल्यसामग्री
0x4008e80top_offset (जल्द ही आ रहा है!)
0x4008f00x400960बच्चे के लिए टाइपिनफो
0x4008f80x400800माँ :: मदरफू ()
0x4009000x400810बच्चा :: फादरफू ()
0x400908-8top_offset
0x4009100x400960बच्चे के लिए टाइपिनफो
0x4009180x400820वर्चुअल एडेप्टर चाइल्ड :: फादरफू ()

स्पष्टीकरण: जैसा कि हमने पहले देखा, बाल में 2 vtables हैं - एक का उपयोग माता और बच्चे के लिए किया जाता है, और दूसरा पिता के लिए। व्यवहार्य पिता में, फादरफू () एक "एडेप्टर" की ओर इशारा करता है, और वाइबल चाइल्ड में सीधे चाइल्ड :: फादरफू () पर इशारा करता है।


और इस "एडेप्टर" में क्या है, आप पूछें?


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

जैसा कि हमने पहले ही चर्चा की है, यह ऑफसेट है और फादरफू () कहा जाता है। और हमें चाइल्ड पाने के लिए इसे कितना शिफ्ट करना चाहिए? top_offset!


कृपया ध्यान दें कि मुझे व्यक्तिगत रूप से गैर-आभासी थंक नाम बेहद भ्रमित करने वाला लगता है क्योंकि यह वर्चुअल फ़ंक्शन के लिए वर्चुअल टेबल एंट्री है। मुझे यकीन नहीं है कि यह आभासी नहीं है, लेकिन यह केवल मेरी राय है।




अभी के लिए यही है, निकट भविष्य में हम 3 और 4 भागों का अनुवाद करेंगे। खबर का पालन करें!

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


All Articles