How Virtual Tables Work in the Itanium C++ ABI

jandeboevrie1 pts0 comments

How Virtual Tables Work in the Itanium C++ ABI | File Descriptor TwoVirtual functions are one of C++&rsquo;s core features, enabling runtime polymorphism. Most C++ programmers use them regularly, yet few understand how they work in practice. What does the compiler actually generate when you declare a function virtual? How does the program figure out which implementation to call at runtime? Where is the vtable data actually stored? This blog post is focused on answering these questions.<br>The C++ standard specifies behavior but not implementation. This post describes the Itanium C++ ABI used by most platforms, with the notable exception of Microsoft MSVC.<br>A Basic Example#<br>Consider these classes with virtual functions:<br>struct Base {<br>virtual void foo() { __builtin_printf("Base::foo\n"); }<br>virtual void bar() { __builtin_printf("Base::bar\n"); }<br>virtual ~Base() {}<br>};

struct Derived : Base {<br>void foo() override { __builtin_printf("Derived::foo\n"); }<br>};

void call_foo(Base* b) {<br>b->foo(); // Which foo() gets called?

int main() {<br>Base base;<br>Derived derived;<br>call_foo(&base); // Should call Base::foo<br>call_foo(&derived); // Should call Derived::foo

The compiler doesn&rsquo;t know at compile time what type b points to in call_foo(). The function needs to dispatch to the correct foo() implementation based on the actual runtime type of the object. This is what vtables (short for virtual tables) enable.<br>The Virtual Table Structure#<br>Let&rsquo;s see what a vtable actually looks like. GCC has a useful flag -fdump-lang-class that dumps the class and vtable layout:<br>g++ -fdump-lang-class example.cpp

GCC should emit a file named a-example.cpp.001l.class containing info about the classes and vtables dumps.<br>Clang can dump similar information too, although the interface is different:<br>clang++ -Xclang -fdump-record-layouts -Xclang -fdump-vtable-layouts example.cpp

I learned about this from Dumping a C++ object&rsquo;s memory layout with Clang.<br>I think GCC has a slightly nicer output format, so all quoted output below is from gcc -fdump-lang-class.<br>For our Base class, GCC outputs:<br>Vtable for Base<br>Base::_ZTV4Base: 6 entries<br>0 (int (*)(...))0<br>8 (int (*)(...))(& _ZTI4Base)<br>16 (int (*)(...))Base::foo<br>24 (int (*)(...))Base::bar<br>32 (int (*)(...))Base::~Base<br>40 (int (*)(...))Base::~Base

Class Base<br>size=8 align=8<br>base size=8 base align=8<br>Base (0x0x23116c0) 0<br>vptr=((& Base::_ZTV4Base) + 16)<br>Let&rsquo;s break down what we&rsquo;re seeing here.<br>Mangled Names#<br>Before looking at the entries, notice the symbol name: _ZTV4Base. This is the mangled name for the vtable.<br>_Z prefix indicates Itanium name mangling<br>T is a special name prefix (used for vtables, typeinfo, thunks, and other compiler-generated symbols)<br>V means vtable (I is typeinfo)<br>4 is the length of the following name<br>Base is the class name<br>Similarly:<br>_ZTV7Derived = vtable for Derived<br>_ZTI4Base = typeinfo for Base<br>Vtable Layout#<br>The vtable named _ZTV4Base has 6 entries, each 8 bytes apart (on a 64-bit system). The structure is:<br>Offset 0: offset-to-top = 0<br>Offset 8: typeinfo pointer = &_ZTI4Base<br>Offset 16: foo() = &Base::foo<br>Offset 24: bar() = &Base::bar<br>Offset 32: ~Base() = &Base::~Base (D1)(complete object destructor)<br>Offset 40: ~Base() = &Base::~Base (D0)(deleting destructor)<br>Virtual Function Pointers#<br>Each virtual function declared in the class gets an entry in the vtable, in declaration order. Functions declared in Base are foo(), bar(), and the destructor.<br>Destructors get two vtable entries in the Itanium ABI:<br>Complete object destructor (D1) (offset 32): Destroys the object but doesn&rsquo;t free memory. Used for stack objects, members, and array elements.<br>Deleting destructor (D0) (offset 40): Calls the complete destructor (D1), then calls operator delete to free memory. Used when you write delete ptr.<br>There&rsquo;s also a third variant, the base object destructor (D2) , which doesn&rsquo;t appear in vtables and is only called directly by derived destructors. We&rsquo;ll see why it exists when we look at virtual inheritance.<br>Offset-to-Top#<br>This field records how far the vptr sits from the start of the complete object. In single inheritance, its value is zero, so it looks uninteresting. It becomes important once multiple inheritance is involved.<br>RTTI Typeinfo Pointer#<br>The typeinfo pointer enables runtime type identification (RTTI). It points to a std::type_info object for the class, which is used by dynamic_cast, typeid, and exception handling.<br>If -fno-rtti is used, this field is null.<br>Where Do Vtables Live?#<br>Vtables are static data structures that live in the .rodata section of the binary. But which translation unit actually emits them?<br>The Itanium ABI uses the key function rule : the vtable is emitted in the translation unit that defines the first non-inline virtual function.<br>// base.h<br>struct Base {<br>virtual void foo() { /* ... */ }; // Inline, not the key function<br>virtual void bar(); // Not inline - this is the key function<br>virtual ~Base();<br>};

// base.cpp<br>void Base::bar() { /*...

base virtual vtable rsquo class offset

Related Articles