首页
论坛
课程
招聘
[原创]从反汇编的角度学C/C++之构造函数与析构函数
2021-10-2 09:43 4184

[原创]从反汇编的角度学C/C++之构造函数与析构函数

2021-10-2 09:43
4184

一.构造函数

    构造函数可以让我们定义如何对类变量进行初始化,那么它们具体是如何工作的,不同的构造函数是否有不同的区别。以下实例定义了两者构造函数来初始化我们的类成员。

class Base
{
public:
	Base()
	{
		x = 1;
		y = 1;
	}
	Base(int x, int y)
	{
		this->x = x;
		this->y = y;
	}
private:
	int x;
	int y;
};

    在main函数中的反汇编代码如下:

	Base base1, base2(3, 4);
008910F8  lea         ecx,[base1]             //将base1的地址赋给ecx
008910FB  call        Base::Base (0891060h)   //调用无参的构造函数
00891100  push        4  
00891102  push        3                       //压入两个实参
00891104  lea         ecx,[base2]             //将base2的地址赋给ecx
00891107  call        Base::Base (0891000h)   //调用构造函数

    可以看到不同的构造函数在内存中的表现差别也只是参数的不同,所以定义多个构造函数和我们的函数重载是一样的,都是由编译器为我们生成了相应的代码供我们调用。那么函数内部的实现是如何。我们来分别看看0x0891060和0x0891000地址的内容

	Base()
00891060  push        ebp  
00891061  mov         ebp,esp  
00891063  sub         esp,0CCh  
00891069  push        ebx  
0089106A  push        esi  
0089106B  push        edi  
0089106C  push        ecx                      //存储类变量地址
0089106D  lea         edi,[ebp-0CCh]  
00891073  mov         ecx,33h  
00891078  mov         eax,0CCCCCCCCh  
0089107D  rep stos    dword ptr es:[edi]  
0089107F  pop         ecx                      //恢复类变量地址
00891080  mov         dword ptr [this],ecx     //将类变量地址保存到局部变量中
00891083  mov         ecx,offset _D3472036_test@cpp (0922008h)  
00891088  call        __CheckForDebuggerJustMyCode (0891330h)  
	{
		x = 1;
0089108D  mov         eax,dword ptr [this]      //取出类变量地址
00891090  mov         dword ptr [eax],1         //地址偏移0处就是x的地址,赋值为1
		y = 1;
00891096  mov         eax,dword ptr [this]      //取出类变量地址
00891099  mov         dword ptr [eax+4],1       //地址偏移4处就是y的地址,赋值给1
	}
008910A0  mov         eax,dword ptr [this]      //将类变量的地址赋给eax,作为返回值返回
008910A3  pop         edi  
008910A4  pop         esi  
008910A5  pop         ebx  
008910A6  add         esp,0CCh  
008910AC  cmp         ebp,esp  
008910AE  call        _RTC_CheckEsp (08912F0h)  
008910B3  mov         esp,ebp  
008910B5  pop         ebp  
008910B6  ret
	Base(int x, int y)
00891000  push        ebp  
00891001  mov         ebp,esp  
00891003  sub         esp,0CCh  
00891009  push        ebx  
0089100A  push        esi  
0089100B  push        edi  
0089100C  push        ecx                          //保存类变量地址
0089100D  lea         edi,[ebp-0CCh]  
00891013  mov         ecx,33h  
00891018  mov         eax,0CCCCCCCCh  
0089101D  rep stos    dword ptr es:[edi]      
0089101F  pop         ecx                          //恢复类变量地址
00891020  mov         dword ptr [this],ecx         //将类变量地址给到局部变量
00891023  mov         ecx,offset _D3472036_test@cpp (0922008h)  
00891028  call        __CheckForDebuggerJustMyCode (0891330h)  
	{
		this->x = x;
0089102D  mov         eax,dword ptr [this]          //取出类变量地址赋给eax
00891030  mov         ecx,dword ptr [ebp+8]         //取出参数1的内容
00891033  mov         dword ptr [eax],ecx           //地址偏移为0即使x的地址,将参数1的内容赋值给x
		this->y = y;
00891035  mov         eax,dword ptr [this]          //取出类变量地址赋给eax
00891038  mov         ecx,dword ptr [ebp+0Ch]       //取出参数2的内容  
0089103B  mov         dword ptr [eax+4],ecx         //地址偏移为4即使y的地址,将参数2的内容赋值给y
	}
