C++ Virtual Function In The Machine Level
OPF Programming, or (*p)->f(p), comes from the concept
of Virtual Function in C++.
OPF Programming removes the fancy parts while keeping the core of
Virtual Function, which leads to a beautiful balance between practicality
and elegance.
During the promotion of OPF Programming, we found that many C++ users do not
understand how virtual function works, and can not see that Virtual Function
and (*p)->f(p) have the same root.
This is why we write this article: By explaining the mechanism of
Virtual Function in C++, we hope more people understand the skill of
OPF Programming.
Example
A multiple inheritance example (type C inherits functions/methods from both
type A and type B):
class A {
public:
virtual int af1(int v);
virtual int af2(int v);
};
class B {
public:
virtual int bf1(int v);
virtual int bf2(int v);
virtual int bf3(int v);
};
class C : public A, public B {
private:
int i;
int j;
public:
int af1(int v) override;
int bf1(int v) override;
};
According to the assemly language code generated by compiler, which can be
found at the bottom of this article, we can see the vtable part:
vtable for C:
.quad 0 ;;Vtable1
.quad typeinfo for C
.quad C::af1(int) ;;Entry of vtable1
.quad A::af2(int)
.quad C::bf1(int)
.quad -8 ;;Vtable2
.quad typeinfo for C
.quad non-virtual thunk to C::bf1(int) ;;Entry of vtable2
.quad B::bf2(int)
.quad B::bf3(int)
We can see that the start of the vtable is not the entry point of the vtable. There are 2 fields before function pointers: The offset of the vtable, the type information.
This type information is the key to support RTTI(Run-time type information).
The way to use vtable can be found in the constructor of type C.
...
call A::A() [base object constructor]
...
call B::B() [base object constructor]
mov edx, OFFSET FLAT:vtable for C+16 ;;Load the address of entry of vtable1
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rax], rdx ;;Write to address of object
mov edx, OFFSET FLAT:vtable for C+56 ;;Load the address of entry of vtable2
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rax+8], rdx ;;Write to address of object + 8
nop
leave
ret
We can understand the way virtual function works through 2 examples.
Example 1, accessing C through pointer of type A:
A *c1 = new C();
int tmp = c1->af1(11);
mov edi, 24 ;;Argument for `new` (the size to allocate)
call operator new(unsigned long) ;;Call `new`
mov rbx, rax
mov QWORD PTR [rbx], 0 ;;Initialize the allocated memory with `0`s
mov QWORD PTR [rbx+8], 0
mov DWORD PTR [rbx+16], 0
mov DWORD PTR [rbx+20], 0
mov rdi, rbx ;;Prepare arguments for constructor of C
call C::C() [complete object constructor] ;;Call constructor
mov QWORD PTR [rbp-24], rbx
mov rax, QWORD PTR [rbp-24] ;;Load address of `c1` to rax
mov rax, QWORD PTR [rax] ;;Indirect memory access to get address of vtable
mov rdx, QWORD PTR [rax] ;;Indirect memory access to get address of function
mov rax, QWORD PTR [rbp-24]
mov esi, 11 ;;Prepare the 2nd argument `11`
mov rdi, rax ;;Prepare the 1st argument `c1`
call rdx ;;Call the function
According the virtual function invocation, the code is:
c1->af1(11);
But actually, through the assembly language code, we know the real code is:
(*c1)->af1(c1, 11);
Ignore the unimportant arguments, it's:
(*p)->f(p);
This is why we said:
The skill of
OPF Programming,(*p)->f(p), comes from the concept ofVirtual Functionin C++.
C++ With Multiple VTable
The vtable of C++ becomes complex when objects contains more than 1 vtables.
Example 2, accessing C through pointer of type B:
B *c2 = new C();
int tmp = c2->bf1(22);
mov edi, 24 ;;Argument for `new` (the size to allocate)
call operator new(unsigned long) ;;Call `new`
mov rbx, rax
mov QWORD PTR [rbx], 0 ;;Initialize the allocated memory with `0`s
mov QWORD PTR [rbx+8], 0
mov DWORD PTR [rbx+16], 0
mov DWORD PTR [rbx+20], 0
mov rdi, rbx ;;Prepare arguments for constructor of C
call C::C() [complete object constructor] ;;Call constructor
test rbx, rbx
je .L9
lea rax, [rbx+8] ;;Adjust the address in rax! Make it point to the pointer to vtable2
jmp .L10
.L9:
mov eax, 0
.L10:
mov QWORD PTR [rbp-32], rax
mov rax, QWORD PTR [rbp-32] ;;Like the previous example...
mov rax, QWORD PTR [rax]
mov rdx, QWORD PTR [rax]
mov rax, QWORD PTR [rbp-32]
mov esi, 22
mov rdi, rax
call rdx
Wait! the address of the object in this invocation is wrong!
it's address of the object + 8 while it should be exactly address of the object.
Because we need the address of the object in the implementation of the virtual function.
So how to solve this problem?
Look back at the vtable, we can find a weird name
non-virtual thunk to C::bf1(int).
The assemly language code for this name is:
non-virtual thunk to C::bf1(int):
sub rdi, 8
jmp .LTHUNK0
What this piece of code is doing is simple:
Subtract 8 from the rdi register (the first argument, address of vtable 2)
to get the address of the object.
After this, it will call the real virtual function C::bf1.
If we merge this subtraction into the previous expression, we can get:
(*p)->f(p - N);
This expression also exists in another article "Another Way For Multiple Interface In C Language". Comparing these 2 articles may help a lot.
The Code
class A {
public:
virtual int af1(int v);
virtual int af2(int v);
};
class B {
public:
virtual int bf1(int v);
virtual int bf2(int v);
virtual int bf3(int v);
};
class C : public A, public B {
private:
int i;
int j;
public:
int af1(int v) override;
int bf1(int v) override;
};
int C::af1(int v)
{
return i + v;
}
int C::bf1(int v)
{
return i + v;
}
int f(void)
{
int tmp;
A *c1 = new C();
tmp = c1->af1(11);
B *c2 = new C();
tmp = c2->bf1(22);
return 0;
}
Assembly code generated by x86-64 gcc 14.1:
C::af1(int):
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
mov DWORD PTR [rbp-12], esi
mov rax, QWORD PTR [rbp-8]
mov edx, DWORD PTR [rax+16]
mov eax, DWORD PTR [rbp-12]
add eax, edx
pop rbp
ret
C::bf1(int):
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
mov DWORD PTR [rbp-12], esi
mov rax, QWORD PTR [rbp-8]
mov edx, DWORD PTR [rax+16]
mov eax, DWORD PTR [rbp-12]
add eax, edx
pop rbp
ret
non-virtual thunk to C::bf1(int):
sub rdi, 8
jmp .LTHUNK0
A::A() [base object constructor]:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
mov edx, OFFSET FLAT:vtable for A+16
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rax], rdx
nop
pop rbp
ret
B::B() [base object constructor]:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
mov edx, OFFSET FLAT:vtable for B+16
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rax], rdx
nop
pop rbp
ret
C::C() [base object constructor]:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call A::A() [base object constructor]
mov rax, QWORD PTR [rbp-8]
add rax, 8
mov rdi, rax
call B::B() [base object constructor]
mov edx, OFFSET FLAT:vtable for C+16
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rax], rdx
mov edx, OFFSET FLAT:vtable for C+56
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rax+8], rdx
nop
leave
ret
f():
push rbp
mov rbp, rsp
push rbx
sub rsp, 40
mov edi, 24
call operator new(unsigned long)
mov rbx, rax
mov QWORD PTR [rbx], 0
mov QWORD PTR [rbx+8], 0
mov DWORD PTR [rbx+16], 0
mov DWORD PTR [rbx+20], 0
mov rdi, rbx
call C::C() [complete object constructor]
mov QWORD PTR [rbp-24], rbx
mov rax, QWORD PTR [rbp-24]
mov rax, QWORD PTR [rax]
mov rdx, QWORD PTR [rax]
mov rax, QWORD PTR [rbp-24]
mov esi, 11
mov rdi, rax
call rdx
mov DWORD PTR [rbp-36], eax
mov edi, 24
call operator new(unsigned long)
mov rbx, rax
mov QWORD PTR [rbx], 0
mov QWORD PTR [rbx+8], 0
mov DWORD PTR [rbx+16], 0
mov DWORD PTR [rbx+20], 0
mov rdi, rbx
call C::C() [complete object constructor]
test rbx, rbx
je .L9
lea rax, [rbx+8]
jmp .L10
.L9:
mov eax, 0
.L10:
mov QWORD PTR [rbp-32], rax
mov rax, QWORD PTR [rbp-32]
mov rax, QWORD PTR [rax]
mov rdx, QWORD PTR [rax]
mov rax, QWORD PTR [rbp-32]
mov esi, 22
mov rdi, rax
call rdx
mov DWORD PTR [rbp-36], eax
mov eax, 0
mov rbx, QWORD PTR [rbp-8]
leave
ret
vtable for C:
.quad 0
.quad typeinfo for C
.quad C::af1(int)
.quad A::af2(int)
.quad C::bf1(int)
.quad -8
.quad typeinfo for C
.quad non-virtual thunk to C::bf1(int)
.quad B::bf2(int)
.quad B::bf3(int)
typeinfo for C:
.quad vtable for __cxxabiv1::__vmi_class_type_info+16
.quad typeinfo name for C
.long 0
.long 2
.quad typeinfo for A
.quad 2
.quad typeinfo for B
.quad 2050
typeinfo name for C:
.string "1C"