反汇编角度分析VC面向对象机制.doc

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

//target:从内存角度熟悉VC++面向对象机制作为MFC编程的基础
//author:by Reduta
//descritption:IA32 + win sp3 + vc6.0 /OD 1.10
一:构造函数的之争
1.1:构造函数是有返回值的——返回当前对象的this指针
基本打开每一本C++的教程,都会对构造函数有如此类似的描述“构造函数无返回值,可进行函数重载”,但是事实上又如何呢?答案是构造函数具备返回值,返回值为当前对象的this指针。

编写如下代码:
//示例
#include <iostream>
using namespace std;
class test
{
int a;
public:
test();
void show();
};
test::test()
{
a=1;
}
void test::show()
{
cout<<a<<endl;
}
int main()
{
test hacker;//调用构造函数eax为构造函数返回值
__asm
{
//借助返回值修改对象数据成员
mov dword ptr ss:[eax],2
}
//查看修改是否成功
hacker.show();
return 0;
}
执行上面的程序,对象hacker的数据成员a的值将变为2,将程序载后OD,看关键代码:
00401588 8D4D FC lea ecx,dword ptr ss:[ebp-4] ;hacker的this指针0040158B E8 AFFCFFFF call api.0040123F ;构造函数
00401590 36:C700 02000000 mov dword ptr ss:[eax],2 ;修改对象的数据成员00401597 8D4D FC lea ecx,dword ptr ss:[ebp-4] ;hacker的this指针0040159A E8 0FFCFFFF call api.004011AE ;hacker的show函数
执行到401590时,注意观察eax与ecx的值,如图1所示,
eax和ecx的值是一样的,说明构造函数的确是有返回值,且返回值为当前对象的this指针,重新载入OD,在构造函数处,F7跟进,如图2所示
,用红框标识的为关键代码,即构造函数,最后都会执行一句指令,将当前对象的this指针保存到eax中,而eax是函数的返回值。

不管是debug版的程序还是release版程序都会执行类似的指令,即无论何时,构造函数将返回当前对象的this指针。

1.2:构造函数并不一定是函数——inline内联的影响
看到构造函数并不一定是函数,你可能觉得可笑,这主要受inline内联的影响。

如果将上面的构造函数定义在类作用域内即如下代码,此时测试release版本程序会发现,执行结果依然为1,如图3所示,
即修改未遂,release版与debug版程序的最大区别在于代码优化,使用release版,基本所有的inline函数都能得到扩展,即此时构造函数并不是一个函数,而只是指令扩展。

将release 版程序载入OD,跟进主函数,发现如下代码:
00401020 55 push ebp
00401021 8BEC mov ebp,esp
00401023 51 push ecx
00401024 C745 FC 01000000 mov dword ptr ss:[ebp-4],1 ;构造函数代码处
0040102B 36:C700 02000000 mov dword ptr ss:[eax],2
00401032 8D4D FC lea ecx,dword ptr ss:[ebp-4]
00401035 E8 C6FFFFFF call api.00401000
0040103A 33C0 xor eax,eax
0040103C 8BE5 mov esp,ebp
0040103E 5D pop ebp
0040103F C3 retn
并没有将test()构造函数进行显示调用,此时的test()构造函数只有一句代码即mov dword ptr ss:[ebp-4],1。

1.3:C++初始化的过程
几乎每本C++的教程都会告诉我们对象是有构造函数初始化数据成员,但是并不能系统的告诉我们初始化的具体过程,本部分着重分析C++面向对象数据成员初始化的一般过程及各种初始化方式的特点。

首先来讨论一下C++初始化的一般方法——全局对象初始化、初始化列表、构造函数、static 成员的初始化。

static数据成员是在类内声明,类外定义的,在定义的时候并不遵守访问限定符的限定,一旦定义,static数据成员即遵守访问限定的限制,这里不过分讨论static成员,我们来看一下剩余的几种初始化方式,它们实际上组成了如图4的初始化层次结构。

