← Back to list

The ASM flow of a C++ Class with virtual methods— Part 1 Constructors

So what do we really know about inheritance and polymorphism?

D-A · 2022-12-02 21:41 · 0 claps · 6.9 min read
#cplusplus #disassembly #reverse-engineering #constructor #windbg
Open on Medium ↗

The ASM flow of a C++ Class with virtual methods— Part 1 Constructors

So what do we really know about inheritance and polymorphism?

Have you ever wondered what’s really happening underneath?

Today we’re going (almost instruction by instruction) following the flow of a class implemented/reimplemented with simple inheritance and virtual methods (Part 2 here).

Going to write the code for both samples, compile it in Visual Studio 2022 in Debug mode (otherwise the simple inheritance sample will be reduced to 2 cout) with “Just my code” disabled to clean up the assembly.

After this I will use the WinDBG feature called Time Travel Debugging to generate a recording of the application execution and will use it instead of the binaries, so I don’t have to be setting breakpoints and restarting the application.

Both samples will produce the same output, the job here is to observe how the compiler generates the code for both cases, you’ll get an understanding on why polymorphism at runtime is said to be slower than when the compiler can infer the types at build time, this is also a good exercise for people getting related to disassembling code.

First the simple inheritance code sample:

// SimpleInheritanceCalls.cpp : This file contains the 'main' function. Program execution begins and ends there.
//
#include <iostream>

using namespace std;

class Parent {

protected:
    int valueSet;

public:
    Parent() { valueSet = 0; }
    ~Parent() {}

    void CallMe();
    int SetMe(int val);
};

void Parent::CallMe() { cout << "Called Parent" << endl; }
int Parent::SetMe(int val) { valueSet = val; return valueSet; }

class Child : public Parent
{
public:
    Child() {}
    ~Child() {}
    void CallMe();
};

void Child::CallMe() 
{
    cout << "Called Child" << endl;
}

int main()
{
    Child* ch = new Child();

    ch->CallMe();

    Parent* prt = dynamic_cast<Parent *>(ch);

    prt->CallMe();
}

And this is the code for the virtual override function on the Child class:

// VirtualFunctionCalls.cpp : This file contains the 'main' function. Program execution begins and ends there.
//

#include <iostream>

using namespace std;

class Parent {

protected:
    int valueSet;

public:
    Parent() { valueSet = 0; }
    ~Parent() {}

    virtual void CallMe();
    virtual int SetMe(int val);
};

void Parent::CallMe() { cout << "Called Parent" << endl; }
int Parent::SetMe(int val) { valueSet = val; return valueSet; }

class Child : public Parent
{
public:
    Child() {}
    ~Child() {}
    void CallMe() override;
};

void Child::CallMe() { cout << "Called Child" << endl; }

int main()
{
    Child* ch = new Child();

    ch->CallMe();

    ch->Parent::CallMe();
}

Now compare the assembly generated for the main() function on both cases.

Believe it or not, the assembly is way simpler/cleaner with the use of pointers as without them a lot of code stubs for the stack cookies/canaries is inserted (check the Guard Stack/GS section), another alternative is to disable the GS security feature for the examples.

