|
|
原作者姓名 陈辉东
正文
本文从一个简单的C++程序所对应的汇编代码来分析类构造函数的产生,调用,以及相应的内存地址分配。所采取的方法是利用堆栈的变化来跟踪演示。由于本文不是讲汇编语言的,因此对涉及的汇编语言知识只是做简单介绍。
本文适合对象:了解C++,对汇编语言有一些基本知识的读者。
一、相关知识简介
EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP都是32位的寄存器.EAX.是累加寄存器,主要用在算术运算中存放运算数;EBX是基址寄存器,主要在内存寻址时存放基地址; ECX是计数器,主要在循环(LOOP)和重复(REP)中用做内部计数器; EDX数据寄存器,乘法指令中作累加器等;ESI,源变址寄存器,通常在字符串操作中用DS:ESI指定源串地址;EDI,目的变址寄存器, 通常在字符串操作中用ES:EDI指定目的串地址; EBP,基址指针寄存器,常用做函数调用的框架指针;ESP, 堆栈指针寄存器,用来指向当前堆栈栈顶.
本文主要通过跟踪EBP, ESP, ECX三个寄存器值的变化来了解构造函数的调用机制.
二、本文所用到的C++程序及其输出结果:
该程序的输出结果是(仅供参考,不同的系统和编译程序结果可能不一样):
这些对象的地址是如何来的?为什么有的还相同?本文将围绕分析这4个对象的来由来展开.
三、main前奏分析
如下图,此处是系统在开始正式处理之前的环境准备工作,主要包括:保存当前环境,初始化寄存器,划分变量空间,初始化变量空间。从堆栈变化图中可以看出,所谓“保存当前设置”,就是将这些变量,指针压栈。
执行到此处,EBP=0X0012FF80, ESP=0X12FF1C,ECX=0X00000000。
四、开始调用
在编译程序运行进入main函数之前,系统已经有对main里的变量,对象进行扫描,计算出该分配的空间,一般情况是按变量,对象的声明顺序,顺序分配在以EBP所指的地址为首的空间里,分配的大小由该变量的类型大小决定,分配的空间是4的倍数,如果不足4字节,则补足4字节;对象的空间是4字节。这个空间就是上面二所提到的“变量空间”。
先来看构造函数的第一次调用.
当声明对象dongdong时,系统会自动调用构造函数.它是如何”自动”的呢?看图4汇编代码,
“CHD dongdong;”对应的汇编代码是
0040108D lea ecx,[ebp-10h]
00401090 call @ILT+35(CHD::CHD) (00401028)
00401095 mov dword ptr [ebp-4],0
系统先将对象dongdong的有效地址(dongdong的相对地址是ebp-10h)存入ecx中,因为EBP=0X0012FF80,所以ECX=0X0012FF70,这就是dongdong的有效地址.然后调用CHD::CHD.
下面我们转到函数CHD::CHD()来分析.
如下:
由于此次调用CHD::CHD的是对象dongdong,因此系统会将dongdong的有效地址0X0012FF70传进去.该地址将被赋给函数CHD::CHD的当前对象.然后程序执行输出的操作,之后调用无显式对象的CHD::CHD(int age),再调用无显式对象的CHD::~CHD().最后退出该函数,退出该函数时,可以发现此时的ESP,EBP和进入函数时的值一样.说明退出该函数后,在进入函数CHD::CHD时所分配的一些变量空间将被舍弃,这些变量就是所谓的临时变量,一旦其作用域结束,它们将”消亡”.其实也不能说”消亡”或者”销毁”,因为系统并没有真正的将这些临时变量进清除掉(比如清零等等),而只是将这些变量置于堆栈指针之外,也就是将这些变量的地址扔掉,使这些地址变成可再用的地址.
五、第二次调用
从图5看出,在上面的过程中,函数里还调用无显式对象的CHD::CHD(int age),如下:
23: CHD(23); //显式调用构造函数,则系统会自动创建一个临时对象
00401163 push 17h
00401165 lea ecx,[ebp-8]
00401168 call @ILT+10(CHD::CHD) (0040100f)
0040116D lea ecx,[ebp-8]
00401170 call @ILT+20(CHD::~CHD) (00401019)
由于此时没有指定一个对象,因此系统在最初的扫描的时候,就已经为其指定了一个对象,这个对象的地址是[ebp-8],因为EBP=0X12FF14,所以ECX的值为 0X0012FF0C.因此此时再调用CHD::CHD(int age)时,其this的值为0012FF0C.
六、第三次调用
下面我们再回到main()函数.
接下来是调用无显式对象的CHD::CHD().如下:
34: CHD::CHD(); //显式调用构造函数,则系统会自动创建一个临时对象
0040109C lea ecx,[ebp-14h]
0040109F call @ILT+35(CHD::CHD) (00401028)
004010A4 lea ecx,[ebp-14h]
004010A7 call @ILT+20(CHD::~CHD) (00401019)
由上面的分析可以知道,系统也会为此构造函数指定一个临时对象.由图4可以看出,此处临时对象的地址是[ebp-14h],EBP=0X0012FF80,所以对象地址为0X0012FF6C.
因此,调用CHD::CHD()时,输出的this地址是0X0012FF6C.
七、第四次调用
在此CHD::CHD()也调用了与上面类似的无显式对象的CHD::CHD(int age).由图4可以看出,
00401090 call @ILT+35(CHD::CHD) (00401028)
时的ESP和
0040109F call @ILT+35(CHD::CHD) (00401028)
时的ESP是不变的,也就是二者的ESP是一样的,因此,当此处再调用与上面类似的无显式对象的CHD::CHD(int age)时,其系统所分配的临时对象的地址是一样的(系统分配的是相对与ESP的地址,当两处的ESP一样时,其地址就一样).因此,此处临时对象的地址还是0X0012FF0C.
八、总结
至此,我们把4个对象的地址都分析出来了.现在回头一看,其实类的构造函数并不神秘.调用构造函数肯定要有对象,如果没有对象,则类会创建一个临时对象;由于是临时对象,所以所有对这个临时对象操作,赋值等的结果都不会被保存,也不能被访问到(当然,直接通过内存地址访问那是另外一回事). 当通过不同渠道调用同一个构造函数,且该构造函数里边又调用
另一个构造函数,则系统在第二个构造函数里产生的临时对象都一样。
|
|