//全局初始化过程
......................
class test
{
int a;
public:
test(){a=1;cout<<"constructor!"<<endl;}
};
test cao;
int main()
{
cout<<"in main!"<<endl;
test dan;
return 0;
}
执行程序如图5所示,
即在执行main函数之前,需要先调用构造函数初始化全局对象cao,这个过程是有系统自动完成的,调用的函数为_cinit(),这是在CRT中的一个函数,有mscvrt.dll提供。

//初始化列表无序进行
..........................
class test
{
int a;
const int b;
int c;
char d;
public:
test():a(1),c(2),d(0x61),b(10){}
};
int main()
{
test hacker;
return 0;
}
初始化列表是C++提供的一种用于初始化const数据常量的,同时他也能初始化非const 数据常量,上面的代码反汇编后,主要的代码如下:
0040108A 894D FC mov dword ptr ss:[ebp-4],ecx
0040108D 8B45 FC mov eax,dword ptr ss:[ebp-4]
00401090 C700 01000000 mov dword ptr ds:[eax],1 ;a=1
00401096 8B4D FC mov ecx,dword ptr ss:[ebp-4]
00401099 C741 04 0A000000 mov dword ptr ds:[ecx+4],0A ;b=10
004010A0 8B55 FC mov edx,dword ptr ss:[ebp-4]
004010A3 C742 08 02000000 mov dword ptr ds:[edx+8],2 ;c=2
004010AA 8B45 FC mov eax,dword ptr ss:[ebp-4]
004010AD C640 0C 61 mov byte ptr ds:[eax+C],61 ;d='a'
004010B1 8B45 FC mov eax,dword ptr ss:[ebp-4]
我们可以清楚的看到,初始化列表的初始化并不是按照代码的书写顺序先初始化a再初始化c,而是无序的初始化数据成员,这种顺序是无法通过c++进行调整的。

//构造函数初始化有序进行
class test
{
int a;
const int b;
int c;
char d;
public:
test():b(10){d=0x61;c=a=1;}
};
int main()
{
test hacker;
return 0;
}
构造函数用来初始化,一定会按照预先设定的代码执行,因此构造函数的初始化顺序是有序进行的。

主要的反汇编代码如下:
0040108A 894D FC mov dword ptr ss:[ebp-4],ecx
0040108D 8B45 FC mov eax,dword ptr ss:[ebp-4]
00401090 C740 04 0A000000 mov dword ptr ds:[eax+4],0A ;将const变量初始化00401097 8B4D FC mov ecx,dword ptr ss:[ebp-4]
0040109A C641 0C 61 mov byte ptr ds:[ecx+C],61 ;d='a';
0040109E 8B55 FC mov edx,dword ptr ss:[ebp-4]
004010A1 C702 01000000 mov dword ptr ds:[edx],1 ;a=1;
004010A7 8B45 FC mov eax,dword ptr ss:[ebp-4]
004010AA C740 08 01000000 mov dword ptr ds:[eax+8],1 ;c=1
004010B1 8B45 FC mov eax,dword ptr ss:[ebp-4]
对比不难发现,先执行初始化列表,然后执行构造函数,因此初始化列表是在构造函数之前执行的。

二:this指针到底在做什么
this指针在诸多的C++书中都会有类似的描述“始终指向当前正在调用的对象”,那么它到底指向什么呢?答案是它有ecx保存,始终指向当前对象的第一个数据成员。

2.1:C++中变量内存分布
C++中的变量或者称为对象在内存的分布是不一样的,一方面作用域与存储类rase(register auto static extern)决定了变量分布于内存的堆还是栈,另一方面,先给谁分配内存的顺序也是有区别的。

大致的规则如下
A:单一变量,先声明先的变量,先分配内存
如下代码
int main()
{
int a;
int b;
}
会先分配a变量需要的内在,然后分配b变量需要的内存,因此,a的地址是最大的,即为ebp-4,而b的地是小的ebp-8。

B:多变量的集合,比如数组、struct结构体、自定义的对象,先声明的变量后分配内存。

如下代码:
int main()
{
int a[3];
}
a[0]的地址最小的,最后分配内存,a[3]的地址是最大的,先分配内存。

理解了上面的顺序我们再来看this指针到底做了什么?
2.2:this指针到底在作什么
this指针始终指向当前对象的第一个数据成员,即告诉对象的行为(函数)要操作的地址是什么,用ecx寄存器保存其值。

