首页
论坛
课程
招聘
[原创]从反汇编的角度学C/C++之类的基本概念
2021-10-1 21:08 4614

[原创]从反汇编的角度学C/C++之类的基本概念

2021-10-1 21:08
4614

一.类的基本数据形式

    类作为C++中最具特色的一个语法工具,为我们的编程带来了许多的便利。我们知道一个类可以保存多个不同的数据类型,那么一个类在内存中的数据分别是如何表现得。他和结构体又有什么异同点。首先我们定义一个最简单得类如下所示

class Base
{
public:
	int x;
	int y;
};

    然后我们在main中对其进行赋值查看在内存中得保存状态。

	Base base;

	base.x = 1;
00571038  mov         dword ptr [ebp-0Ch],1      //为x赋值为1
	base.y = 2;
0057103F  mov         dword ptr [ebp-8],2        //为y赋值为2

    可以看到在内存中,其实类只是按照类中定义得数据按顺序存储我们的数据。而base这个变量则是我们这个类的首地址,和结构体的保存形式是一样的。事实上类和结构体唯一的区别只是结构体中声明的变量和方法是public而类是private。

    类中的数据既然有public和private,那他们在内存中的保存形式是否不同。

class Base
{
public:
	int x;
	void init()
	{
		x = 1;
		y = 2;
	}
private:
	int y;
};

    将上述定义的y设置为私有变量,使用init函数对其进行赋值,对应的反汇编代码如下:

0027101F  pop         ecx                      //取出类的地址
00271020  mov         dword ptr [ebp-8],ecx    //并把相应的地址赋给局部变量
00271023  mov         ecx,302008h  
00271028  call        002712C0  
		x = 1;
0027102D  mov         eax,dword ptr [ebp-8]   //从局部变量中取出地址赋给eax
00271030  mov         dword ptr [eax],1       //此时eax就是base.x的地址
		y = 2;
00271036  mov         eax,dword ptr [ebp-8]  
00271039  mov         dword ptr [eax+4],2    //而eax+4就是base.Y的地址

    从上可以看出无论是public还是private类型,在内存中并没用什么区别,由此可以知道所谓的公有变量或者私有变量都是编译器的检查罢了。如果我们想访问这些私有变量,只需要算出相应地址就可以,如下。

	Base base;

	base.init();

	int *test = (int *)&base;    //test指针指向base

	test++;                      //第一个是public的x往前走4个字节就是y
	printf("%d\n", *test);       //输出y的值

    对应反汇编如下:

	int *test = (int *)&base;
00D71150  lea         eax,[base]              //取出base地址
00D71153  mov         dword ptr [test],eax    //放入test

	test++;
00D71156  mov         eax,dword ptr [test]    //此时test的值就是base的地址
00D71159  add         eax,4                   //由于是int *类型所以+1地址会+4,此时指向的就是y
00D7115C  mov         dword ptr [test],eax    
	printf("%d\n", *test);
00D7115F  mov         eax,dword ptr [test]    //取出test的值,这个值就是y的地址
00D71162  mov         ecx,dword ptr [eax]     //从y的地址中取出数据
00D71164  push        ecx                      //开始压栈输出
00D71165  push        offset string "%d\n" (0DE31B0h)  
00D7116A  call        printf (0D711C0h)  
00D7116F  add         esp,8

    最后就通过这种方法顺利输出了private变量,结果如下

            

二.类中的成员函数

    每个类都可以定于相应的成员函数,这些函数和普通函数的区别是否有不同。为上述的定义增加一个成员函数如下

class Base
{
public:
	int x;
	int y;
	void test()
	{
		printf("Base test\n");
	}
};

    接下来在main中调用函数并查看反汇编形式来区别成员函数与普通函数如下

	Base base;

	base.test();
003D1138  lea         ecx,[base]              //取出base类的地址赋值给ecx
003D113B  call        Base::test (03D1000h)   //调用函数

    可以看到对于成员函数的调用,唯一的区别就是在调用之前编译器会把类的内存地址赋给ecx。test函数的反汇编如下

	void test()
	{
003D1000  push        ebp  
003D1001  mov         ebp,esp  
003D1003  sub         esp,0CCh  
003D1009  push        ebx  
003D100A  push        esi  
003D100B  push        edi  
003D100C  push        ecx                         //压入ecx,此时ecx保存的就是先前的类地址 
003D100D  lea         edi,[ebp-0CCh]  
003D1013  mov         ecx,33h  
003D1018  mov         eax,0CCCCCCCCh  
003D101D  rep stos    dword ptr es:[edi]  
003D101F  pop         ecx                        //取出ecx
003D1020  mov         mov dword ptr [ebp-8],ecx   //将ecx也就是类的地址保存在局部变量
003D1023  mov         ecx,offset _D3472036_test@cpp (0462008h)  
003D1028  call        __CheckForDebuggerJustMyCode (03D13D0h)  
		printf("Base test\n");
003D102D  push        offset string "Base test\n" (04431B0h)  
003D1032  call        printf (03D1190h)  
003D1037  add         esp,4  
	}
