首页
社区
课程
招聘
反汇编C++之构造函数篇
发表于: 2014-12-11 18:21 9484

反汇编C++之构造函数篇

2014-12-11 18:21
9484
在类的对象创建的时候,构造函数起到了对对象的初始化的作用。本节将对构造函数的机理进行探讨。可以思考下面的几个问题:1、构造函数的用到是什么机制?2、构造函数在汇编形式下有什么特点,也即是怎样在汇编代码中识别构造函数?由于存在不同生命周期、不同作用域的对象,那么这些构造函数的实现机制都是相同的么?
带着这样的疑问,我们来使用实际的例子探究一番。
本节实验环境:VC++ 2013

例子1:

#include <iostream>
using namespace std;

class Rectangle{
private:
    int width ;
    int height ;
public:
    Rectangle();
};

Rectangle::Rectangle (int width , int height)
{
    this.width = width ;
    this.height = height ;
}

void main()
{
    Rectangle t (3, 6);
}

在debug模式下查看其main函数的反汇编代码:
void main()
{
00D75300  push        ebp 
00D75301  mov         ebp,esp 
00D75303  sub         esp,0D4h  
00D75309  push        ebx 
00D7530A  push        esi 
00D7530B  push        edi 
00D7530C  lea         edi,[ebp+FFFFFF2Ch] 
00D75312  mov         ecx,35h  
00D75317  mov         eax,0CCCCCCCCh  
00D7531C  rep stos    dword ptr es:[edi] 
00D7531E  mov         eax,dword ptr ds:[00D7F000h];该行以及下面两行是检测栈是否被修改策略用到的方法,先不用追究。
00D75323  xor         eax,ebp 
00D75325  mov         dword ptr [ebp -4], eax 
    Rectangle t (3, 6 );
00D75328  push        6 
00D7532A  push        3 
00D7532C  lea         ecx,[ebp-10h];ebp-10h是变量t的首地址,即是该对象的this指针 
00D7532F  call        00D712B2 
}
.....

需要注意的是,在栈帧的建立过程中,需要遵守x86的函数调用约定,将callee保存的寄存器压入栈内。
callee保存的寄存器值:ebx、esi、edi.可以发现这个函数调用将callee需要保存的寄存器全部保存起来。
在创建对象的时候,下面的代码即是调用构造函数的代码:
00D75328  push        6 
00D7532A  push        3 
00D7532C  lea         ecx,[ebp-10h]  
00D7532F  call        00D712B2 
其中lea ecx,[ebp-10h]值得注意,ecx保存的值相当于就是this指针,也就是说调用构造函数也要传递this指针了。其实,识别C++类的比较好的方法是,C++的成员函数的this指针都是通过ecx寄存器传递的。
回到代码中,可以发现在构造函数调用前,会腾出一段栈空间(ebp-10到ebp之间的空间)提供给对象使用。
在执行到call函数之后,进行单步调试,会进入到一个jmp指令:
00D712B2  jmp         00D73030 
之后进入构造函数进行执行:
Rectangle::Rectangle (int width, int height)
{
00D73030  push        ebp 
00D73031  mov         ebp,esp 
00D73033  sub         esp,0CCh  
00D73039  push        ebx 
00D7303A  push        esi 
00D7303B  push        edi 
00D7303C  push        ecx 
00D7303D  lea         edi,[ebp+FFFFFF34h] 
00D73043  mov         ecx,33h  
00D73048  mov         eax,0CCCCCCCCh  
00D7304D  rep stos    dword ptr es:[edi] 
00D7304F  pop         ecx 
00D73050  mov         dword ptr [ebp -8], ecx;将this指针保存到地址为ebp-8的内存呢
    this-> width = width ;
00D73053  mov         eax,dword ptr [ ebp-8 ];将this指针传递到eax  
00D73056  mov         ecx,dword ptr [ ebp+8 ];将参数width传递给ecx
00D73059  mov         dword ptr [eax ],ecx;对this指针所指对象的第一个变量进行赋值
    this-> height = height ;
00D7305B  mov         eax,dword ptr [ ebp-8 ] 
    this-> height = height ;
00D7305E  mov         ecx,dword ptr [ ebp+0Ch ];获取参数height  
00D73061  mov         dword ptr [eax +4], ecx ;对this指针所指对象的第二个变量进行赋值
}
00D73064  mov         eax,dword ptr [ ebp-8 ];eax保存了this指针,  
00D73067  pop         edi 
00D73068  pop         esi 
00D73069  pop         ebx 
00D7306A  mov         esp,ebp 
00D7306C  pop         ebp 
00D7306D  ret         8 
注意此代码中ecx在函数最前面要使用一堆数据填充栈时需要用到ecx,所有先保存起来,后来再使用,所以有压栈和弹栈的过程。通过上面的代码我们可以知道,对成员变量的赋值,其实都是通过this指针进行索引,间接寻址方式找到成员变量,也可以看出隐含着的this指针作用多么重要。另外,虽然我们写构造函数时没有返回值,但在汇编代码中还体现了构造函数“拥有”返回值这一现象,即通过eax构造函数返回了this指针,这些隐藏于代码之下的细节,也只有通过汇编的形式才能看到。
上面的对象作为局部对象,是在栈空间内存储对象的数据成员的。
那么,作为在main函数执行之前就已经存在的全局对象和静态对象,在什么时候调用构造函数的呢?
例2:
Rectangle my(5 , 7 );
void main()
{
    

    
}
注:Rectangle类与例1中的定义相同。
直接在Rectangle my (5 , 7 )处下断点,将会到下面的地方执行:
00AC19B0  push        ebp 
00AC19B1  mov         ebp,esp 
00AC19B3  sub         esp,0C0h  
00AC19B9  push        ebx 
00AC19BA  push        esi 
00AC19BB  push        edi 
00AC19BC  lea         edi,[ebp-0C0h]  
00AC19C2  mov         ecx,30h  
00AC19C7  mov         eax,0CCCCCCCCh  
00AC19CC  rep stos    dword ptr es:[edi] 
00AC19CE  push        7 
00AC19D0  push        5 
00AC19D2  mov         ecx,0ACF324h  
00AC19D7  call        Rectangle ::Rectangle (0AC12B2h) 
00AC19DC  pop         edi 
00AC19DD  pop         esi 
00AC19DE  pop         ebx 
00AC19DF  add         esp,0C0h  
00AC19E5  cmp         ebp,esp 
00AC19E7  call        __RTC_CheckEsp (0AC12DAh) 
00AC19EC  mov         esp,ebp 
00AC19EE  pop         ebp 
00AC19EF  ret 