0:000> uf main
SimpleInheritanceCalls!main:
00007ff7`e4f52500 4055            push    rbp
00007ff7`e4f52502 57              push    rdi
00007ff7`e4f52503 4881ec68010000  sub     rsp,168h
00007ff7`e4f5250a 488d6c2420      lea     rbp,[rsp+20h]
00007ff7`e4f5250f b904000000      mov     ecx,4
00007ff7`e4f52514 e832ebffff      call    SimpleInheritanceCalls!ILT+70(??2YAPEAX_KZ) (00007ff7`e4f5104b)
00007ff7`e4f52519 48898528010000  mov     qword ptr [rbp+128h],rax
00007ff7`e4f52520 4883bd2801000000 cmp     qword ptr [rbp+128h],0
00007ff7`e4f52528 7415            je      SimpleInheritanceCalls!main+0x3f (00007ff7`e4f5253f)  Branch

SimpleInheritanceCalls!main+0x2a:
00007ff7`e4f5252a 488b8d28010000  mov     rcx,qword ptr [rbp+128h]
00007ff7`e4f52531 e849eeffff      call    SimpleInheritanceCalls!ILT+890(??0ChildQEAAXZ) (00007ff7`e4f5137f)
00007ff7`e4f52536 48898538010000  mov     qword ptr [rbp+138h],rax
00007ff7`e4f5253d eb0b            jmp     SimpleInheritanceCalls!main+0x4a (00007ff7`e4f5254a)  Branch

SimpleInheritanceCalls!main+0x3f:
00007ff7`e4f5253f 48c7853801000000000000 mov qword ptr [rbp+138h],0

SimpleInheritanceCalls!main+0x4a:
00007ff7`e4f5254a 488b8538010000  mov     rax,qword ptr [rbp+138h]
00007ff7`e4f52551 48898508010000  mov     qword ptr [rbp+108h],rax
00007ff7`e4f52558 488b8508010000  mov     rax,qword ptr [rbp+108h]
00007ff7`e4f5255f 48894508        mov     qword ptr [rbp+8],rax
00007ff7`e4f52563 488b4d08        mov     rcx,qword ptr [rbp+8]
00007ff7`e4f52567 e899efffff      call    SimpleInheritanceCalls!ILT+1280(?CallMeChildQEAAXXZ) (00007ff7`e4f51505)
00007ff7`e4f5256c 488b4508        mov     rax,qword ptr [rbp+8]
00007ff7`e4f52570 48894528        mov     qword ptr [rbp+28h],rax
00007ff7`e4f52574 488b4d28        mov     rcx,qword ptr [rbp+28h]
00007ff7`e4f52578 e879efffff      call    SimpleInheritanceCalls!ILT+1265(?CallMeParentQEAAXXZ) (00007ff7`e4f514f6)
00007ff7`e4f5257d 33c0            xor     eax,eax
00007ff7`e4f5257f 488da548010000  lea     rsp,[rbp+148h]
00007ff7`e4f52586 5f              pop     rdi
00007ff7`e4f52587 5d              pop     rbp
00007ff7`e4f52588 c3              ret
0:000> uf main
VirtualFunctionCalls!main:
00007ff7`edf22550 4055            push    rbp
00007ff7`edf22552 57              push    rdi
00007ff7`edf22553 4881ec48010000  sub     rsp,148h
00007ff7`edf2255a 488d6c2420      lea     rbp,[rsp+20h]
00007ff7`edf2255f b910000000      mov     ecx,10h
00007ff7`edf22564 e8e2eaffff      call    VirtualFunctionCalls!ILT+70(??2YAPEAX_KZ) (00007ff7`edf2104b)
00007ff7`edf22569 48898508010000  mov     qword ptr [rbp+108h],rax
00007ff7`edf22570 4883bd0801000000 cmp     qword ptr [rbp+108h],0
00007ff7`edf22578 7415            je      VirtualFunctionCalls!main+0x3f (00007ff7`edf2258f)  Branch

VirtualFunctionCalls!main+0x2a:
00007ff7`edf2257a 488b8d08010000  mov     rcx,qword ptr [rbp+108h]
00007ff7`edf22581 e8f9edffff      call    VirtualFunctionCalls!ILT+890(??0ChildQEAAXZ) (00007ff7`edf2137f)
00007ff7`edf22586 48898518010000  mov     qword ptr [rbp+118h],rax
00007ff7`edf2258d eb0b            jmp     VirtualFunctionCalls!main+0x4a (00007ff7`edf2259a)  Branch

