C++ Virtual Function In The Machine Level
The skill of OPF Programming
, (*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 Function
in C++.
C++
With Multiple VTable
The vtable
of C++ becomes complex when objects contains more than 1 vtable
s.
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"