2011-01-05 5 views
7

Ich "spiele" mit virtueller Vererbung in C++, und ich möchte wissen, wie ein Klassenobjekt ausgelegt ist. Ich habe diese drei Klassen:entschlüsseln vtable dumps

class A { 
private: 
    int a; 
public: 
    A() {this->a = 47;} 
    virtual void setInt(int x) {this->a = x;} 
    virtual int getInt() {return this->a;} 
    ~A() {this->a = 0;} 
}; 

class B { 
private: 
    int b; 
public: 
    B() {b = 48;} 
    virtual void setInt(int x) {this->b = x;} 
    virtual int getInt() {return this->b;} 
    ~B() {b = 0;} 
}; 

class C : public A, public B { 
private: 
    int c; 
public: 
    C() {c = 49;} 
    virtual void setInt(int x) {this->c = x;} 
    virtual int getInt() {return this->c;} 
    ~C() {c = 0;} 
}; 

(ich glaube, sie sind richtig: p)

I verwendet -fdump-class-hierarchy mit g ++, und ich habe diese

Vtable for A 
A::_ZTV1A: 4u entries 
0  (int (*)(...))0 
8  (int (*)(...))(& _ZTI1A) 
16 A::setInt 
24 A::getInt 

Class A 
    size=16 align=8 
    base size=12 base align=8 
A (0x10209fb60) 0 
    vptr=((& A::_ZTV1A) + 16u) 

Vtable for B 
B::_ZTV1B: 4u entries 
0  (int (*)(...))0 
8  (int (*)(...))(& _ZTI1B) 
16 B::setInt 
24 B::getInt 

Class B 
    size=16 align=8 
    base size=12 base align=8 
B (0x1020eb230) 0 
    vptr=((& B::_ZTV1B) + 16u) 

Vtable for C 
C::_ZTV1C: 8u entries 
0  (int (*)(...))0 
8  (int (*)(...))(& _ZTI1C) 
16 C::setInt 
24 C::getInt 
32 (int (*)(...))-0x00000000000000010 
40 (int (*)(...))(& _ZTI1C) 
48 C::_ZThn16_N1C6setIntEi 
56 C::_ZThn16_N1C6getIntEv 

Class C 
    size=32 align=8 
    base size=32 base align=8 
C (0x1020f5080) 0 
    vptr=((& C::_ZTV1C) + 16u) 
    A (0x1020ebd90) 0 
     primary-for C (0x1020f5080) 
    B (0x1020ebe00) 16 
     vptr=((& C::_ZTV1C) + 48u) 

Nun, was zum Teufel sind diejenigen, (int (*)(...))-0x00000000000000010 und C::_ZThn16_N1C6setIntEi and (int (*)(...))0 ?? Kann jemand den Müll erklären?

Vielen Dank.

+1

Es ist undefined. Jeder Compiler (und sogar Versionen des Compilers) machen es anders. –

+0

Sie können C++ filt verwenden, um '_ZTI1C' zu decodieren. Die anderen sind Orte, die wahrscheinlich durch eine spätere Stufe im Compiler mit Funktionszeigern gefüllt werden. –

Antwort

6

Ich bin nicht 100% sicher, dass diese Antwort richtig ist, aber hier ist meine beste Schätzung.

Wenn Sie eine Klasse haben, die Multiplizieren und nicht-virtuell erbt, ist das Layout der Klasse normalerweise ein vollständiges Objekt des ersten Basistyps, dann ein vollständiges Objekt des zweiten Basistyps und dann die Daten für das Objekt selbst . Wenn Sie B betrachten, können Sie den vtable-Zeiger für das A-Objekt sehen, und wenn Sie sich C ansehen, können Sie sehen, dass es Zeiger für die Objekte A und B in der vtable gibt.

