The object virtual function call compiled under VS resolve

Compiler for VS2017

Look at a simple virtual inheritance

#include <stdio.h>

class Base {

public:
    virtual void __stdcall Output() {
        printf("Class Base/n");
    }
};

class Derive : public Base {
public:
    void __stdcall Output() {
        printf("Class Derive/n");
    }
};

void Test(Base *p) {
    p->Output();
}

int __cdecl main(int argc, char* argv[]) {
    Derive obj;
    Test(&obj);
    return 0;
}

After disassembly process under execution trace
must first clear the stack address is descending. Bottom of the stack base address high memory address ebp, esp stack memory lower address.

Interpreted as follows:

00E219F0  push        ebp      
//即将上层函数在调用main前的基址指针寄存器ebp压栈
00E219F1  mov         ebp,esp  
//更新main的栈基址ebp为原栈顶esp
00E219F3  sub         esp,0CCh 
//新的栈顶在原栈顶下移0xCCh,至于为何大小是0xCCh待研究。
00E219F9  push        ebx  
00E219FA  push        esi  
00E219FB  push        edi  
//保存相关栈的原始数据,先压栈,并在main函数结束前需要弹栈进行恢复
00E219FC  lea         edi,[ebp-0CCh]  
//因为栈大小是0xCCh,把ebp-0CCh即esp寄存器的值加载到edi中,edi存的是esp在push三个寄存器前的地址。
00E21A02  mov         ecx,33h         
//ecx是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器
00E21A07  mov         eax,0CCCCCCCCh  
//INT 3指令的目的就是使CPU中断(break)到调试器,其机器码就是我们熟悉的0XCC, 在调试时,防止编译器把栈上的内容当作指令来执行。一旦编译器执行了0XCC,就会产生INT3中断,这里把eax设定为0xCCCCCCCCh
00E21A0C  rep stos    dword ptr es:[edi]
//rep指令的目的是重复其上面的指令.ECX的值是重复的次数.STOS指令的作用是将eax中的值拷贝到以指针ES:EDI(如ES=023H为段选择子,EDI=12EAB5H为线形地址偏移,经段描述符后,变为线性地址,再经分页机制,转为物理地址)指向的地址,如果设置了标志位DF(direction flag), 那么edi会在该指令执行后减小, 如果没有设置, 那么edi的值会增加,并根据对寄存器DI作相应增减。该指令不影响任何标志位。执行完此指令,栈区的0xCC = 0x33 * 4(dword ptr)长度的main函数栈空间为0xCC

Why stack initialization value 0XCC


Initialization visible size 0x33 = 51 th dword ptr size. dword ptr i.e. double word pointer, here 32-bit machine, ie 4 byte pointer

00E21A0E  lea         ecx,[obj]  
//obj的地址放到ecx寄存器
00E21A11  call        Derive::Derive (0E2130Ch)   
//调用Derive::Derive构造




00E217B0  push        ebp  
00E217B1  mov         ebp,esp  
00E217B3  sub         esp,0CCh  
00E217B9  push        ebx  
00E217BA  push        esi  
00E217BB  push        edi 
//更新Derive::Derive函数栈,及保存main现场,
00E217BC  push        ecx  
//ecx寄存器值压栈,因为后面要用到这个计数寄存器,由上面可知ecx实际保存的是obj的地址即0x0075FE7C
00E217BD  lea         edi,[ebp-0CCh]  
00E217C3  mov         ecx,33h  
00E217C8  mov         eax,0CCCCCCCCh  
00E217CD  rep stos    dword ptr es:[edi]  
//同前面初始化Derive::Derive函数栈
00E217CF  pop         ecx  
//弹栈还原ecx为obj的地址
00E217D0  mov         dword ptr [this],ecx  
//用ecx即obj的地址给this赋值,这里操作过程中因为重新运行了,导致各值变化了,运行此行前,this值为的值0xcccccccc执行此行后,this值变为obj的地址
00E217D3  mov         ecx,dword ptr [this]  
//再把this值给ecx
00E217D6  call        Base::Base (0E21037h)  
//调用 Base::Base


