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