Da die Objekte auf diese Weise angeordnet sind, bedeutet das, dass wenn Sie einen B* Zeiger auf ein C Objekt zeigen, der Zeiger tatsächlich nicht an der Basis des Objekts sein wird; eher wird es irgendwo in der Mitte zeigen. Dies bedeutet, dass Sie, wenn Sie das Objekt jemals in eine A*-Instanz umwandeln müssen, den Zeiger B* so einstellen müssen, dass er zum Anfang des Objekts zurückspringt. Um dies zu tun, muss der Compiler irgendwo die Anzahl der Bytes codieren, die Sie zurückspringen müssen, um zum Anfang des Objekts zu gelangen. Ich denke, dass die erste (int(*)(...)) tatsächlich nur eine rohe Anzahl von Bytes ist, die Sie betrachten müssen, um zum Anfang des Objekts zu gelangen. Wenn Sie bemerken, ist für die A vtable dieser Zeiger 0 (da die vtable für A ist am Anfang des Objekts, und das gleiche gilt für die B vtable (da es auch am Anfang des Objekts lebt. Allerdings Beachten Sie, dass die C vtable zwei Teile hat - der erste Teil ist der vtable für A, und der erste verrückte Eintrag ist ebenfalls Null (da Sie, wenn Sie an der A vtable sind, keine Anpassungen vornehmen müssen). Nach der ersten Hälfte dieser Tabelle scheint jedoch die B Vtable, und beachten Sie, dass der erste Eintrag ist Hexadezimalwert -0x10. Wenn Sie das C Objektlayout betrachten, werden Sie feststellen, dass der Vtable-Zeiger B 16 Bytes ist nach dem A Vtable-Zeiger.Dieser -0x10 Wert kann der korrigierende Offset sein, den Sie über die 012 zurückspringen müssenVtable-Zeiger, um zurück zum Stamm des Objekts zu gelangen.

Der zweite verrückte Eintrag jeder vtable scheint ein Zeiger auf die vtable selbst zu sein. Beachten Sie, dass es immer der Adresse des vtable-Objekts entspricht (vergleichen Sie den Namen der vtable und was sie anzeigt). Dies wäre notwendig, wenn Sie irgendeine Art von Laufzeittypidentifikation durchführen möchten, da dies normalerweise die Adresse der vtable (oder zumindest etwas in der Nähe davon) betrifft.

Schließlich, wie, warum es die kryptisch benannten setInt und getInt Funktionen am Ende des C VTable, ich bin ziemlich sicher, das ist, weil die C Typ zwei verschiedene Sätze von Funktionen erbt namens setInt und getInt - ein durch A und eins bis B.Wenn ich raten musste, ist das Mangeln hier, um sicherzustellen, dass die Interna des Compilers zwischen den zwei virtuellen Funktionen unterscheiden können.

Hoffe, das hilft!

+1

Auf die erste Zahl "-0x10" habe ich auch als Offset des Unterobjekts innerhalb des endgültigen Objekts gedacht. Warum das so sein würde ... Ich stimme Ihrer Argumentation nicht ganz zu, da der Compiler alle Klassendefinitionen sieht, wenn er die Umwandlungen durchführt (ob implizit oder explizit), also ist es kein Hinweis für den Compiler. Dann dachte ich über andere mögliche Gründe nach, und das Einzige, was mir einfällt, ist, dass der Compiler beim Löschen durch einen Zeiger auf "B" einen Zeiger auf den Anfang bekommen kann, um den Speicher freizugeben. Aber ich bin mir dessen nicht sicher. –

+1

Bei den zweiten Einträgen handelt es sich höchstwahrscheinlich nicht um Zeiger auf die Vtables, sondern um Zeiger auf das typeinfo-Objekt, das dieser bestimmten Instanz zugeordnet ist. Beachten Sie die konkreten Werte: 'A :: _ ZTV1A' vs' _ZTI1A', und dass vptr im 'A'-Objekt auf' (& A :: _ ZTV1A - 16u) gesetzt wird '... sie stimmen nicht überein. –

+0

Das sind beides sehr gute Punkte. Ich bin mir ziemlich sicher, dass du in beiden Punkten richtig bist. – templatetypedef

5

Hier ist Ihre Dump durch c lief ++ filt:

Vtable for A 
A::vtable for A: 4u entries 
0  (int (*)(...))0 
8  (int (*)(...))(& typeinfo for A) 
16 A::setInt 
24 A::getInt 

Class A 
    size=16 align=8 
    base size=12 base align=8 
A (0x10209fb60) 0 
    vptr=((& A::vtable for A) + 16u) 

Vtable for B 
B::vtable for B: 4u entries 
0  (int (*)(...))0 
8  (int (*)(...))(& typeinfo for B) 
16 B::setInt 
24 B::getInt 

Class B 
    size=16 align=8 
    base size=12 base align=8 
B (0x1020eb230) 0 
    vptr=((& B::vtable for B) + 16u) 

Vtable for C 
C::vtable for C: 8u entries 
0  (int (*)(...))0 
8  (int (*)(...))(& typeinfo for C) 
16 C::setInt 
24 C::getInt 
32 (int (*)(...))-0x00000000000000010 
40 (int (*)(...))(& typeinfo for C) 
48 C::non-virtual thunk to C::setInt(int) 
56 C::non-virtual thunk to C::getInt() 

Class C 
    size=32 align=8 
    base size=32 base align=8 
C (0x1020f5080) 0 
    vptr=((& C::vtable for C) + 16u) 
    A (0x1020ebd90) 0 
     primary-for C (0x1020f5080) 
    B (0x1020ebe00) 16 
     vptr=((& C::vtable for C) + 48u) 

keine Ahnung, was die (int (*)(...))-0x00000000000000010 und (int (*)(...))0 sind.
Der C::_ZThn16_N1C6setIntEi/C::non-virtual thunk to C::setInt(int) Teil ist eine "Optimierung von virtuellen Funktionsaufrufen bei Vorhandensein mehrerer oder virtueller Vererbung" wie beschrieben here.

+0

+1 Beat mich dazu :) Dies ist der einzige Teil der Frage, die ich hatte klar - es ist einfach, wenn Sie sowohl den Klassen-Dump und die Assembly erhalten: Offset der 'this-Zeiger um 16 und springen zu' setInt/getInt' (in jedem Fall) –

+0

@David: Wenn irgendwelche anderen Teile klar werden, fügen Sie sie bitte der Antwort hinzu. –