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.
<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).
- 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>.
<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. 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).
- • 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.
- • 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.