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

D-A
7 min readDec 2, 2022

--

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.

Here we have 4 calls in total, these are:

1. Call to new/malloc

2. Call to constructor

3. Call to CallMe on Child class

4. 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.

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 to 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.

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.

--

--

D-A

Writing tech stuff about my different working experiences (low level Windows, Linux, Embedded and now learning about Web3)