• 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内存低地址。

    解析如下:

    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


    可见初始化大小为0x33 = 51个dword ptr大小。dword ptr即双字指针,这里32位机即4字节指针

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

    看下虚表地址0E27B34h有什么

    找到内存0E27B34h位置,前4个字节ad 12 e2 00大小端转换下即0x00e212ad,我们看下这个位置是什么

    可以看出是一条jmp指令用于跳转到Base::Output (0E21810h)地址

    即虚表指针指向一块内存地址(虚表的位置),虚表中存放的是一些跳转指令,跳转指令跳转到对应的虚函数实现的位置,进行调用。
    关于内存0E27B34h第二个双字的位置,有00 00 00 00,这里应该虚表大小结束标志,就像指针数组使用NULL作为有效数据的结束标记一样。这个暂时不深究。

    先不管虚函数的执行。再来看看拷贝后的基类的this中虚表指针位置有啥变化,回到Base::Base拷贝虚表后的0x00E21786位置
    指向拷贝虚表指针前

    执行后

    由上面的图可知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的

    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个字节,但在反汇编中并没有体现,这里也不再研究汇编代码。

  • 相关阅读:
    java中getResourceAsStream的问题 缓存
    springside 查询条件
    如何修改MySQL数据库引擎为INNODB
    获取用户真实 IP , 淘宝IP接口获得ip地理位置
    hibernate注解的CascadeType属性
    AssignmentHandler实现类用spring注入
    使用 jQuery dataTables
    转载:一致性 hash 算法( consistent hashing )
    转载:memcached完全剖析
    转载:连接MySQL数据库的两种方式介绍
  • 原文地址:https://www.cnblogs.com/kuikuitage/p/12341038.html
Copyright © 2020-2023  润新知