//理解this指针
......................
class test
{
public:
int a;
int b;
int c;
test(){a=b=c;}
int main()
{
unsigned thisaddr;
test hacker;
__asm
{
//取this指针
mov dword ptr ss:[ebp-4],ecx
}
cout<<&hacker.a<<" "<<&hacker.b<<" "<<&hacker.c<<"
"<<hex<<thisaddr<<endl;
return 0;
}
执行结果如图6所示,
a的地址与this指针的值是一样的,即this指针始终指向对象的第一个数据成员的内存地址。

当然这在继承中略有不同,这点不同在于vbtable的引入。

三:虚基类继承时发生了什么
继承是什么呢?继承解实际上抽象了具有相同特性的类,这使得类之间有了一定的层次,最上层的是最核心的,即所有的类的交集,比如C++的输入输出类即为此种层次结构。

如图7描述了C++的继承关系,
如图所示使用虚基类的情况是一个平行四边形的继承关系时,暂时称为平行四边形法则。

//虚基类示例
..................
class base
public:
int a;
};
class d1:virtual public base
{
public:
int b;
};
class d2:virtual public base
{
public:
int c;
};
class d3:public d1,public d2
{
public:
int sum;
};
int main()
{
d3 ob3;
ob3.a=10;
ob3.b=20;
ob3.c=30;
ob3.sum=ob3.a+ob3.b+ob3.c;
cout<<ob3.sum<<endl;
return 0;
}
3.1:vbtable(虚基类表)是如何工作的
将上面的程序载入OD,跟进主函数,关键代码如下:
//反汇编代码
00401598 6A 01 push 1 ;这个参数是一个控制标志
0040159A 8D4D E8 lea ecx,dword ptr ss:[ebp-18] ; this指针指向d1的vbtable地址,即等同于第一个元素a的内存地址
0040159D E8 FEFAFFFF call
api.004010A0 ; 构造函数依次调用基类子
类的构造函数
004015A2 8B45 E8 mov eax,dword ptr
ss:[ebp-18] ; d1 vbtable
004015A5 8B48 04 mov ecx,dword ptr
ds:[eax+4] ; eax+4=offset
004015A8 C7440D E8 0A00000>mov dword ptr
ss:[ebp+ecx-18],0A ; this+offset
此时我们执行完构造函数,观察栈区如图8所示,
此时构造函数将虚基类的vbtable压入栈中,我们dd vbtable的内容,如图9所示。

综合分析,得出vbtable的结构如下:
vbtable struc
x1 dw ?
offset dw ?
x2 dw ?
vbtable ends
其中offset指明了当前数据成员距离惟一副本的偏移,在此程序中即为d1中vbtable所在栈地址与其数据成员a的偏移地址,即this虚基类通过vbtable中偏移+4的偏移量来定位多副本程序,从而实现子类中只有一个虚基类的副本。

3.2:vbtable是如何引入的
重新载入程序,在40159d处跟进构造函数,会发现如下代码:
00401620 > 55 push ebp ; 保存基址
00401621 8BE mov ebp,esp ;开辟新栈帧
00401623 83EC 44 sub
esp,44 ; 为新栈帧分配相应的内存
00401626 53 push ebx
00401627 56 push esi
00401628 57 push edi
00401629 51 push ecx ; 保存ecx,因为ecx要作为初始化的计数器
0040162A 8D7D BC lea edi,dword ptr ss:[ebp-44]
0040162D B9 11000000 mov ecx,11
00401632 B8 CCCCCCCC mov eax,CCCCCCCC
00401637 F3:AB rep stos dword ptr es:[edi]
00401639 59 pop ecx ; this指针出栈
0040163A 894D FC mov dword ptr ss:[ebp-4],ecx ; 保存ecx
0040163D 837D 08 00 cmp dword ptr ss:[ebp+8],0 ; 参数1与0比较以判断是否使用虚基类00401641 74 13 je short api.00401656
00401643 8B45 FC mov eax,dword ptr ss:[ebp-4]
00401646 C700 28E04600 mov dword ptr ds:[eax],offset api.d3::`vbtable' ; d2的vbtable
0040164C 8B4D FC mov ecx,dword ptr ss:[ebp-4]
0040164F C741 08 1CE04600 mov dword ptr ds:[ecx+8],offset api.d3::`vbtable' ; d1的vbtable
分析40163d处的代码,为cmp dword ptr ss:[ebp+8],0,对于任何一个函数来说,它的栈区结构应该如图10所示,
而此时的ebp+8即为参数1,因此在使用虚基类时,总会先push 1用于控制判断是否有虚基类,将将虚基类的vbtable保存到其多副本数据成员区,在本程序中即为d1和d2数据成员的a位置处。