前面的过程依然是栈帧的建立,着重看后面调用构造函数的代码: 
00AC19CE   push        7 
00AC19D0   push        5 
00AC19D2   mov         ecx ,0ACF324h ;传递this指针
00AC19D7   call        Rectangle :: Rectangle (0AC12B2h ) 
注意到此时传递的this指针并不是在栈上的,而是编译器分配的一个地址,而且这个地址处在虚拟内存中的低地址部分。那么是谁调用了该构造函数呢?我们继续执行,直到该段代码的ret。

当创建堆对象时,又会进行怎样的处理呢?

代码:
void main()
{
    Rectangle * t = new Rectangle(3 , 4 );

}

汇编代码部分:

Rectangle * t = new Rectangle(3, 4);
00E97E7D  push        8 
00E97E7F  call        operator new (0E9137Ah);在堆上申请8个字节的空间  
00E97E84  add         esp,4  
00E97E87  mov         dword ptr [ebp -0E0h], eax;将申请到的空间首地址拿到栈上  
00E97E8D  mov         dword ptr [ebp -4], 0 
00E97E94  cmp         dword ptr [ebp -0E0h], 0;检查返回的地址是不是NULL.即有没有申请成功  
00E97E9B  je          main +74h (0E97EB4h );申请空间失败,则不调用构造函数  
00E97E9D  push        4 
00E97E9F  push        3 
00E97EA1  mov         ecx,dword ptr [ ebp-0E0h ];this指针传入  
00E97EA7  call        Rectangle ::Rectangle (0E912B2h) ;调用构造函数
00E97EAC  mov         dword ptr [ebp -0F4h], eax;返回的this指针保存
00E97EB2  jmp         main +7Eh (0E97EBEh ) 
00E97EB4  mov         dword ptr [ebp -0F4h], 0;申请空间失败,指针赋值为空  
00E97EBE  mov         eax,dword ptr [ ebp-0F4h ];得到该对象的地址
00E97EC4  mov         dword ptr [ebp -0ECh], eax ;保存
00E97ECA  mov         dword ptr [ebp -4], 0FFFFFFFFh 
00E97ED1  mov         ecx,dword ptr [ ebp-0ECh ];再将对象的首地址移到ecx中  
00E97ED7  mov         dword ptr [t],ecx  ;获取到对象首地址。
通过上面的代码,可以观察到:堆对象创建时,会首先使用new函数申请堆空间,如果申请成功,则执行构造函数,返回首地址,如果不成功,则不执行构造函数,返回为空。既然该指针为空,那么可以想象得到,如果此时引用成员函数,并且访问了该类的动态成员变量,就会引发段错误。

C++拷贝构造函数(浅拷贝)可能带来的问题:
代码:

#include <string.h>
#include <stdlib.h>

class CStudent
{
public :
    int id;
    char* name;
public:
    CStudent(int m_id, char* m_name );
    CStudent(CStudent &stu);
    ~CStudent ();
};

CStudent::CStudent (int m_id, char* m_name )
{
    this->id = m_id;
    int length = strlen(m_name);
    this->name = new char [length + sizeof(char)];
    strncpy_s(this ->name, length+sizeof(char), m_name, length );
}
CStudent::~CStudent ()
{
    delete  this ->name;
    this->name = NULL ;
}
CStudent::CStudent (CStudent &stu)
{
    this->id = stu. id;
    this->name = stu. name;

}

