代码如下:
class A{
public:
A(){_a = 0x00;}
virtual ~A(){}
virtual void printA(){}
virtual A* clone()const{return NULL;}
int _a;
};
class B{
public:
B(){_b = 0x11;}
virtual ~B(){}
virtual void printB(){}
virtual B* clone()const{return NULL;}
int _b;
};
class C:public A, public B{
public:
C(){_c = 0x22;}
virtual ~C(){}
virtual C* clone()const{return new C;}
int _c;
};
int _tmain()
{
A a;
B b;
C c;
B* pb = &c;
B* pc = pb->clone();
}
这里的关键是clone函数,它的返回值是指向C类型对象的指针,如果要赋给B*指针的话,则返回指针应该加上8,移指向C对象中的B子对象,那这个调整是在哪完成的了?
首先看一下C对象的布局:
__vfptr |
_a |
__vfptr |
_b |
_c |
红色为C对象中的A子对象,绿色为C对象中的B子对象。
当将c的地址赋给pb时,编译器会将c的地址减8,所以pb实际指向的是绿色部分。
接下来就是进行函数调用了,标红语句会被翻译成如下形式:
(*pb->__vfptr[1])(this);假设clone在虚函数表中的索引为1
由于多态性,该条语句实际要调用的代码是C::clone。所以this的地址是要调整的,在该例中,this需要减8以指向实际的C对象。又因为B::clone的函数原型,返回值也需要调整,VC通过对虚函数插入一个包装函数来做这些工作。下面是this指针调整的函数:
[thunk]:C::clone`adjustor{8}':这个函数是为了进行this调整为了在给类C的虚函数加的包装
0042EF50 sub ecx,8 编译器知道在C对象中由第2个基类指针调用时,this指针要上移8个字节
0042EF53 jmp C::clone (42C31Bh)
然而,返回值也是需要调整的,因为按B中clone的原型,C::clone的返回值需要+8,所以上面jmp跳转的仍然是一个包装函数:
C::clone:
0042EF60 push ebp
0042EF61 mov ebp,esp
0042EF63 sub esp,0D4h
0042EF69 push ebx
0042EF6A push esi
0042EF6B push edi
0042EF6C push ecx
0042EF6D lea edi,[ebp-0D4h]
0042EF73 mov ecx,35h
0042EF78 mov eax,0CCCCCCCCh
0042EF7D rep stos dword ptr es:[edi]
0042EF7F pop ecx 取出this参数
0042EF80 mov dword ptr [ebp-8],ecx
0042EF83 mov ecx,dword ptr [this]
0042EF86 call C::clone (42CD43h) 调用函数生成C对象,返回地址在eax中
0042EF8B mov dword ptr [ebp-0D0h],eax 这是clone出得C的地址
0042EF91 cmp dword ptr [ebp-0D0h],0
0042EF98 je C::clone+4Bh (42EFABh)
0042EF9A mov eax,dword ptr [ebp-0D0h]
0042EFA0 add eax,8 关键点,对返回值进行调整
0042EFA3 mov dword ptr [ebp-0D4h],eax
0042EFA9 jmp C::clone+55h (42EFB5h)
0042EFAB mov dword ptr [ebp-0D4h],0
0042EFB5 mov eax,dword ptr [ebp-0D4h]
0042EFBB pop edi
0042EFBC pop esi
0042EFBD pop ebx
0042EFBE add esp,0D4h
0042EFC4 cmp ebp,esp
0042EFC6 call @ILT+3560(__RTC_CheckEsp) (42CDEDh)
0042EFCB mov esp,ebp
0042EFCD pop ebp
0042EFCE ret
ok,通过这两步调整,函数成功调用。