#include <cstdio>
class B
{
private:
int x;
public:
B(int i) :x(i) {}
virtual void printinfo() const
{
printnum();
printf("Base\n");
}
virtual void printnum() const
{
printf("%d\n", x);
}
};
class D : public B
{
private:
int y;
public:
D(int i, int j) :B(i), y(j){}
virtual void printinfo() const
{
printnum();
printf("Derived\n");
}
};
void PutInfo(const B &b)
{
b.printinfo();
}
int main()
{
B b(1);
D d(2, 3);
printf("\tB\tD\nsize:\t%d\t%d\n", sizeof b, sizeof d);
printf("addr:\t%x\t%x\n", &b, &d);
putchar('\n');
PutInfo(b);
PutInfo(d);
return 0;
}
使用VS2013生成,Release模式,无优化,运行结果如下:
18f918和18f90c这两个是变量b,d的地址,每次运行都可能不同。
由于B类有虚函数,因为会有一个虚表指针(4字节),加上一个int成员变量x(4字节),因此大小为8字节。
D类是B类派生类,在B类基础上,增加了一个int成员变量y,所以大小为12字节。
一些基本的约定:
Windows栈从高地址向低地址增长,栈底地址高于栈顶地址,局部变量按声明顺序从高地址向低地址存放。
__cdecl函数调用方式:一般来说,函数参数从右向左依次入栈,然后是返回地址入栈,然后是ebp入栈,再将ebp设为栈顶指针esp,用于访问数据,esp减小,开辟栈空间以保存局部变量。
具体汇编代码如下:
; int __cdecl main(int argc, const char **argv, const char **envp)
_main proc near
d= D ptr -14h
b= B ptr -8
argc= dword ptr 8
argv= dword ptr 0Ch
envp= dword ptr 10h
push ebp
mov ebp, esp
sub esp, 14h
push 1 ; i
lea ecx, [ebp+b] ; this
call ??0B@@QAE@H@Z ; B::B(int)
push 3 ; j
push 2 ; i
lea ecx, [ebp+d] ; this
call ??0D@@QAE@HH@Z ; D::D(int,int)
push 0Ch
push 8
push offset aBDSizeDD ; "\tB\tD\nsize:\t%d\t%d\n"
call ds:__imp__printf
add esp, 0Ch
lea eax, [ebp+d]
push eax
lea ecx, [ebp+b]
push ecx
push offset aAddrXX ; "addr:\t%x\t%x\n"
call ds:__imp__printf
add esp, 0Ch
push 0Ah ; Ch
call ds:__imp__putchar
add esp, 4
lea edx, [ebp+b]
push edx ; b
call ?PutInfo@@YAXABVB@@@Z ; PutInfo(B const &)
add esp, 4
lea eax, [ebp+d]
push eax ; b
call ?PutInfo@@YAXABVB@@@Z ; PutInfo(B const &)
add esp, 4
xor eax, eax
mov esp, ebp
pop ebp
retn
_main endp
sub esp, 14h
为局部变量分配空间,b和d一共20字节,因此esp减去14h。
push 1 ; i
lea ecx, [ebp+b] ; this
call ??0B@@QAE@H@Z ; B::B(int)
1入栈,同时ecx = &b。
lea reg, mem的作用是将mem的地址传递给reg寄存器,类似于reg = &mem,[reg]是取地址为reg的数据,类似于*reg,所以
lea ecx, [ebp+b]
将ebp-8赋给ecx,ebp-8就是b的地址,接下来调用B的构造函数B::B(int)。
; void __thiscall B::B(B *this, int i)
??0B@@QAE@H@Z proc near
this= dword ptr -4
i= dword ptr 8
push ebp
mov ebp, esp
push ecx
mov [ebp+this], ecx
mov eax, [ebp+this]
mov dword ptr [eax], offset ??_7B@@6B@ ; const B::`vftable'
mov ecx, [ebp+this]
mov edx, [ebp+i]
mov [ecx+4], edx
mov eax, [ebp+this]
mov esp, ebp
pop ebp
retn 4
??0B@@QAE@H@Z endp
从代码可以看到,成员函数中其实是有一个隐藏的参数this,用于保存对象地址。
mov [ebp+this], ecx
表明this指针通过ecx寄存器传递,在B::B(int)函数中,形成的stack是这样的:
mov eax, [ebp+this]
mov dword ptr [eax], offset ??_7B@@6B@ ; const B::`vftable'
这两句将b的地址&b复制到eax,并将B类的虚表地址复制到&b处。
mov ecx, [ebp+this]
mov edx, [ebp+i]
mov [ecx+4], edx
这里再将ecx设为&b,edx设为ebp+8中的数据,即参数i的值1,再将edx复制到&b+4处,即成员变量b.x被初始化为1。
push 3 ; j
push 2 ; i
lea ecx, [ebp+d] ; this
call ??0D@@QAE@HH@Z ; D::D(int,int)
将D类构造函数的参数压入栈中,ecx = &d,调用D::D(int, int)。
; void __thiscall D::D(D *this, int i, int j)
??0D@@QAE@HH@Z proc near
this= dword ptr -4
i= dword ptr 8
j= dword ptr 0Ch
push ebp
mov ebp, esp
push ecx
mov [ebp+this], ecx
mov eax, [ebp+i]
push eax ; i
mov ecx, [ebp+this] ; this
call ??0B@@QAE@H@Z ; B::B(int)
mov ecx, [ebp+this]
mov dword ptr [ecx], offset ??_7D@@6B@ ; const D::`vftable'
mov edx, [ebp+this]
mov eax, [ebp+j]
mov [edx+8], eax
mov eax, [ebp+this]
mov esp, ebp
pop ebp
retn 8
??0D@@QAE@HH@Z endp
this = &d,调用基类构造函数B::B(int),初始化d.x,然后D类虚表地址复制到&d,再初始化d.y。
看一下虚表中的数据:
.rdata:00402164 ; const B::`vftable'
.rdata:00402164 30 10 40 00 ??_7B@@6B@ dd offset ?printinfo@B@@UBEXXZ
.rdata:00402164 ; DATA XREF: B::B(int)+Ao
.rdata:00402164 ; B::printinfo(void)
.rdata:00402168 60 10 40 00 dd offset ?printnum@B@@UBEXXZ ; B::printnum(void)
按照成员函数声明的顺序,B类虚表的第一项是B::printinfo(void)地址,第二项是B::printnum(void)地址。
D类虚表类似:
.rdata:00402158 ; const D::`vftable'
.rdata:00402158 B0 10 40 00 ??_7D@@6B@ dd offset ?printinfo@D@@UBEXXZ
.rdata:00402158 ; DATA XREF: D::D(int,int)+16o
.rdata:00402158 ; D::printinfo(void)
.rdata:0040215C 60 10 40 00 dd offset ?printnum@B@@UBEXXZ ; B::printnum(void)
注意到由于派生类D的printinfo覆盖了基类的printinfo,但并未对B::printnum(void)进行覆盖,所以虚表第一项是D::printinfo(void)地址,第二项仍然是B::printnum(void)的地址。
再看
PutInfo(b);
PutInfo(d);
对应的汇编:
lea edx, [ebp+b]
push edx ; b
call ?PutInfo@@YAXABVB@@@Z ; PutInfo(B const &)
add esp, 4
lea eax, [ebp+d]
push eax ; b
call ?PutInfo@@YAXABVB@@@Z ; PutInfo(B const &)
add esp, 4
lea edx, [ebp+b]
push edx ; b
edx = &b,然后edx入栈,调用PutInfo(B const &)。
PutInfo(const B &)代码:
; void __cdecl PutInfo(B *b)
?PutInfo@@YAXABVB@@@Z proc near
b= dword ptr 8
push ebp
mov ebp, esp
mov eax, [ebp+b]
mov edx, [eax]
mov ecx, [ebp+b]
mov eax, [edx]
call eax
pop ebp
retn
?PutInfo@@YAXABVB@@@Z endp
左图为main形成的stack,右图为PutInfo(b)形成的stack,
在PutInfo(b)中,
eax = &b
edx = offset B::vftable
ecx = &b
eax = B::vftable = offset B::printinfo(void)。
然后call eax调用B::printinfo(void)。
类似地,在PutInfo(d)中,由于虚表第一项是offset D::printinfo(void),因此call eax会调用D::printinfo(void)。
于是,对象在运行时根据虚表中保存的函数地址,自动调用所属类的虚函数。
如果仅仅将参数传递方式改为pass by value,即
void PutInfo(const B b)
结果如下:
; int __cdecl main(int argc, const char **argv, const char **envp)
_main proc near
d= D ptr -14h
b= B ptr -8
argc= dword ptr 8
argv= dword ptr 0Ch
envp= dword ptr 10h
push ebp
mov ebp, esp
sub esp, 14h
push 1 ; i
lea ecx, [ebp+b] ; this
call ??0B@@QAE@H@Z ; B::B(int)
push 3 ; j
push 2 ; i
lea ecx, [ebp+d] ; this
call ??0D@@QAE@HH@Z ; D::D(int,int)
push 0Ch
push 8
push offset aBDSizeDD ; "\tB\tD\nsize:\t%d\t%d\n"
call ds:__imp__printf
add esp, 0Ch
lea eax, [ebp+d]
push eax
lea ecx, [ebp+b]
push ecx ; b
push offset aAddrXX ; "addr:\t%x\t%x\n"
call ds:__imp__printf
add esp, 0Ch
push 0Ah ; Ch
call ds:__imp__putchar
add esp, 4
sub esp, 8
mov ecx, esp ; this
lea edx, [ebp+b]
push edx ; __that
call ??0B@@QAE@ABV0@@Z ; B::B(B const &)
call ?PutInfo@@YAXVB@@@Z ; PutInfo(B)
add esp, 8
sub esp, 8
mov ecx, esp ; this
lea eax, [ebp+d]
push eax ; __that
call ??0B@@QAE@ABV0@@Z ; B::B(B const &)
call ?PutInfo@@YAXVB@@@Z ; PutInfo(B)
add esp, 8
xor eax, eax
mov esp, ebp
pop ebp
retn
_main endp
sub esp, 8
mov ecx, esp ; this
lea edx, [ebp+b]
push edx ; __that
call ??0B@@QAE@ABV0@@Z ; B::B(B const &)
call ?PutInfo@@YAXVB@@@Z ; PutInfo(B)
add esp, 8
可以看到,程序又开辟了8字节的空间,这用于PutInfo函数中的临时变量(形参b)的存放,之后又回收了这8字节的空间。
源代码并没有声明copy构造函数,但是在PutInfo中,参数传递必须利用实参构造形参,因此编译器自动生成了一个copy构造函数B::B(const &)。
考察B::B(const &),__that是已经构造的对象,this是将要构造的对象,在调用copy函数之前,ecx = esp这是栈顶,也是形参b的地址,或者说this指针,edx是实参b的地址。
; void __thiscall B::B(B *this, B *__that)
??0B@@QAE@ABV0@@Z proc near
this= dword ptr -4
__that= dword ptr 8
push ebp
mov ebp, esp
push ecx
mov [ebp+this], ecx
mov eax, [ebp+this]
mov dword ptr [eax], offset ??_7B@@6B@ ; const B::`vftable'
mov ecx, [ebp+this]
mov edx, [ebp+__that]
mov eax, [edx+4]
mov [ecx+4], eax
mov eax, [ebp+this]
mov esp, ebp
pop ebp
retn 4
??0B@@QAE@ABV0@@Z endp
构造函数中,初始化B类虚表地址。
mov edx, [ebp+__that]
mov eax, [edx+4]
mov [ecx+4], eax
然后复制that->x到this->x,完成对象的copy。
pass by value版本的PutInfo:
; void __cdecl PutInfo(B b)
?PutInfo@@YAXVB@@@Z proc near
b= B ptr 8
push ebp
mov ebp, esp
lea ecx, [ebp+b] ; this
call ?printinfo@B@@UBEXXZ ; B::printinfo(void)
pop ebp
retn
?PutInfo@@YAXVB@@@Z endp
直接调用了B::printinfo(void),与实参的实际类型无关。