void main()
{
                 CStudent stu(1, "bingxian" );
                 CStudent stu2(stu);
}

执行上面的程序,就会出现错误。
其原因是析构函数会释放掉字符串资源,而st1和st2两个对象共享了name资源,析构的时候就会两次释放该资源,于是导致了错误。所以,在编写拷贝构造函数时,为了避免出现这样的错误发生,应使用深度拷贝构造函数。
上面的例子稍加改动拷贝构造函数即可:

CStudent::CStudent(CStudent &stu)
{
                 this->id = stu .id;
                 int length = strlen(stu .name);
                 this->name =new char  [length + sizeof(char )];/*在堆上开辟一段缓冲区*/
                strncpy_s( this->name, length+sizeof (char), stu.name, length);/*将字符串copy到申请的缓冲区中*/
                 /*this->name = stu.name;*/
}

我们需要分析的参数对象产生时的汇编形式,看看拷贝构造函数被调用时发生了什么:
    CStudent stu2 (stu) ;
00E71680  push        8 
00E71682  lea         ecx,[stu2 ] 
00E71685  call        CStudent ::__autoclassinit2 (0E711E0h) 
00E7168A  lea         eax,[stu ] 
00E7168D  push        eax 
00E7168E  lea         ecx,[stu2 ] 
00E71691  call        CStudent ::CStudent (0E710FAh)  
可以看到,编译器会首先为stu2分配8字节的栈空间,并且做初始化工作,接着使用stu对象作为参数传递到构造函数中。
我们为上面的例子中加一个函数:
void InforStu(CStudent stu)
{
                cout<< "id = " << stu .id << endl;
                cout << "name:" << stu .name << endl;
}
这个函数将CStudent对象作为参数,那么在调用的时候自然会生成对象的拷贝,自然会调用拷贝构造函数,来看一下反汇编代码:
    InforStu(stu2 );
003262CA  sub         esp,8  
003262CD  mov         ecx,esp;新对象的this指针
003262CF  mov         dword ptr [ebp -0F8h], esp 
003262D5  lea         eax,[stu2 ];stu2对象  
003262D8  push        eax 
003262D9  call        CStudent ::CStudent (032123Fh);调用拷贝构造函数
003262DE  mov         dword ptr [ebp -100h], eax ;返回this指针
003262E4  call        InforStu (0321005h)  
从反汇编代码中可以看到,编译器会在栈底弄出一部分空间,使用拷贝构造函数新建一个对象,函数会使用这个新建的对象作为参数。

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 0
支持
分享
最新回复 (12)
雪    币: 719
活跃值: (777)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
2
学习了  很仔细 谢谢!
2014-12-11 20:39
0
雪    币: 274
活跃值: (88)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
很详细 ,
2014-12-11 23:29
0
雪    币: 3
活跃值: (47)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
堆对象创建时,会首先使用new函数申请堆空间,如果申请成功,则执行构造函数,返回首地址,如果不成功,则不执行构造函数,返回为空。既然该指针为空,那么可以想象得到,如果此时引用成员函数,就会引发段错误。
楼主这段话不准确,指针为空可以引用成员函数的,只要成员函数里面不访问成员变量就不会有问题,这种情况如果引发错误是因为成员函数访问了成员变量
2014-12-12 11:53
0
雪    币: 15
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
谢谢指正,已改正。
2014-12-12 16:15
0
雪    币: 1305
活跃值: (228)
能力值: ( LV5,RANK:75 )
在线值:
发帖
回帖
粉丝
6
学习了,感谢楼主啊,分析的很仔细,再次感谢
2014-12-15 09:41
0
雪    币: 468
活跃值: (52)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
c++是高级语言,才有对对象,封装的概念,每个函数有其层次结构,每个类都有其父代模型,这是认为的建立一种层次逻辑结构,便于人类进行编程,归类,但是汇编语言是面向机器的,c++经过编译以后,就没有任何层次结构了,湖边语言里面各种函数都是平行关系,互相call来call去真的是错综复杂,根本理不清楚。
2014-12-15 10:05
0
雪    币: 0
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
感谢楼主的分享
2014-12-15 11:16
0
雪    币: 3542
活跃值: (1867)
能力值: ( LV6,RANK:93 )
在线值:
发帖
回帖
粉丝
9
msvc的this是ecx,gcc是入栈。构造inline的话就是直接把函数实现放在那里,否则就是一个call。
2014-12-15 11:43
0
雪    币: 43
活跃值: (40)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
认清了this指针,其他就好办了
2014-12-16 10:56
0
雪    币: 188
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
好帖,感谢分享。
2014-12-17 16:03
0
雪    币: 21
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
可以继续分析下初始化列表以及有继承关系的构造
2014-12-22 15:32
0
雪    币: 52
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
讲的非常仔细。
2014-12-24 09:20
0
游客
登录 | 注册 方可回帖
返回
//