综上所述,当使用虚基类时,构造函数会通过一个标志控制参数,将vbtable的地址保存到多副本数据成员的内存位置,对应于本程序的变量a,当访问具有多副本变量时,通过vbtable+4处的偏移值+this指针即可,这保证了继承时子类中只有基类的一个数据成员副本。

四:多态到底为何物
实际上C++面向对象程序设计的重要的问题是解决类与类之间的关系,类与类之间的关系,可以简单的理解为完全不同的类、大部分不同的类、大部分相同的类、完全相同的类。

我们可以简称为类的四象,如图11所示,
结合太极四象更容易理解类与类的关系。

多态即为只有一小部分相似的类的关系,反过来说即为大部分不同的类关系,这种关系通过运行时的vftable来完成。

//示例
class test
{
public:
virtual void vfunc(){cout<<"base's vfunc"<<endl;}
};
class d1:public test
{
public:
void vfunc(){cout<<"d1's vfunc!"<<endl;}
};
class d2:public test
{
public:
void vfunc(){cout<<"d2's vfunc!"<<endl;}
};
int main()
{
test a,*p;
d1 b;
d2 c;
p=&a;
p->vfunc();
p=&b;
p->vfunc();
p=&c;
p->vfunc();
return 0;
}
将上面的函数反汇编一下。

//反汇编上面的程序分析虚函数机制
004012C8 8D4D FC lea ecx,dword ptr ss:[ebp-4] ; this指针a
004012CB E8 6CFDFFFF call api.0040103C
004012D0 8D4D F4 lea ecx,dword ptr ss:[ebp-C] ; this指针b
004012D3 E8 69FDFFFF call api.00401041
004012D8 8D4D F0 lea ecx,dword ptr ss:[ebp-10] ; this指针c
004012DB E8 48FDFFFF call api.00401028
004012E0 8D45 FC lea eax,dword ptr ss:[ebp-4] ; 对象a的地址保存到eax中
004012E3 8945 F8 mov dword ptr ss:[ebp-8],eax ; 借助对象指针p保存对象a
004012E6 8B4D F8 mov ecx,dword ptr ss:[ebp-8] ; 设置当前this指针为对象a的
004012E9 8B11 mov edx,dword ptr ds:[ecx] ; vftable
004012EB 8BF4 mov esi,esp
004012ED 8B4D F8 mov ecx,dword ptr ss:[ebp-8]
004012F0 FF12 call dword ptr ds:[edx] ; api.0040102D //此处为vftable中保存的地址。

查看vftable的内容,如图12所示。

那么vftable的结构应该如下:
vftable struc
address dw ?
????????????????
vftable ends
address为多态时函数的真正地址,后面的内容,在本程序为程序要输出的字符内容,如果我们不使用内联函数,则不会看到这些内容,而只看到address,即所谓多态的实现是通过在执行时,先确定vftable的位置,再调用函数的,而vftable是通过构造函数引入的。

在对象a的构造函数处跟进,会发现如下代码:
00401279 59 pop ecx ; 0012FF7C
0040127A 894D FC mov dword ptr ss:[ebp-4],ecx
0040127D 8B45 FC mov eax,dword ptr ss:[ebp-4]
00401280 C700 1C204300 mov dword ptr ds:[eax],offset api.test::`vftable'
00401286 8B45 FC mov eax,dword ptr ss:[ebp-4]
vftable通过构造函数,保存到this指针的首位置处。

相关文档
最新文档