0089103E  mov         eax,dword ptr [this]          //将类变量的地址赋给eax,作为返回值返回
00891041  pop         edi  
00891042  pop         esi  
00891043  pop         ebx  
00891044  add         esp,0CCh  
0089104A  cmp         ebp,esp  
0089104C  call        _RTC_CheckEsp (08912F0h)  
00891051  mov         esp,ebp  
00891053  pop         ebp  
00891054  ret         8

    可以看到构造函数和我们平时写的普通函数并没什么不一样,只不过在函数返回的时候构造函数会将类变量的地址作为返回值返回给用户。

二.  拷贝构造函数

    拷贝构造函数作为一种特殊的构造函数在编程中有着广泛的用途,将上面的实例增加一个拷贝构造函数如下

class Base
{
public:
	Base()
	{
		x = 1;
		y = 1;
	}
	Base(int x, int y)
	{
		this->x = x;
		this->y = y;
	}
	Base(const Base &obj)
	{
		this->x = obj.x;
		this->y = obj.y;
	}
private:
	int x;
	int y;
};

    首先看看main函数中的反汇编

	Base base1(3, 4);
00D310F8  push        4  
00D310FA  push        3  
00D310FC  lea         ecx,[base1]  
00D310FF  call        Base::Base (0D31060h)      //调用有参构造函数
	Base base2(base1);
00D31104  lea         eax,[base1]                //取出base1的变量地址
00D31107  push        eax                        //将地址入栈
00D31108  lea         ecx,[base2]                //将base2的地址赋给ecx
00D3110B  call        Base::Base (0D31000h)      //调用相应的拷贝构造函数

    可以看到拷贝构造函数如预期一样把相应的类变量地址入栈后调用函数,接下来我们看看拷贝构造函数

	Base(const Base &obj)
00D31000  push        ebp  
00D31001  mov         ebp,esp  
00D31003  sub         esp,0CCh  
00D31009  push        ebx  
00D3100A  push        esi  
00D3100B  push        edi  
00D3100C  push        ecx                    //保存类变量地址
00D3100D  lea         edi,[ebp-0CCh]          
00D31013  mov         ecx,33h  
00D31018  mov         eax,0CCCCCCCCh  
00D3101D  rep stos    dword ptr es:[edi]  
00D3101F  pop         ecx      
00D31020  mov         dword ptr [this],ecx  //恢复类变量地址并赋值给局部变量
00D31023  mov         ecx,offset _D3472036_test@cpp (0DC2008h)  
00D31028  call        __CheckForDebuggerJustMyCode (0D31340h)  
	{
		this->x = obj.x;
00D3102D  mov         eax,dword ptr [this]      //取出目标类变量地址ecx
00D31030  mov         ecx,dword ptr [ebp+8]     //取出参数,即源类变量地址赋给ecx
00D31033  mov         edx,dword ptr [ecx]       //此时ecx偏移0处地址就是源类变量的x,赋值给edx
00D31035  mov         dword ptr [eax],edx       //此时eax偏移为0地址就是目标类变量的x,将edx赋值给它
		this->y = obj.y;
00D31037  mov         eax,dword ptr [this]  
00D3103A  mov         ecx,dword ptr [ebp+8]  
00D3103D  mov         edx,dword ptr [ecx+4]  
00D31040  mov         dword ptr [eax+4],edx      //与上述实现原理一样只是y是在偏移为4的地方
	}
00D31043  mov         eax,dword ptr [this]       //将目标类变量地址作为返回值赋给eax
00D31046  pop         edi  
00D31047  pop         esi  
00D31048  pop         ebx  
00D31049  add         esp,0CCh  
00D3104F  cmp         ebp,esp  
00D31051  call        _RTC_CheckEsp (0D31300h)  
00D31056  mov         esp,ebp  
00D31058  pop         ebp  
00D31059  ret         4

    当然还有一种比较常见的是在类初始化赋值的时候会调用拷贝构造函数如下

	Base base2 = base1;
00AA1104  lea         eax,[base1]  
00AA1107  push        eax  
00AA1108  lea         ecx,[base2]  
00AA110B  call        Base::Base (0AA1000h)

    反汇编的内容跟上面传参调用是一样的,可见由拷贝构造函数的时候,初始化的时候编译器会帮我们自动调用拷贝构造函数。

三.析构函数

    在C/C++中,析构函数常常被用来释放内存空间,修改上述实例为如下

class Base
{
public:
	Base()
	{
		pTest = malloc(10);
	}
	Base(int size)
	{
		pTest = malloc(size);
	}
	~Base()
	{
		printf("~Base()\n");
		if (pTest)	free(pTest);
	}
private:
	void *pTest;
};

    接着我将会在test函数中定义这个变量,依次来观察析构函数的使用