00E21760  push        ebp  
00E21761  mov         ebp,esp  
00E21763  sub         esp,0CCh  
00E21769  push        ebx  
00E2176A  push        esi  
00E2176B  push        edi  
00E2176C  push        ecx  
00E2176D  lea         edi,[ebp-0CCh]  
00E21773  mov         ecx,33h  
00E21778  mov         eax,0CCCCCCCCh  
00E2177D  rep stos    dword ptr es:[edi]  
00E2177F  pop         ecx
//上面这些之前已经讲过,不再提,ecx此时存的依然是obj的this地址即Derived子类对象的地址
00E21780  mov         dword ptr [this],ecx  
//这里因为单继承子类的开始位置同时也是基类的开始位置,这行将ecx的值存到基类对象的this
00E21783  mov         eax,dword ptr [this]  
//这行将基类this值复制到eax寄存器
00E21786  mov         dword ptr [eax],offset Base::`vftable' (0E27B34h)  
//上面这行就是保存虚表指针的核心代码,即将0E27B34h拷贝到this地址的前双字4字节中,即基类对象的首地址
00E2178C  mov         eax,dword ptr [this]
//将基类的this重新放回eax,eax通常也用来存放函数返回值。

Virtual address table look what 0E27B34h

Found 0E27B34h memory location, the first four bytes next ad 12 e2 00 Endian conversion i.e. 0x00e212ad, we look at this position is it

may be seen that a jmp instruction for jumping to Base: : Output (0E21810h) address

that is a virtual table pointer to the memory address (the position of the virtual table), is stored in the virtual table number of jump instruction, the jump instruction to jump to a position corresponding to the virtual function implemented call.
0E27B34h memory regarding the position of the second double word, there are 00 million, there should be dummy end flag table size, as a NULL pointer array mark the end of valid data is the same. This is not a temporary bottom.

Regardless of the implementation of the first virtual function. 0x00E21786 look at this position of the base class virtual table copy pointer position so what changes back to Base :: Base virtual table after the copy
before the copy vtable pointer points

after performing

the address 0x00e27b34 vfptr is apparent from the above FIG.
vfptr value of this pointer to this base class is set well.

After this address back to the base class constructor subclass copying eax recovery function Derive :: Derive site, and returns to the ret Derive :: Derive.

00E2178F  pop         edi  
00E21790  pop         esi  
00E21791  pop         ebx  
00E21792  mov         esp,ebp  
00E21794  pop         ebp  
00E21795  ret

Back to Derive :: Derive the

00E217DB  mov         eax,dword ptr [this]  
//eax本来是存了Base::Base的基类this地址的,这里因为简单的单继承,基类和子类的this地址是一样的,拿子类的this覆盖了eax的值
00E217DE  mov         dword ptr [eax],offset Derive::`vftable' (0E27B50h)  
//拿子类的虚表地址放到eax寄存器值子类对象this指向的地址的前4个字节。
00E217E4  mov         eax,dword ptr [this] 
//子类对象的this作为返回值放到eax。

我们再看下子类的虚表地址0E27B50h存放了什么

f9 11 e2 00改成0x00e211f9

即子类Output的实现位置

至此完成了基类子类构造基类构造,基类构造初始化vfptr和子类对象初始化vfptr的过程。

接下来我们看下虚函数的调用过程,如上obj的虚表已经被初始化而且是Derived::Output的jmp地址,继续运行到

00E21A16  lea         eax,[obj]  
00E21A19  push        eax  
//指针参数obj压栈
00E21A1A  call        Test (0E2123Ah)  
调用Test函数
00E21A1F  add         esp,4 
//加4意思是从堆栈中推出4个字节,这里是因为在main调用Test之前有eax即obj地址压栈,这里相当于回收这占用的栈空间

清栈操作后面再说。进入到Test看下

00E218EE  mov         eax,dword ptr [p]  
//p的内容及obj对象的首地址存到eax
00E218F1  mov         ecx,dword ptr [eax]
//取eax即obj对象的前4个字节就是vfptr的值存到ecx
    00E218F3  mov         esi,esp  
    //esp暂存到esi,堆栈平衡检查后面
00E218F5  mov         edx,dword ptr [p]  
//p的值存到edx
00E218F8  push        edx 
//edx压栈
00E218F9  mov         eax,dword ptr [ecx] 
// 将ecx寄存器中的vfptr值指向的地址的前4个字节即虚表中的Derived::Output的跳转指令地址给eax
00E218FB  call        eax  
//调用eax即调用jmp
    00E218FD  cmp         esi,esp 
    //堆栈平衡检查
    00E218FF  call        __RTC_CheckEsp (0E21131h) 
    //堆栈平衡检查



对照前面的图,
EAX = 00E211F9即

EDX = 0019F9E8即p的值也是obj的地址

我们看Test的右扩号还执行了一些操作,包括弹栈,恢复现场操作。

但在这之前发现后压栈的push edx并没有弹栈
进一步跟踪发现call eax 前后栈esp信息如下

可见在调用后从call eax 退出时,多弹了4个字节,但在反汇编中并没有体现,这里也不再研究汇编代码。

Guess you like

Origin www.cnblogs.com/kuikuitage/p/12341038.html