Documente Academic
Documente Profesional
Documente Cultură
In this mini post-series we’ll explore how clang implements vtables & RTTI. In this part
we’ll start with some basic classes and later on cover multiple inheritance and virtual
inheritance.
Please note that this mini-series will include some digging into the binary generated for
our different pieces of code via gdb. This is somewhat low-level(ish), but I’ll do all the
heavy lifting for you. I don’t believe many future posts will be this low-level.
Disclaimer: everything written here is implementation specific, may change in any future
version, and should not be relied on. We look into this for educational reasons only.
☑ I agree
#include <iostream>
class NonVirtualClass {
public:
void foo() {}
};
class VirtualClass {
public:
virtual void foo() {}
};
int main() {
Size of NonVirtualClass: 1
Size of VirtualClass: 8
NonVirtualClass has a size of 1 because in C++ class es can’t have zero size.
However, this is not important right now.
To get some deeper understanding on how vtables look let’s explore the following code
with gdb to find out how the memory is laid out:
#include <iostream>
class Parent {
public:
public:
};
int main() {
$ # compile our code with debug symbols and start debugging using gdb
...
(gdb) b main
(gdb) run
(gdb) n
(gdb) n
(gdb) # print p1, p2, d1, d2 - we'll talk about what the output means soon
(gdb) p p1
(gdb) p p2
(gdb) p d1
fields>}
(gdb) p d2
fields>}
Even though the classes have no data members, there’s a hidden pointer to a
vtable;
vtable for p1 and p2 is the same. vtables are static data per-type;
d1 and d2 inherit a vtable-pointer from Parent which points to Derived ’s vtable;
All vtables point to an offset of 16 (0x10) bytes into the vtable. We’ll also discuss
this later.
Let’s continue with our gdb session to see the contents of the vtables. I will use
the x command, which dumps memory to the screen. I ask it to print 300 bytes in hex
format, starting at 0x400b40. Why this address? Because above we saw that the vtable
pointer points to 0x400b50, and the symbol for that address is vtable for
Derived+16 (16 == 0x10).
0x400b60 <typeinfo name for Derived>: 0x37 0x44 0x65 0x72 0x69
0x400ba8 <vtable for Parent>: 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00
...
Note: we’re looking at demangled symbols. If you really want to know, _ZTV is a prefix
for vtable, _ZTS is a prefix for type-string (name) and _ZTI is for type-info.
1:
(gdb) # find out what debug symbol we have for address 0x400aa0
2:
3:
Remember that the vtable pointer in Derived pointed to a +16 bytes offset into the
vtable? The 3rd pointer is the address of the first method pointer. Want the 3rd method?
No problem - add 2 * sizeof(void*) to vtable-pointer. Want the typeinfo record? jump
to the pointer before.
Parent ’s:
1:
a.out
2:
3:
of a.out
4:
If you want to read more about __si_class_type_info you can find some info here, and
also here.
This exhausts my gdb skills, and also concludes this post. I assume some people will
find this too low-level, or maybe just unactionable. If so, I’d recommend skipping parts 2
and 3, jumping straight to part 4.
The world of single-parent inheritance hierarchies is simpler for the compiler. As we saw
in Part 1, each child class extends its parent vtable by appending entries for each new
virtual method.
In this post we will cover multiple inheritance, which complicates things even when only
inheriting from pure-interfaces.
public:
int mother_data;
};
class Father {
public:
int father_data;
};
public:
int child_data;
};
Child ’s layout
_vptr$Mother
mother_data (+ padding)
_vptr$Father
father_data
child_data1
Note that there are 2 vtable pointers. Intuitively I’d expect either 1 or 3 pointers
( Mother , Father and Child ). In reality it’s impossible to have a single pointer (more on
this soon), and the compiler is smart enough to combine Child ’s vtable entries as a
continuation of Mother ’s vtable, thus saving 1 pointer.
Why can’t Child have one vtable pointer for all 3 types? Remember that a Child pointer
can be passed to a function accepting a Mother pointer or a Father pointer, and both
will expect the this pointer to hold the correct data in the correct offsets. These
functions don’t necessarily know of Child , and definitely shouldn’t assume that
a Child is really what’s underneath the Mother / Father pointer they have in their hands.
1 Unrelated to this topic, but interesting nontheless, is that child_data is actually placed
inside Father ’s padding. This is called ‘tail padding’, and might be the topic of a future
post.
In this example, an instance of Child will have the same pointer when casted to
a Mother pointer. But when casting to a Father pointer the compiler calculates an offset
of the this pointer to point to the _vptr$Father part of Child (3rd field in Child ’s
layout, see table above).
What if Child decided to override one of Father ’s methods? Consider this code:
class Mother {
public:
};
class Father {
public:
};
public:
};
This gets tricky. A function may take a Father* argument and call FatherFoo() on it.
But if you pass a Child instance, it is expected to invoke Child ’s overridden method
with the correct this pointer. However, the caller doesn’t know it’s really holding
a Child . It has a pointer to a Child ’s offset where Father ’s layout is. Someone needs to
offset this , but how is it done? What magic does the compiler perform to get this to
work?
[Before we answer that, note that overriding one of Mother ’s methods is not really tricky
as the this pointer is the same. Child knows to read beyond the Mother vtable and
expects the Child methods to be right after that.]
Here’s the solution: the compiler creates a ‘thunk’ method that corrects this and then
calls the ‘real’ method. The address of the thunk method will sit
under Child ’s Father vtable, while the ‘real’ method will be under Child ’s vtable.
0x00 0x00
Which means:
0x400908 -8 top_offset
Explanation: as we saw earlier, Child has 2 vtables - one used for Mother and Child ,
and the other for Father . In Father ’s vtable, FatherFoo() points to a thunk,
while Child ’s vtable points directly to Child::FatherFoo() .
And what’s in this thunk, you ask?
%rbp
%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)
0x8(%rbp),%rdi
$0xfffffffffffffff8,%rdi
0x400810 <Child::FatherFoo()>
$0x10,%rsp
0x0000000000400840 <non-virtual thunk to Child::FatherFoo()+32>: pop
%rbp
Like we discussed - offsetting this and calling FatherFoo() . And by how much should
we offset this to get Child? top_offset !
[Please note that I personally think that the name non-virtual thunk is extremely
confusing as this is the entry in the virtual table to the virtual function. I’m not sure
what’s not virtual about it, but that’s just my opinion.]
Stay tuned for Part 3 - Virtual inheritance - where things get even funkier.
In Part 1 and Part 2 of this series we talked about how vtables work in the simplest
cases, and then in multiple inheritance. Virtual inheritance complicates things even
further.
As you may remember, virtual inheritance means that there’s only one instance of a
base class in a concrete class. For example:
If weren’t for the virtual keyword above, iostream would in fact have two instances
of ios , which may cause sync headaches and would just be inefficient.
#include <iostream>
class Grandparent {
public:
};
public:
int parent1_data;
};
public:
int parent2_data;
};
public:
int child_data;
};
int main() {
Child child;
}
Let’s explore child . I’ll start by dumping a whole lot of memory just where Child ’s
vtable begins like we did in previous posts and will then analyze the results. I suggest
quickly glazing over the output here and coming back to it as I reveal details below.
(gdb) p child
0x400938 <vtable for Child>: 0x20 0x00 0x00 0x00 0x00 0x00
0x00 0x00
0x4009a0 <VTT for Child>: 0x50 0x09 0x40 0x00 0x00 0x00
0x00 0x00
0x4009a8 <VTT for Child+8>: 0xf8 0x09 0x40 0x00 0x00 0x00
0x00 0x00
0x4009b0 <VTT for Child+16>: 0x18 0x0a 0x40 0x00 0x00 0x00
0x00 0x00
0x4009b8 <VTT for Child+24>: 0x98 0x0a 0x40 0x00 0x00 0x00
0x00 0x00
0x4009c0 <VTT for Child+32>: 0xb8 0x0a 0x40 0x00 0x00 0x00
0x00 0x00
0x4009c8 <VTT for Child+40>: 0x98 0x09 0x40 0x00 0x00 0x00
0x00 0x00
0x4009d0 <VTT for Child+48>: 0x78 0x09 0x40 0x00 0x00 0x00
0x00 0x00
0x400a20 <typeinfo name for Parent1>: 0x37 0x50 0x61 0x72 0x65
0x400ac0 <typeinfo name for Parent2>: 0x37 0x50 0x61 0x72 0x65
0x400af8 <typeinfo name for Child>: 0x35 0x43 0x68 0x69 0x6c
Wow. That’s a lot of input. 2 new questions that immediately pop up: what’s a VTT and
what’s a construction vtable for X-in-Child ? We’ll answer these soon enough.
Size Value
8 bytes _vptr$Parent1
8 bytes _vptr$Parent2
4 bytes parent2_data
4 bytes child_data
8 bytes _vptr$Grandparent
Indeed, Child has only 1 instance of Grandparent . The non-trivial thing is that it is last
in memory even though it is topmost in the hierarchy.
0x400940 0 top_offset
Address Value Meaning
Above there’s a new concept - virtual-base offset . We’ll soon understand why it’s
there.
Value Meaning
0 top-offset
0x400870 Parent1::parent1_foo()
0 virtual-base offset
-32 top-offset
0x400880 Grandparent::grandparent_foo()
At this point I think it would be clearer to describe the process rather than dump more
tables with random numbers on you. So here goes:
Imagine you’re Child . You are asked to construct yourself on a fresh new piece of
memory. Since you’re inheriting Grandparent directly (that’s what virtual-inheritance
means), first you will call its constructor directly (if it wasn’t virtual inheritance you’d
call Parent1 ’s constructor, which in turn would have called Grandparent ’s constructor).
You set this += 32 bytes , as this is where Grandparent ’s data sits, and you call the
constructor. Easy peasy.
Next it’s time to construct Parent1 . Parent1 can safely assume that by the time it
constructs itself Grandparent had already been constructed, so it can, for instance,
access Grandparent ’s data and methods. But wait, how can it know where to find this
data? It’s not even near Parent1 ’s variables!
Now that we understand this, constructing Parent2 is basically the same, only
using construction table for Parent2-in-Child . And indeed, Parent2-in-Child has
a virtual-base offset of 16 bytes.
Take a moment to let all this info sink in. Are you ready to continue? Good.
Now let’s get back to that VTT thingy. Here’s the VTT layout:
Pheww… many details, but I think we covered everything I wanted to cover. In Part 4
we will talk about higher level details of vtables. Don’t miss it as it’s probably the most
important post in this series!
So far in this mini-series we learned how the vtables and typeinfo records are placed in
our binaries and how the compiler uses them. Now we’ll understand some of the work
the compiler does for us automatically.
Constructors
For any class’s constructor the following code is generated:
Here’s an example:
#include <iostream>
#include <string>
using namespace std;
class Parent {
public:
Parent() { Foo(); }
int i = 0;
};
public:
int j;
};
public:
string s;
};
int main() {
Grandchild g;
Given this, it’s no surprise that in the context of a class constructor, the vtable points to
that very class’s vtable rather than its concrete class. This means that virtual calls are
resolved as if no inheritors are available. Thus the output is:
Parent
Child
Grandchild
What about pure virtual functions? If they are not implemented (yes, you can implement
pure virtual functions, but why would you?) you’re probably (and hopefully) going to
segfault. Some compilers actually omit an error about this, which is cool.
Destructors
As one might imagine, destructors have the same behavior of constructors, only happen
in reverse order.
Here’s a quick thought-exercise: why do destructors change the vtable pointer to point
to the their own class’s rather than keep it pointing to the concrete class? Answer:
Because by the time the destructor runs, any inheriting class had already been
destroyed. Calling such class’s methods is not something you want to do.
Implicit casts
As we saw in Part 2 & Part 3, a pointer to a child is not necessarily equal to the same
instance’s parent pointer (like in multiple inheritance).
Yet, there’s no added work for you (the developer) to call a function that receives a
parent’s pointer. This is because the compiler implicitly offsets this when you up-cast
pointers and references to parent classes.
Method pointers
I plan to write a full post about method pointers in the future. Until then I’d like to stress
that a method pointer pointing at a virtual function will actually call the overridden
method (unlike non-member function pointers).
Test yourself!
You should now be able to explain to yourself why the following piece of code behaves
the way it does:
#include <iostream>
class FooInterface {
public:
};
class BarInterface {
public:
};
public:
};
int main() {
Concrete c;
c.Foo();
c.Bar();
foo->Foo();
This concludes my first blog post, which grew to become a 4 piece post. I hope you
learned some new things, I know I sure did.