VirtualFunctionCalls!main+0x3f:
00007ff7`edf2258f 48c7851801000000000000 mov qword ptr [rbp+118h],0

VirtualFunctionCalls!main+0x4a:
00007ff7`edf2259a 488b8518010000  mov     rax,qword ptr [rbp+118h]
00007ff7`edf225a1 488985e8000000  mov     qword ptr [rbp+0E8h],rax
00007ff7`edf225a8 488b85e8000000  mov     rax,qword ptr [rbp+0E8h]
00007ff7`edf225af 48894508        mov     qword ptr [rbp+8],rax
00007ff7`edf225b3 488b4508        mov     rax,qword ptr [rbp+8]
00007ff7`edf225b7 488b00          mov     rax,qword ptr [rax]
00007ff7`edf225ba 488b4d08        mov     rcx,qword ptr [rbp+8]
00007ff7`edf225be ff10            call    qword ptr [rax]
00007ff7`edf225c0 488b4d08        mov     rcx,qword ptr [rbp+8]
00007ff7`edf225c4 e837efffff      call    VirtualFunctionCalls!ILT+1275(?CallMeParentUEAAXXZ) (00007ff7`edf21500)
00007ff7`edf225c9 33c0            xor     eax,eax
00007ff7`edf225cb 488da528010000  lea     rsp,[rbp+128h]
00007ff7`edf225d2 5f              pop     rdi
00007ff7`edf225d3 5d              pop     rbp
00007ff7`edf225d4 c3              ret

Assembly side by side. Highlighted the calls to functions.

Assembly side by side. Highlighted the calls to functions.

Here we have 4 calls in total, these are:

  1. Call to new/malloc
  1. Call to constructor
  1. Call to CallMe on Child class
  1. Call to CallMe on Parent class

As you can notice the assembly is almost the same for almost half of the code, we start to see it differ shortly after the call to the constructor (which is a non-virtual constructor in both cases) and as soon as we approach to the code holding the polymorphic calls.

The usual first step when calling a function for code compiled in Debug mode for VS is going through the ILT (which for this specific scenario means Incremental Link Table), this step won’t be present for Release mode builds.

I will ignore this step from now on, so pay attention if your plan is to replicate this scenario.

ILT, is just a table with JMP instructions to the real function calls. Used in debug mode for faster build speed.

ILT, is just a table with JMP instructions to the real function calls. Used in debug mode for faster build speed.

Once we get into the Child constructor we can see some differences and the first appearance of a reference to the VTable of our class with virtual methods.

0:000> uf VirtualFunctionCalls!Child::Child
VirtualFunctionCalls!Child::Child:
00007ff7`edf21df0 48894c2408      mov     qword ptr [rsp+8],rcx
00007ff7`edf21df5 55              push    rbp
00007ff7`edf21df6 57              push    rdi
00007ff7`edf21df7 4881ece8000000  sub     rsp,0E8h
00007ff7`edf21dfe 488d6c2420      lea     rbp,[rsp+20h]
00007ff7`edf21e03 488b8de0000000  mov     rcx,qword ptr [rbp+0E0h]
00007ff7`edf21e0a e8f6f6ffff      call    VirtualFunctionCalls!ILT+1280(??0ParentQEAAXZ) (00007ff7`edf21505)
00007ff7`edf21e0f 488b85e0000000  mov     rax,qword ptr [rbp+0E0h]
00007ff7`edf21e16 488d0d8ba00000  lea     rcx,[VirtualFunctionCalls!Child::`vftable' (00007ff7`edf2bea8)]
00007ff7`edf21e1d 488908          mov     qword ptr [rax],rcx
00007ff7`edf21e20 488b85e0000000  mov     rax,qword ptr [rbp+0E0h]
00007ff7`edf21e27 488da5c8000000  lea     rsp,[rbp+0C8h]
00007ff7`edf21e2e 5f              pop     rdi
00007ff7`edf21e2f 5d              pop     rbp
00007ff7`edf21e30 c3              ret
0:000> uf SimpleInheritanceCalls!Child::Child
SimpleInheritanceCalls!Child::Child:
00007ff7`e4f51f70 48894c2408      mov     qword ptr [rsp+8],rcx
00007ff7`e4f51f75 55              push    rbp
00007ff7`e4f51f76 57              push    rdi
00007ff7`e4f51f77 4881ece8000000  sub     rsp,0E8h
00007ff7`e4f51f7e 488d6c2420      lea     rbp,[rsp+20h]
00007ff7`e4f51f83 488b8de0000000  mov     rcx,qword ptr [rbp+0E0h]
00007ff7`e4f51f8a e871f5ffff      call    SimpleInheritanceCalls!ILT+1275(??0ParentQEAAXZ) (00007ff7`e4f51500)
00007ff7`e4f51f8f 488b85e0000000  mov     rax,qword ptr [rbp+0E0h]
00007ff7`e4f51f96 488da5c8000000  lea     rsp,[rbp+0C8h]
00007ff7`e4f51f9d 5f              pop     rdi
00007ff7`e4f51f9e 5d              pop     rbp
00007ff7`e4f51f9f c3              ret

Side by side comparison of constructors, see how some extra work is performed to deal with the VTable

Side by side comparison of constructors, see how some extra work is performed to deal with the VTable

So what’s the deal with those extra instructions? In the case of the class with a VTable we need to set it as the first element of our just initialized pointer to the object, this will happen on every constructor of the inheritance chain so first Parent will set it’s VTable when called from Child’s constructor and once the Child constructor regains control it will be overwrite with Childs VTable.

Remember that we return values on the RAX register and that’s why we are moving the this pointer aka RBP+0xE0 to RAX before returning.

Let’s check the layout for the objects in both cases, first the example with virtual methods:

0:000> x VirtualFunctionCalls!*Parent*
00007ff7`edf2bda8 VirtualFunctionCalls!Parent::`vftable' = <function> *[3]
00007ff7`edf223c0 VirtualFunctionCalls!Parent::SetMe (int)
00007ff7`edf22380 VirtualFunctionCalls!Parent::CallMe (void)
00007ff7`edf21e40 VirtualFunctionCalls!Parent::Parent (void)