003D103A  pop         edi  
003D103B  pop         esi  
003D103C  pop         ebx  
003D103D  add         esp,0CCh  
003D1043  cmp         ebp,esp  
003D1045  call        _RTC_CheckEsp (03D1390h)  
003D104A  mov         esp,ebp  
003D104C  pop         ebp  
003D104D  ret

     从上可以看出,成员函数和我们平时写的普通函数的区别就在于在调用成员函数的时候,编译器首先会把类的地址赋给ecx。而在函数执行的过程中,会首先把这个地址给到局部变量。那么这么做的目的是为何,保存的这个地址有何作用?

    根据上面的讲解我们知道类所定义的变量最后在内存中会按顺序保存着,而要找到这些变量的地址,我们首先就需要得到类在内存中的地址。而这个地址就是根据ecx来进行传递。修改上面类的定义如下:

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

    此时我们需要在main中调用InitData来为我们的成员变量赋值。对应的反汇编代码如下

	Base base;
	int x = 3, y = 4;
00C41098  mov         dword ptr [x],3  
00C4109F  mov         dword ptr [y],4          //分别为x,y赋值

	base.InitData(x, y);
00C410A6  mov         eax,dword ptr [y]  
00C410A9  push        eax  
00C410AA  mov         ecx,dword ptr [x]  
00C410AD  push        ecx                      //首先将参数入栈
00C410AE  lea         ecx,[base]               //将类的地址保存在ecx中
00C410B1  call        Base::InitData (0C41000h)

    从上面可以看出,我们对InitData调用的时候,首先将两个参数压入栈中,然后把类变量的地址赋给ecx,函数的反汇编如下

        void InitData(int x, int y)
	{
00C41000  push        ebp  
00C41001  mov         ebp,esp  
00C41003  sub         esp,0CCh  
00C41009  push        ebx  
00C4100A  push        esi  
00C4100B  push        edi  
00C4100C  push        ecx                 //将ecx的值压入栈中
00C4100D  lea         edi,[ebp-0CCh]  
00C41013  mov         ecx,33h  
00C41018  mov         eax,0CCCCCCCCh  
00C4101D  rep stos    dword ptr es:[edi]          //初始化栈空间
00C4101F  pop         ecx                 //将ecx的值恢复
00C41020  mov         dword ptr [this],ecx       //将ecx的值赋给局部变量,只是这个值就是类变量的地址
00C41023  mov         ecx,offset _D3472036_test@cpp (0CD2008h)  
00C41028  call        __CheckForDebuggerJustMyCode (0C412D0h)  
		this->x = x;
00C4102D  mov         eax,dword ptr [this]        //取出局部变量的值给eax,此时eax保存了类变量的地址
00C41030  mov         ecx,dword ptr [x]         //将传入的参数x赋给ecx
00C41033  mov         dword ptr [eax],ecx        //将ecx赋值给eax所代表的地址,此时这个地址就代表了Base中的x
		this->y = y;                
00C41035  mov         eax,dword ptr [this]          
00C41038  mov         ecx,dword ptr [y]         //将传入的参数y赋给ecx
00C4103B  mov         dword ptr [eax+4],ecx       //根据类的定义eax+4的地址代表的就是Base中的y
	}
00C4103E  pop         edi  
00C4103F  pop         esi  
00C41040  pop         ebx  
00C41041  add         esp,0CCh  
00C41047  cmp         ebp,esp  
00C41049  call        _RTC_CheckEsp (0C41290h)  
00C4104E  mov         esp,ebp  
00C41050  pop         ebp  
00C41051  ret         8

    从上面可以看出在调用成员函数之前将类变量的地址赋给ecx是为了在成员函数中可以顺利找到类变量的地址,以此来找到相应的数据进行操作。

三.函数重载

    编写C++程序时候,我们经常需要用到函数重载这个特性,那么函数重载究竟是如何实现,如何做到同名函数不同功能。修改上述类定义如下

class Base
{
public:
	void InitData(int x, int y)
	{
		this->x = x;
		this->y = y;
		this->z = 0;
	}

