Where is the C++ Virtual Function Table Located?

In general, the combination of inheritance and virtual functions in C++ provides us with polymorphic behavior. Today, we will explore the underlying mechanics of virtual function pointer calls and the location of the virtual function table in memory.

Let’s write a basic C++ program to verify this:

#include <iostream>

class Base {
public:
    Base() = default;
    virtual ~Base() {
        std::cout << "Base destructor" << std::endl;
    }

    virtual void func() {
        std::cout << "Base func call" << std::endl;
    }
private:
};

class Derive : public Base {
public:
    Derive() = default;
    ~Derive() {
        std::cout << "Derive destructor" << std::endl;
    }

    void func() override {
        std::cout << "Derive func call" << std::endl;
    }
private:
};

int main() {
    Base *bp = new Derive();
    bp->func();
    delete bp;

    return 0;
}
  • <span>Derive</span> inherits from <span>Base</span> and overrides the virtual function <span>func</span>
  • • In main, we create a subclass object and point it to a base class pointer
  • • The base class pointer calls <span>func</span>

The final output is as follows:

Derive func call
Derive destructor
Base destructor

Theory

This is the well-known inheritance and virtual function overriding. In this example, we know that:

  • • The <span>bp</span> pointer points to an object on the heap, corresponding to a virtual function pointer;
  • • This virtual function pointer points to the virtual function table, which contains the address of the overridden <span>func</span> in <span>Derive</span>;
  • • When we use <span>bp->func()</span>, a <span>this</span> pointer (the base class pointer) is passed to <span>func</span>, which is then converted to the subclass type <span>Derive</span> based on RTTI information, and the address of the overridden <span>func</span> is found using the <span>offset_to_top</span> from the virtual function table.
  • • We also know that the virtual function table is constructed during compilation.

So, where is the virtual function table located in the ELF file/executable file and process space after compilation?

Location of the Virtual Function Table in ELF

We can verify this using the following command:

nm -C test | c++filt | grep vtable

The output is as follows:

0000000000003d30 V vtable for Base
0000000000003d08 V vtable for Derive

These two addresses are the virtual addresses of the virtual function tables (vtable) for <span>Base</span> and <span>Derive</span> in the executable file.

objdump -S test | c++filt

Focus on the following output:

[24] .data.rel.ro      PROGBITS         0000000000003d08  00002d08
     0000000000000078  0000000000000000  WA       0     0     8

<span>.data.rel.ro</span> section starts at address <span>0x3d08</span> and has a size of <span>0x78</span> bytes.

Thus, the addresses of the virtual function tables for <span>Base</span> and <span>Derive</span> are both located in <span>.data.rel.ro</span>.

So why does the virtual function table appear in <span>.data.rel.ro</span> in this example?

The reasons are as follows:

  • • The vtable is a global static array that stores the addresses (pointers) of virtual functions.
  • • These pointers may need to be relocated at program load time, as the implementations of virtual functions may reside in different shared libraries or locations.
  • • The vtable does not need to be modified during program execution, so it is read-only.

Calling the Virtual Function Pointer in ELF

Next, let’s see how the corresponding virtual function is called in ELF. We can use:

objdump -S test | c++filt

We locate the output corresponding to <span>main</span>:

int main() {
    ....
    Base *bp = new Derive();
    11f6:       bf 08 00 00 00          mov    $0x8,%edi
    11fb:       e8 c0 fe ff ff          call   10c0 <operator new(unsigned long)@plt>
    1200:       48 89 c3                mov    %rax,%rbx
    1203:       48 c7 03 00 00 00 00    movq   $0x0,(%rbx)
    120a:       48 89 df                mov    %rbx,%rdi
    120d:       e8 4c 02 00 00          call   145e <Derive::Derive()>
    1212:       48 89 5d e8             mov    %rbx,-0x18(%rbp)
    bp->func();
    1216:       48 8b 45 e8             mov    -0x18(%rbp),%rax
    121a:       48 8b 00                mov    (%rax),%rax
    121d:       48 83 c0 10             add    $0x10,%rax
    1221:       48 8b 10                mov    (%rax),%rdx
    1224:       48 8b 45 e8             mov    -0x18(%rbp),%rax
    1228:       48 89 c7                mov    %rax,%rdi
    122b:       ff d2                   call   *%rdx
    delete bp;
    ....

Now, let’s explain this assembly code in detail, divided into two parts: object creation and virtual function call.

  1. 1. <span>Base *bp = new Derive();</span> object creation part
11f6:       bf 08 00 00 00          mov    $0x8,%edi
  • • The value 8 (the size of the object, since the Derive class only has a virtual table pointer and no member variables) is placed into <span>%edi</span> as a parameter for <span>operator new</span>.
11fb:       e8 c0 fe ff ff          call   10c0 <operator new(unsigned long)@plt>
  • • Calls <span>operator new</span>, allocating 8 bytes of memory, with the address returned in <span>%rax</span>.
1200:       48 89 c3                mov    %rax,%rbx
  • • Saves the allocated memory address into <span>%rbx</span>.
1203:       48 c7 03 00 00 00 00    movq   $0x0,(%rbx)
  • • Initializes the object’s starting address content to 0 (clears the vptr first).
120a:       48 89 df                mov    %rbx,%rdi
  • • Passes the object address as a parameter to the Derive constructor (this pointer).
120d:       e8 4c 02 00 00          call   145e <Derive::Derive()>
  • • Calls the Derive constructor, which will set the vptr.
1212:       48 89 5d e8             mov    %rbx,-0x18(%rbp)
  • • Saves the object address to a local variable (bp).
  1. 2. <span>bp->func();</span> virtual function call part
1216:       48 8b 45 e8             mov    -0x18(%rbp),%rax
  • • Retrieves the bp pointer (object address) into <span>%rax</span>.
121a:       48 8b 00                mov    (%rax),%rax
  • • Retrieves the content of the object’s starting address (vptr), which is the virtual function table address.
121d:       48 83 c0 10             add    $0x10,%rax
  • • Adds an offset of 0x10 to the virtual function table to find the function pointer for func (the second virtual function, usually the first is the destructor).
1221:       48 8b 10                mov    (%rax),%rdx
  • • Retrieves the address of func from the virtual function table into <span>%rdx</span>.
1224:       48 8b 45 e8             mov    -0x18(%rbp),%rax
  • • Retrieves the bp pointer again into <span>%rax</span>.
1228:       48 89 c7                mov    %rax,%rdi
  • • Passes the object address as the this pointer to func.
122b:       ff d2                   call   *%rdx
  • • Indirectly calls func (actually calls Derive::func, since bp points to Derive).

From the above process, we can see that the virtual function pointer is initialized in the object’s constructor, and when we call <span>func</span>, we need the bp pointer (which is this) to pass as a parameter. Let’s see how the virtual function pointer is set in the <span>Derive</span> constructor:

000000000000145e <Derive::Derive()>:
    Derive() = default;
    145e:       f3 0f 1e fa             endbr64 
    1462:       55                      push   %rbp
    1463:       48 89 e5                mov    %rsp,%rbp
    1466:       48 83 ec 10             sub    $0x10,%rsp
    146a:       48 89 7d f8             mov    %rdi,-0x8(%rbp)
    146e:       48 8b 45 f8             mov    -0x8(%rbp),%rax
    1472:       48 89 c7                mov    %rax,%rdi
    1475:       e8 c6 ff ff ff          call   1440 <Base::Base()>
    147a:       48 8d 15 97 28 00 00    lea    0x2897(%rip),%rdx        # 3d18 <vtable for Derive+0x10>
    1481:       48 8b 45 f8             mov    -0x8(%rbp),%rax
    1485:       48 89 10                mov    %rdx,(%rax)
    1488:       90                      nop
    1489:       c9                      leave  
    148a:       c3                      ret

This code does several things:

  • • Sets up the function stack frame, saves the old <span>%rbp</span>, and allocates space for local variables.
  • • Saves the incoming <span>this</span> pointer (object address) onto the stack at <span>-0x8(%rbp)</span>.
  • • Retrieves the <span>this</span> pointer and passes it to the <span>Base</span> constructor to complete the parent class initialization.
  • • Calculates the address of the <span>Derive</span> virtual function table (<span>vtable for Derive+0x10</span>) and stores it in <span>%rdx</span>.
  • • Writes the <span>%rdx</span> (vtable address) into the object’s starting address (i.e., sets the vptr).
  • • Cleans up the stack frame and returns.

Here, the address of the <span>Derive</span> virtual function table is also commented, calculated as follows:

  • • Current instruction address: <span>0x147a</span>
  • • Offset: <span>0x2897</span>
  • • Target address: <span>0x147a + 0x2897 = 0x3d11</span> (the actual objdump will automatically add the instruction length, resulting in <span>0x3d18</span>).

This is consistent with the virtual function table address we verified earlier.

Location of the Virtual Function Table in Process Space

Now that we have clarified the calling of the virtual function pointer in ELF and the location of the virtual function table in ELF, let’s look at the location of the virtual function table in process space. We can use gdb to debug the program:

gdb ./test
(gdb) break 31
(gdb) run
....
(gdb) print bp
(gdb) x/gx bp

<span>x/gx</span> is gdb’s memory examination command, which means:

  • <span>x</span>: examine, view memory content.
  • <span>g</span>: giant word, indicates examining 8 bytes (64 bits) at a time.
  • <span>x</span>: display in hexadecimal format.

I set a breakpoint at <span>b->func()</span><span>, and the output of the above commands is as follows:</span>

(gdb) p bp
$1 = (Base *) 0x55555556aeb0
(gdb) p &bp
$2 = (Base **) 0x7fffffffdf48
(gdb) x/gx bp
0x55555556aeb0: 0x0000555555557d30

• The bp is at <span>0x55555556aeb0</span>.

  • • The virtual function pointer is at <span>0x0000555555557d30</span>, pointing to the virtual function table.
  • To find the corresponding areas of these two addresses in the process virtual address space, we can use <span>info proc mappings</span> in gdb:

    (gdb) info symbol 0x0000555555557d30
    vtable for Derive + 16 in section .data.rel.ro of /home/zjp-android/test/test
    (gdb) info proc mappings
    process 126813
    Mapped address spaces:
    
              Start Addr           End Addr       Size     Offset  Perms  objfile
          0x555555554000     0x555555555000     0x1000        0x0  r--p   /home/zjp-android/test/test
          0x555555555000     0x555555556000     0x1000     0x1000  r-xp   /home/zjp-android/test/test
          0x555555556000     0x555555557000     0x1000     0x2000  r--p   /home/zjp-android/test/test
          0x555555557000     0x555555558000     0x1000     0x2000  r--p   /home/zjp-android/test/test
          0x555555558000     0x555555559000     0x1000     0x3000  rw-p   /home/zjp-android/test/test
          0x555555559000     0x55555557a000    0x21000        0x0  rw-p   [heap]
          0x7ffff7800000     0x7ffff7828000    0x28000        0x0  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
          0x7ffff7828000     0x7ffff79bd000   0x195000    0x28000  r-xp   /usr/lib/x86_64-linux-gnu/libc.so.6
          0x7ffff79bd000     0x7ffff7a15000    0x58000   0x1bd000  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
          0x7ffff7a15000     0x7ffff7a16000     0x1000   0x215000  ---p   /usr/lib/x86_64-linux-gnu/libc.so.6
          0x7ffff7a16000     0x7ffff7a1a000     0x4000   0x215000  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
          0x7ffff7a1a000     0x7ffff7a1c000     0x2000   0x219000  rw-p   /usr/lib/x86_64-linux-gnu/libc.so.6
          0x7ffff7a1c000     0x7ffff7a29000     0xd000        0x0  rw-p   
          0x7ffff7c00000     0x7ffff7c9a000    0x9a000        0x0  r--p   /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
          0x7ffff7c9a000     0x7ffff7dab000   0x111000    0x9a000  r-xp   /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
          0x7ffff7dab000     0x7ffff7e1a000   0x6f000   0x1ab000  r--p   /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
          0x7ffff7e1a000     0x7ffff7e1b000     0x1000   0x21a000  ---p   /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
          0x7ffff7e1b000     0x7ffff7e26000     0xb000   0x21a000  r--p   /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
          0x7ffff7e26000     0x7ffff7e29000     0x3000        0x0  rw-p   /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
          0x7ffff7e29000     0x7ffff7e2c000     0x3000        0x0  rw-p   
          0x7ffff7e9e000     0x7ffff7ea2000     0x4000        0x0  rw-p   
          0x7ffff7ea2000     0x7ffff7ea5000     0x3000        0x0  r--p   /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
          0x7ffff7ea5000     0x7ffff7ebc000    0x17000     0x3000  r-xp   /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
          0x7ffff7ebc000     0x7ffff7ec0000     0x4000    0x1a000  r--p   /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
          0x7ffff7ec0000     0x7ffff7ec1000     0x1000    0x1d000  r--p   /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
          0x7ffff7ec1000     0x7ffff7ec2000     0x1000    0x1e000  rw-p   /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
          0x7ffff7ec2000     0x7ffff7ed0000     0xe000        0x0  r--p   /usr/lib/x86_64-linux-gnu/libm.so.6
          0x7ffff7ed0000     0x7ffff7f4c000    0x7c000     0xe000  r-xp   /usr/lib/x86_64-linux-gnu/libm.so.6
          0x7ffff7f4c000     0x7ffff7fa7000    0x5b000    0x8a000  r--p   /usr/lib/x86_64-linux-gnu/libm.so.6
          0x7ffff7fa7000     0x7ffff7fa8000     0x1000    0xe4000  r--p   /usr/lib/x86_64-linux-gnu/libm.so.6
          0x7ffff7fa8000     0x7ffff7fa9000     0x1000    0xe5000  rw-p   /usr/lib/x86_64-linux-gnu/libm.so.6
          0x7ffff7fbb000     0x7ffff7fbd000     0x2000        0x0  rw-p   
          0x7ffff7fbd000     0x7ffff7fc1000     0x4000        0x0  r--p   [vvar]
          0x7ffff7fc1000     0x7ffff7fc3000     0x2000        0x0  r-xp   [vdso]
          0x7ffff7fc3000     0x7ffff7fc5000     0x2000        0x0  r--p   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
          0x7ffff7fc5000     0x7ffff7fef000    0x2a000     0x2000  r-xp   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
          0x7ffff7fef000     0x7ffff7ffa000     0xb000    0x2c000  r--p   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
          0x7ffff7ffb000     0x7ffff7ffd000     0x2000    0x37000  r--p   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
          0x7ffff7ffd000     0x7ffff7fff000     0x2000    0x39000  rw-p   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
          0x7ffffffde000     0x7ffffffff000    0x21000        0x0  rw-p   [stack]
      0xffffffffff600000 0xffffffffff601000     0x1000        0x0  --xp   [vsyscall]
    (gdb)

    Results:

    Element Address Memory Area Permissions Description
    <span>bp</span> pointer itself <span>0x7fffffffdf48</span> [stack] (stack) rw-p The local variable <span>bp</span> is stored on the stack, but its value is a heap address pointing to the dynamically created <span>Derive</span> object.
    <span>bp</span> pointed object <span>0x55555556aeb0</span> [heap] (heap) rw-p The object instance allocated on the heap by <span>new Derive()</span><span>.</span>
    Virtual Function Table (vtable) <span>0x0000555555557d30</span> The program’s <span>.data.rel.ro</span> section (within the executable file) rw-p <span>Derive</span> class’s virtual function table, stored in the program’s data segment.
    vptr <span>0x55555556aeb0</span> content at this address Beginning part of the object memory layout N/A Pointer to the virtual function table, stored at the beginning of the object instance.
    • • Address of the virtual function table
      • Address: <span>0x0000555555557d30</span>
      • Location: Executable file <span>/home/zjp-android/test/test</span> in the <span>.data.rel.ro</span> section.
      • • This address falls within the mapped area: <span>0x555555554000 0x555555555000 0x1000 0x0 r--p</span> (program text segment, read-only) and <span>0x555555558000 0x555555559000 0x1000 0x3000 rw-p</span> (data segment, read-write) in between. According to the output of <span>info symbol</span>, it explicitly indicates its location in the <span>.data.rel.ro</span> section. This is a segment that stores read-only relocation data.

    Summary

    1. 1. Lifecycle and Location of the Virtual Function Table (vtable)
    • • The vtable is generated by the compiler at compile time and is static data of the class.
    • • The addresses of <span>vtable for Derive</span> and <span>vtable for Base</span> can be confirmed by using the <span>nm</span> and <span>objdump</span> commands.
    • • In this example, it is stored in the executable file’s relocatable read-only data segment (<span>.data.rel.ro</span>).
    • • During program execution, the vtable is loaded into the corresponding memory mapped area (in GDB, this corresponds to the executable file mapping with read permissions).
  • 2. Lifecycle and Location of the Virtual Function Pointer (vptr)
    • • The vptr is part of each object instance that contains virtual functions. It is initialized during the runtime object construction, pointing to the corresponding vtable of its class.
    • • The vptr is typically located at the beginning of the object in memory layout. This can be verified by using the <span>x/gx bp</span> command in GDB, which shows that the value at the object address <span>0x55555556aeb0</span> is the vptr value <span>0x0000555555557d30</span>.
    • • The memory area where the vptr itself resides depends on how the object is allocated.
  • 3. Process of Polymorphic Calls The process of polymorphic calls is as follows:
    • • Find the vtable through the object’s vptr.
    • • Find the address of the virtual function to be called in the vtable using a fixed offset.
    • • Pass the <span>this</span> pointer (i.e., the object address) and call the function.

    Thoughts

    Where else might the virtual function table be stored besides in <span>.data.rel.ro</span>?

    I’ll leave that to you.

    Leave a Comment