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 of Virtual Function in 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"