The ASM flow of a C++ Class with virtual methods — Part 2 Virtual Function Calls

D-A
4 min readDec 3, 2022

--

We are going to be using the same code as in the previous section and now will continue the code at the place we left it the last time.

// 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();
}
// 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();
}
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
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
Side to side comparison of the assembly code for main, highlighted the calls to member functions

So let’s continue

Notice from the code how before every call to a C++ member function the pointer to “this” is passed implicitly as a first argument through the register RCX (if not sure what this means, take a look at the X64 calling conventions for Windows AKA: Windows ABI and this on C++).

For the case of the the class with virtual methods, we can see how the calls happen after loading a pointer and dereferencing it to make a call, when the call happens the value pointed by the RAX register is equal to the head of the VTable for Child, as the declaration “CallMe” is before than “SetMe” in the source it is also the first function on the VTable (would expect this also follows the C/C++ layout rules for structs).

0:000> r
rax=00007ff7edf2bea8 rbx=0000000000000000 rcx=000001e0b135c710
rdx=cdcdcdcdcdcdcdcd rsi=0000000000000000 rdi=0000000000000000
rip=00007ff7edf225be rsp=00000032e79df880 rbp=00000032e79df8a0
r8=0000000000000010 r9=00007ffd91ca0508 r10=0000000000000000
r11=000001e0b135c710 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei pl nz na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
VirtualFunctionCalls!main+0x6e:
00007ff7`edf225be ff10 call qword ptr [rax] ds:00007ff7`edf2bea8={VirtualFunctionCalls!ILT+1270(?CallMeChildUEAAXXZ) (00007ff7`edf214fb)}
0:000> dqs 00007ff7edf2bea8 l2
00007ff7`edf2bea8 00007ff7`edf214fb VirtualFunctionCalls!ILT+1270(?CallMeChildUEAAXXZ)

00007ff7`edf2beb0 00007ff7`edf214f6 VirtualFunctionCalls!ILT+1265(?SetMeParentUEAAHHZ)

We go through the ILT (incremental link table, debug only), and end up on the “CallMe” function which is practically identical for both.

Notice how the call to the “Parent CallMe” function happens in the same way for both?

Seems that as Parent is not inheriting from anyone the address for the “CallMe” function can be inferred statically at compile time and no pointer manipulation/dereferencing is required.

This part was way simpler than the constructors, right?

But you did saw the extra steps required after the virtual function call, this is the well known performance penalty that comes form polymorphic code.

It is a coincidence that the code for both main methods has the same number of instructions, but remember that we create an extra variable for the code that uses simple inheritance so these extra instructions are the ones that make the simple inheritance code equal in number of instructions (extra pointer declaration and the dynamic_cast<>).

We may revisit this later to see how does it works on more complex classes, like the ones part of a bigger chain of inheritance.

--

--

D-A

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