void test()
{
008E10F0  push        ebp  
008E10F1  mov         ebp,esp  
008E10F3  sub         esp,0CCh  
008E10F9  push        ebx  
008E10FA  push        esi  
008E10FB  push        edi  
008E10FC  lea         edi,[ebp-0CCh]  
008E1102  mov         ecx,33h  
008E1107  mov         eax,0CCCCCCCCh  
008E110C  rep stos    dword ptr es:[edi]  
008E110E  mov         ecx,offset _D3472036_test@cpp (097200Bh)  
008E1113  call        __CheckForDebuggerJustMyCode (08E14C0h)  
	Base base1(10);
008E1118  push        0Ah  
008E111A  lea         ecx,[base1]  
008E111D  call        Base::Base (08E1000h)      //调用构造函数
}
008E1122  lea         ecx,[base1]                //将base1的地址赋给ecx
008E1125  call        Base::~Base (08E1060h)     //调用析构函数
008E112A  push        edx                        //开始执行函数推出的代码
008E112B  mov         ecx,ebp  
008E112D  push        eax  
008E112E  lea         edx,ds:[8E1150h]  
008E1134  call        _RTC_CheckStackVars (08E1410h)  
008E1139  pop         eax  
008E113A  pop         edx  
008E113B  pop         edi  
008E113C  pop         esi  
008E113D  pop         ebx  
008E113E  add         esp,0CCh  
008E1144  cmp         ebp,esp  
008E1146  call        _RTC_CheckEsp (08E1480h)  
008E114B  mov         esp,ebp  
008E114D  pop         ebp  
008E114E  ret

    由上可以看出,对于局部变量来说,在函数退出之前会首先调用相应的析构函数释放内存,随后才开始函数退出代码的执行。再看析构函数内容

	~Base()
	{
008E1060  push        ebp  
008E1061  mov         ebp,esp  
008E1063  push        0FFFFFFFFh  
008E1065  push        952990h  
008E106A  mov         eax,dword ptr fs:[00000000h]  
008E1070  push        eax  
008E1071  mov         dword ptr fs:[0],esp  
008E1078  sub         esp,0CCh  
008E107E  push        ebx  
008E107F  push        esi  
008E1080  push        edi  
008E1081  push        ecx                              //保存类变量地址
008E1082  lea         edi,[ebp-0D8h]  
008E1088  mov         ecx,33h  
008E108D  mov         eax,0CCCCCCCCh  
008E1092  rep stos    dword ptr es:[edi]  
008E1094  pop         ecx                              //恢复类变量地址
008E1095  mov         dword ptr [this],ecx             //将类变量地址赋给局部变量
008E1098  mov         ecx,offset _D3472036_test@cpp (097200Bh)  
008E109D  call        __CheckForDebuggerJustMyCode (08E14C0h)  
		printf("~Base()\n");
008E10A2  push        offset string "~Base()\n" (09531B0h)  
008E10A7  call        printf (08E1280h)               //打印输出
008E10AC  add         esp,4                            
		if (pTest)	free(pTest);
008E10AF  mov         eax,dword ptr [this]            //取出类变量地址
008E10B2  cmp         dword ptr [eax],0               //判断地址中的变量值是否为0
008E10B5  je          Base::~Base+65h (08E10C5h)      //为0则跳转
008E10B7  mov         eax,dword ptr [this]            //取出类变量地址
008E10BA  mov         ecx,dword ptr [eax]             //将类变量地址中的内容取出赋给ecx
008E10BC  push        ecx                             //此时ecx便是pTest
008E10BD  call        free (091A2A0h)                 //调用free函数
008E10C2  add         esp,4  
	}
008E10C5  mov         ecx,dword ptr [ebp-0Ch]  
008E10C8  mov         dword ptr fs:[0],ecx  
008E10CF  pop         edi  
008E10D0  pop         esi  
008E10D1  pop         ebx  
008E10D2  add         esp,0D8h  
008E10D8  cmp         ebp,esp  
008E10DA  call        _RTC_CheckEsp (08E1480h)  
008E10DF  mov         esp,ebp  
008E10E1  pop         ebp  
008E10E2  ret

    可以看到析构函数的执行和我们的普通成员函数的执行是一样的,它并没有把函数地址作为返回值返回给用户。


2022 KCTF春季赛【最佳人气奖】火热评选中!快来投票吧~

最后于 2021-10-20 11:24 被1900编辑 ,原因:
收藏
点赞0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回