	void InitData(int x, int y, int z)
	{
		this->x = x;
		this->y = y;
		this->z = z;
	}
private:
	int x;
	int y;
	int z;
};

    在main函数中我们分别调用两个函数,反汇编如下

	Base base;
	int x = 3, y = 4, z = 5;
00E410F8  mov         dword ptr [x],3  
00E410FF  mov         dword ptr [y],4  
00E41106  mov         dword ptr [z],5  

	base.InitData(x, y);
00E4110D  mov         eax,dword ptr [y]  
00E41110  push        eax  
00E41111  mov         ecx,dword ptr [x]  
00E41114  push        ecx  
00E41115  lea         ecx,[base]  
00E41118  call        Base::InitData (0E41000h)      //对第一个函数的调用,地址为0x0E41000
	base.InitData(x, y, z);
00E4111D  mov         eax,dword ptr [z]  
00E41120  push        eax  
00E41121  mov         ecx,dword ptr [y]  
00E41124  push        ecx  
00E41125  mov         edx,dword ptr [x]  
00E41128  push        edx  
00E41129  lea         ecx,[base]  
00E4112C  call        Base::InitData (0E41060h)   //对第二个函数的调用,地址为0x0E41060

    可以看到对于重载的函数的调用,编译器这里产生的两个call的地址是完全不一样的。在内存中分别查看这两个地址如下

    这两个地址分别代表了两个函数的在内存中的位置,所以所谓的函数重载就只是编译器为我们生成了不同的函数而已。

四.运算符重载

    在C/C++中允许我们使用运算符重载技术来简化我们的开发,那么运算符重载是如何实现的,修改类定义如下

class Base
{
public:
	Base(int x, int y)
	{
		this->x = x;
		this->y = y;
	}
	void operator++()
	{
		this->x++;
		this->y++;
	}
	void Show()
	{
		printf("x=%d, y=%d\n", this->x, this->y);
	}
private:
	int x;
	int y;
};

    以上实现了对++的重载,我们在main函数中查看编译器的实现如下:

	Base base(3, 4);
00C91208  push        4  
00C9120A  push        3  
00C9120C  lea         ecx,[base]  
00C9120F  call        Base::Base (0C91000h)      //类变量初始化

	base.Show();
00C91214  lea         ecx,[base]              
00C91217  call        Base::Show (0C910C0h)      //两个输出函数用来显示结果
	++base;
00C9121C  lea         ecx,[base]  
00C9121F  call        Base::operator++ (0C91060h)  //将类变量传递给ecx后调用函数
	base.Show();
00C91224  lea         ecx,[base]  
00C91227  call        Base::Show (0C910C0h)

    由上可以看出,所谓的运算符重载,其实就是编译器为我们实现了一个成员函数,这个成员函数的功能就是我们所写的运算符重载的函数,这样当相应的类实例进行运算时候就会调用相应的函数。我们接着看实现的函数。

	void operator++()
	{
00C91060  push        ebp  
00C91061  mov         ebp,esp  
00C91063  sub         esp,0CCh  
00C91069  push        ebx  
00C9106A  push        esi  
00C9106B  push        edi  
00C9106C  push        ecx                  //保存ecx
00C9106D  lea         edi,[ebp-0CCh]  
00C91073  mov         ecx,33h  
00C91078  mov         eax,0CCCCCCCCh  
00C9107D  rep stos    dword ptr es:[edi]  
00C9107F  pop         ecx                  //取出ecx
00C91080  mov         dword ptr [this],ecx  //将ecx保存到局部变量
00C91083  mov         ecx,offset _D3472036_test@cpp (0D22008h)  
00C91088  call        __CheckForDebuggerJustMyCode (0C914B0h)  
		this->x++;
00C9108D  mov         eax,dword ptr [this]  //取出局部变量,此时这个值是类变量地址
00C91090  mov         ecx,dword ptr [eax]    //取出地址偏移0的地方,即x的数值  
00C91092  add         ecx,1                  //将x数值+1
00C91095  mov         edx,dword ptr [this]   //取出类变量地址给edx
00C91098  mov         dword ptr [edx],ecx    //将加一以后的数值赋给x完成自加操作
		this->y++;
00C9109A  mov         eax,dword ptr [this]  
00C9109D  mov         ecx,dword ptr [eax+4]  
00C910A0  add         ecx,1  
00C910A3  mov         edx,dword ptr [this]  
00C910A6  mov         dword ptr [edx+4],ecx      //同上
	}
