VS下对象虚函数调用汇编解析

编译器为VS2017

先看一个简单的虚继承

#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;
}

反汇编后跟踪下执行流程
首先要明确栈地址是从高到低的。栈底基址ebp内存高地址,栈顶esp内存低地址。
VS下对象虚函数调用汇编解析
解析如下:

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

为何栈的初始化值为0XCC

VS下对象虚函数调用汇编解析
可见初始化大小为0x33 = 51个dword ptr大小。dword ptr即双字指针,这里32位机即4字节指针

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

VS下对象虚函数调用汇编解析
VS下对象虚函数调用汇编解析
VS下对象虚函数调用汇编解析
VS下对象虚函数调用汇编解析

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

VS下对象虚函数调用汇编解析
VS下对象虚函数调用汇编解析

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通常也用来存放函数返回值。

看下虚表地址0E27B34h有什么
VS下对象虚函数调用汇编解析
找到内存0E27B34h位置,前4个字节ad 12 e2 00大小端转换下即0x00e212ad,我们看下这个位置是什么
VS下对象虚函数调用汇编解析
可以看出是一条jmp指令用于跳转到Base::Output (0E21810h)地址
VS下对象虚函数调用汇编解析
即虚表指针指向一块内存地址(虚表的位置),虚表中存放的是一些跳转指令,跳转指令跳转到对应的虚函数实现的位置,进行调用。
关于内存0E27B34h第二个双字的位置,有00 00 00 00,这里应该虚表大小结束标志,就像指针数组使用NULL作为有效数据的结束标记一样。这个暂时不深究。

先不管虚函数的执行。再来看看拷贝后的基类的this中虚表指针位置有啥变化,回到Base::Base拷贝虚表后的0x00E21786位置
指向拷贝虚表指针前
VS下对象虚函数调用汇编解析
执行后
VS下对象虚函数调用汇编解析
由上面的图可知0x00e27b34就是vfptr的地址
至此基类的this指针中的vfptr的值设定好了。

后面把基类的this地址拷到eax后恢复子类构造Derive::Derive函数现场,并ret返回到Derive::Derive。

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

回到Derive::Derive的
VS下对象虚函数调用汇编解析

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存放了什么
VS下对象虚函数调用汇编解析
f9 11 e2 00改成0x00e211f9
VS下对象虚函数调用汇编解析
即子类Output的实现位置
VS下对象虚函数调用汇编解析

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

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

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

清栈操作后面再说。进入到Test看下
VS下对象虚函数调用汇编解析

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) 
    //堆栈平衡检查

VS下对象虚函数调用汇编解析
VS下对象虚函数调用汇编解析
对照前面的图,
EAX = 00E211F9即
VS下对象虚函数调用汇编解析
EDX = 0019F9E8即p的值也是obj的地址

我们看Test的右扩号还执行了一些操作,包括弹栈,恢复现场操作。
VS下对象虚函数调用汇编解析
但在这之前发现后压栈的push edx并没有弹栈
进一步跟踪发现call eax 前后栈esp信息如下
VS下对象虚函数调用汇编解析
可见在调用后从call eax 退出时,多弹了4个字节,但在反汇编中并没有体现,这里也不再研究汇编代码。

原文链接: https://www.cnblogs.com/kuikuitage/p/12341038.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    VS下对象虚函数调用汇编解析

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/331306

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年3月1日 下午5:59
下一篇 2023年3月1日 下午6:00

相关推荐