0:000> dps VirtualFunctionCalls!Parent::`vftable' l2
00007ff7`edf2bda8  00007ff7`edf21500 VirtualFunctionCalls!ILT+1275(?CallMeParentUEAAXXZ)
00007ff7`edf2bdb0  00007ff7`edf214f6 VirtualFunctionCalls!ILT+1265(?SetMeParentUEAAHHZ)

0:000> x VirtualFunctionCalls!*Child*
00007ff7`edf2bea8 VirtualFunctionCalls!Child::`vftable' = <function> *[3]
00007ff7`edf21df0 VirtualFunctionCalls!Child::Child (void)
00007ff7`edf222a0 VirtualFunctionCalls!Child::CallMe (void)

0:000> dps VirtualFunctionCalls!Child::`vftable' l2
00007ff7`edf2bea8  00007ff7`edf214fb VirtualFunctionCalls!ILT+1270(?CallMeChildUEAAXXZ)
00007ff7`edf2beb0  00007ff7`edf214f6 VirtualFunctionCalls!ILT+1265(?SetMeParentUEAAHHZ)

You can see how the methods are labelled in a way that you can easily tell which version of the overloaded function will be called by each class, for example in both cases the same “SetMe” function will be called, but each object has it’s own pointer for the “CallMe” function.

Now the output for the simple inheritance case:

0:000> x SimpleInheritanceCalls!*Parent*
00007ff7`e4f523c0 SimpleInheritanceCalls!Parent::SetMe (int)
00007ff7`e4f52380 SimpleInheritanceCalls!Parent::CallMe (void)
00007ff7`e4f51fb0 SimpleInheritanceCalls!Parent::Parent (void)

0:000> x SimpleInheritanceCalls!*Child*
00007ff7`e4f51f70 SimpleInheritanceCalls!Child::Child (void)
00007ff7`e4f52290 SimpleInheritanceCalls!Child::CallMe (void)

As you can see in this case there is no VTable at all as you need at least 1 virtual method in order to force the compiler to generate one, so the code will always jump right into the function being called with no intermediate steps.

Now move into the Parent constructor for both cases:

0:000> uf VirtualFunctionCalls!Parent::Parent
VirtualFunctionCalls!Parent::Parent:
00007ff7`edf21e40 48894c2408      mov     qword ptr [rsp+8],rcx
00007ff7`edf21e45 55              push    rbp
00007ff7`edf21e46 57              push    rdi
00007ff7`edf21e47 4881ecc8000000  sub     rsp,0C8h
00007ff7`edf21e4e 488bec          mov     rbp,rsp
00007ff7`edf21e51 488b85e0000000  mov     rax,qword ptr [rbp+0E0h]
00007ff7`edf21e58 488d0d499f0000  lea     rcx,[VirtualFunctionCalls!Parent::`vftable' (00007ff7`edf2bda8)]
00007ff7`edf21e5f 488908          mov     qword ptr [rax],rcx
00007ff7`edf21e62 488b85e0000000  mov     rax,qword ptr [rbp+0E0h]
00007ff7`edf21e69 c7400800000000  mov     dword ptr [rax+8],0
00007ff7`edf21e70 488b85e0000000  mov     rax,qword ptr [rbp+0E0h]
00007ff7`edf21e77 488da5c8000000  lea     rsp,[rbp+0C8h]
00007ff7`edf21e7e 5f              pop     rdi
00007ff7`edf21e7f 5d              pop     rbp
00007ff7`edf21e80 c3              ret
0:000> uf SimpleInheritanceCalls!Parent::Parent
SimpleInheritanceCalls!Parent::Parent:
00007ff7`e4f51fb0 48894c2408      mov     qword ptr [rsp+8],rcx
00007ff7`e4f51fb5 55              push    rbp
00007ff7`e4f51fb6 57              push    rdi
00007ff7`e4f51fb7 4881ecc8000000  sub     rsp,0C8h
00007ff7`e4f51fbe 488bec          mov     rbp,rsp
00007ff7`e4f51fc1 488b85e0000000  mov     rax,qword ptr [rbp+0E0h]
00007ff7`e4f51fc8 c70000000000    mov     dword ptr [rax],0
00007ff7`e4f51fce 488b85e0000000  mov     rax,qword ptr [rbp+0E0h]
00007ff7`e4f51fd5 488da5c8000000  lea     rsp,[rbp+0C8h]
00007ff7`e4f51fdc 5f              pop     rdi
00007ff7`e4f51fdd 5d              pop     rbp
00007ff7`e4f51fde c3              ret

Side by side comparison between Parent constructors for both samples, again we see the extra instructions to copy the VTable in the class with virtual methods.

Side by side comparison between Parent constructors for both samples, again we see the extra instructions to copy the VTable in the class with virtual methods.

The only difference here again is the copy of the VTable to the just initialized instance, in both cases we can see how the local variable valueSet is initialized to 0 with one minor difference for the case of the class with virtual methods the location of valueSet is 8 bytes ahead of the start of the instance as we always have the VTable pointer as the first element. Notice the use of “[rax+8], 0” vs just “[rax], 0” on the right.

Going back to main after calling the constructors, we have to deal with storing the return of the constructor into the “ch” variable.

Next time we will see how the virtual function calls differ from the ones with simple inheritance.


메타데이터
post_id
c80fca4845b1
slug
the-asm-flow-of-a-c-class-with-virtual-methods-part-1-constructors-c80fca4845b1
url
https://medium.com/@dastuam/the-asm-flow-of-a-c-class-with-virtual-methods-part-1-constructors-c80fca4845b1
canonical_url
https://medium.com/@dastuam/the-asm-flow-of-a-c-class-with-virtual-methods-part-1-constructors-c80fca4845b1
author_url
https://medium.com/@dastuam
status
ok
fetched_at
2026-07-26 10:11:49