00C910A9  pop         edi  
00C910AA  pop         esi  
00C910AB  pop         ebx  
00C910AC  add         esp,0CCh  
00C910B2  cmp         ebp,esp  
00C910B4  call        _RTC_CheckEsp (0C91470h)  
00C910B9  mov         esp,ebp  
00C910BB  pop         ebp  
00C910BC  ret

       可以看到,实现的函数便是我们写的函数,完成的就是类成员变量的自增操作,以上便是类的运算符重载。最后的程序运行结果如下

                                

五.模板

    模板同样作为C++的一个强大的语法工具被广泛使用,修改类定义如下。

template <class T>
class Base
{
public:
	void InitData(T x, T y)
	{
		this->x = x;
		this->y = y;
	}
private:
	T x;
	T y;
};

       接下来通过对main函数的进行反汇编查看模板的实现究竟是如何的

	Base<int> iBase;
	Base<char> cBase;

	iBase.InitData(3, 4);
00A010F8  push        4                              
00A010FA  push        3                                //两个参数入栈
00A010FC  lea         ecx,[iBase]                      //取出iBase地址赋给ecx
00A010FF  call        Base<int>::InitData (0A01060h)   //调用函数
	cBase.InitData('a', 'b');
00A01104  push        62h  
00A01106  push        61h                              //两个参数入栈
00A01108  lea         ecx,[cBase]                      //取出cBase地址赋给ecx
00A0110B  call        Base<char>::InitData (0A01000h)  //调用函数

    可以看到和函数重载非常类型,这里对于同一个函数也是有两个地址的调用,分别查看这两个地址可以看到如下内容

	void InitData(T x, T y)
	{
00A01060  push        ebp  
00A01061  mov         ebp,esp  
00A01063  sub         esp,0CCh  
00A01069  push        ebx  
00A0106A  push        esi  
00A0106B  push        edi  
00A0106C  push        ecx  
00A0106D  lea         edi,[ebp-0CCh]  
00A01073  mov         ecx,33h  
00A01078  mov         eax,0CCCCCCCCh  
00A0107D  rep stos    dword ptr es:[edi]  
00A0107F  pop         ecx  
00A01080  mov         dword ptr [this],ecx  
00A01083  mov         ecx,offset _D3472036_test@cpp (0A92008h)  
00A01088  call        __CheckForDebuggerJustMyCode (0A01340h)  
		this->x = x;
00A0108D  mov         eax,dword ptr [this]                 
00A01090  mov         ecx,dword ptr [x]  
00A01093  mov         dword ptr [eax],ecx                   //注意这里是dword,也就是说4字节的赋值
		this->y = y;
00A01095  mov         eax,dword ptr [this]                  
00A01098  mov         ecx,dword ptr [y]  
00A0109B  mov         dword ptr [eax+4],ecx                 //同样这里也是
	}
00A0109E  pop         edi  
00A0109F  pop         esi  
00A010A0  pop         ebx  
00A010A1  add         esp,0CCh  
00A010A7  cmp         ebp,esp  
00A010A9  call        _RTC_CheckEsp (0A01300h)  
00A010AE  mov         esp,ebp  
00A010B0  pop         ebp  
00A010B1  ret         8
	void InitData(T x, T y)
	{
00A01000  push        ebp  
00A01001  mov         ebp,esp  
00A01003  sub         esp,0CCh  
00A01009  push        ebx  
00A0100A  push        esi  
00A0100B  push        edi  
00A0100C  push        ecx  
00A0100D  lea         edi,[ebp-0CCh]  
00A01013  mov         ecx,33h  
00A01018  mov         eax,0CCCCCCCCh  
00A0101D  rep stos    dword ptr es:[edi]  
00A0101F  pop         ecx  
00A01020  mov         dword ptr [this],ecx  
00A01023  mov         ecx,offset _D3472036_test@cpp (0A92008h)  
00A01028  call        __CheckForDebuggerJustMyCode (0A01340h)  
		this->x = x;
00A0102D  mov         eax,dword ptr [this]                  
00A01030  mov         cl,byte ptr [x]  
00A01033  mov         byte ptr [eax],cl              //而这里的赋值确实byte的一字节的赋值
		this->y = y;
00A01035  mov         eax,dword ptr [this]  
00A01038  mov         cl,byte ptr [y]  
00A0103B  mov         byte ptr [eax+1],cl            //这里也是
	}
00A0103E  pop         edi  
00A0103F  pop         esi  
00A01040  pop         ebx  
00A01041  add         esp,0CCh  
00A01047  cmp         ebp,esp  
00A01049  call        _RTC_CheckEsp (0A01300h)  
00A0104E  mov         esp,ebp  
00A01050  pop         ebp  
00A01051  ret         8

    由此我们可以得出结论,所谓的模板和我们的函数重载非常像,只是编译器为我们生成了相应的函数代码而已。


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

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