“逆向一个非常有意思的小程序”的非汇编解读

首先附上原文地址

http://bbs.hackav.com/thread-1026-1-1.html

文中讨论的一段代码的输出结果问题,具体代码如下:

#include <iostream>
using namespace std;

//男人类 
class boy
{ 
    public:  //他敞开胸怀 
    void LoveYou() 
    { 
        cout << "Jessica, I love you, marry me!" << endl; 
    } 
    void KissYou() 
    { 
        cout << "Jessica, you are too sexy, I want to kiss you..." << endl; 
    } 
};

//上帝说女人是男人的肋骨做的,所以女人就由男人派生出来吧……
class girl : public boy 
{ 
public:    //她也敞开胸怀……我什么都没看见 -_-! 
    void LoveYou() 
    { 
        cout << "I love you too...But I am too shy to say..." << endl; 
    } 
    void KissYou() 
    { 
        cout << "You do what you want..." << endl; 
    } 
};

void main(void) 
{ 
    boy  A1Pass;  //一个男孩叫A1Pass ^_^
    girl Jessica; //一个女孩叫杰西卡

    boy  *the_A1Pass_home  = &Jessica;
    girl *the_Jessica_home = &Jessica;

    //
    //------他们在干什么呀?请写出结果---------

    the_A1Pass_home ->LoveYou(); 
    the_Jessica_home->LoveYou();

    the_A1Pass_home ->KissYou(); 
    the_Jessica_home->KissYou();

    //
}

请读者自行思考一下

输出的结果会是什么呢

请不要看结果 先自己想一会



----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------



结果如下:

 Jessica, I love you, marry me! 
 I love you too...But I am too shy to say... 
 Jessica, you are too sexy, I want to kiss you... 
 You do what you want.. 

假如跟你的预料有所出入 那么请继续往下看


原文主要采用的是逆向分析看汇编代码的方式来解答

很多人根本没有接触过汇编语言

所以我准备从C++的机制上来解释一下这个问题

首先我们应该知道C++的继承派生机制

C++的类是可以通过继承产生出很多新的类的

在C++中,数据成员可以分为公有、私有以及保护类型

继承也一样,也有公有、私有以及保护三种类型。

下面是一张继承方式与继承后数据成员的属性变化关系表

  public protected private
共有继承 public protected 不可见
私有继承 private private 不可见
保护继承 protected protected 不可见
可以看到 public的优先级是最低的,public属性的数据成员会被每一种继承方式所继承,而且public属性就会被不同的继承方式所修改。

protected的优先级是中等,它也可以被每一种继承方式继承,而且它会改变基类中public成员的属性,基类中protected的成员会被private的继承方式所改变属性。

private是最高级的,它不能被任何一种继承方式所继承,它只存在于该类中。


回到我们的程序上来

可以看到boy类是一个基类,boy类中的两个函数都是public的

girl是boy的一个派生类,其继承方式为public,通过上面的表格可以看到,基类boy中的public部分应当都被girl类所继承,而且依然是public属性

看看我们的定义

    boy  A1Pass;  //一个男孩叫A1Pass ^_^
    girl Jessica; //一个女孩叫杰西卡

    boy  *the_A1Pass_home  = &Jessica;
    girl *the_Jessica_home = &Jessica;


在定义时我们定义了两个指针分别是boy类型与girl类型,并且都指向Jessica

    the_A1Pass_home ->LoveYou(); 
    the_Jessica_home->LoveYou();
    the_A1Pass_home ->KissYou(); 
    the_Jessica_home->KissYou();
看我们main函数中的函数调用部分,大家都指向Jessica,似乎the_A1Pass_home ->LoveYou()和the_Jessica_home->LoveYou()应该是一样的

the_A1Pass_home ->KissYou()和the_Jessica_home->KissYou()也应该是一样的

但是为什么输出结果却不是这样

记得公有继承会继承些什么吗?我们上面说过了

会继承基类的公有和保护部分,我们的基类也就是boy类中的函数是什么类型来着,不就是public么

那么实际上Jessic这个对象中实际有四个函数,分别是:

A1Pass::LoveYou() A1Pass::KissYou()LoveYou()KissYou()

所以当我们执行the_A1Pass_home ->LoveYou()和the_A1Pass_home ->KissYou()时,它们实际调用的是A1Pass::LoveYou()和A1Pass::KissYou()这两个函数,因为该指针就是boy类型的

那么出现上面我写到的输出结果也就不奇怪了。

我们可以改一改,把boy  *the_A1Pass_home  = &Jessica;girl *the_Jessica_home = &Jessica;改为

boy  *the_A1Pass_home  = &A1Pass;girl *the_Jessica_home = &A1Pass;

发现根本就不能通过编译,为什么呢

在C++语法里规定:基类指针可以指向一个派生类对象,但派生类指针不能指向基类对象。

我们可以通过一个比喻来理解,类的继承就是表示一种“继承类是基类中的更具体的东西”的关系,我们可以说人是基类,男人女人是派生类,

人指向男人女人肯定错不了,单独用男人或者女人去指人,就是不恰当的。

下面这段是我在知乎上看到的,关于基类指针能否指向派生类,很有道理:

著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
作者:浮生空
链接:http://www.zhihu.com/question/25572937/answer/34439248
来源:知乎

可以指向,但是无法使用不存在于基类只存在于派生类的元素。(所以我们需要虚函数和纯虚函数)
原因是这样的:
在内存中,一个基类类型的指针是覆盖N个单位长度的内存空间。
当其指向派生类的时候,由于派生类元素在内存中堆放是:前N个是基类的元素,N之后的是派生类的元素。
于是基类的指针就可以访问到基类也有的元素了,但是此时无法访问到派生类(就是N之后)的元素。


本来这篇文章到这里就结束了,可是我看到A1Pass在博客中写了一段话,让我查了一下午才弄懂,这段话是这么说的:

因此我特意用以下两种方式做了试验,程序运行结果完全一样。
    boy  *the_A1Pass_home  = &A1Pass;
    girl *the_Jessica_home = (girl *)&A1Pass; 
    与
    boy  *the_A1Pass_home  = NULL;
    girl *the_Jessica_home = NULL; 




最上面那种情况,即

 boy  *the_A1Pass_home  = &A1Pass;
 girl *the_Jessica_home = (girl *)&A1Pass; 

这就相当于the_A1Pass_home这个指针指向了A1Pass对象的地址

而the_Jessica_home这个指针指向了另一个指针 

/*

关于girl*the_Jessica_home=(girl*)&A1Pass这个地方我也不太清楚是怎么回事

主要是等号后面(girl*)&A1Pass这个部分我不太确定是什么意思 我认为(girl*)&A1Pass是把A1Pass的地址给强制转换成指向girl的指针类型了

但是没法确定

*/

这里涉及到指针的问题,于是我想到大一下的一个pdf叫“让你不再害怕指针”还从没认真看过

下面摘抄一些上面关于指针的基础知识,弄清指针,需要弄清指针的四个方面:

1.指针的类型
从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分

就是这个指针的类型。这是指针本身所具有的类型。让我们看看例一中

各个指针的类型:
(1)int*ptr;//指针的类型是int*
(2)char*ptr;//指针的类型是char*
(3)int**ptr;//指针的类型是int**
(4)int(*ptr)[3];//指针的类型是int(*)[3]
(5)int*(*ptr)[4];//指针的类型是int*(*)[4]
怎么样?找出指针的类型的方法是不是很简单?
2.指针所指向的类型
当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了
编译器将把那片内存区里的内容当做什么来看待。
从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉

剩下的就是指针所指向的类型。例如:

(1)int*ptr; //指针所指向的类型是int
(2)char*ptr; //指针所指向的的类型是char
(3)int**ptr; //指针所指向的的类型是int*
(4)int(*ptr)[3]; //指针所指向的的类型是int()[3]
(5)int*(*ptr)[4]; //指针所指向的的类型是int*()[4]
在指针的算术运算中,指针所指向的类型有很大的作用。
指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。

当你对C 越来越熟悉时,你会发现,把与指针搅和在一起的"类型"这个概念分成
"指针的类型"和"指针所指向的类型"两个概念,是精通指针的关键点之一。
我看了不少书,发现有些写得差的书中,就把指针的这两个概念搅在一起了,
所以看起书来前后矛盾,越看越糊涂。
3.指针的值----或者叫指针所指向的内存区或地址
指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,

而不是一个一般的数值。在32 位程序里,所有类型的指针的值都是一个32 位
整数,因为32 位程序里内存地址全都是32 位长。指针所指向的内存区就
是从指针的值所代表的那个内存地址开始,长度为si zeof(指针所指向的类型)的

一片内存区。以后,我们说一个指针的值是XX,就相当于说该指针指
向了以XX 为首地址的一片内存区域;我们说一个指针指向了某块内存区域,
就相当于说该指针的值是这块内存区域的首地址。
指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在例
一中,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向
的内存区是不存在的,或者说是无意义的。
以后,每遇到一个指针,都应该问问:这个指针的类型是什么?指针指
的类型是什么?该指针指向了哪里?(重点注意)
4 指针本身所占据的内存区
指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下
就知道了。在32 位平台里,指针本身占据了4 个字节的长度。
指针本身占据的内存这个概念在判断一个指针表达式(后面会解释)是
否是左值时很有用。

当看到下面这一段的时候,我知道了(girl*)&A1Pass的意思

如果有一个指针p,我们需要把它的类型和所指向的类型改为
TYEP *TYPE, 那么语法格式是: (TYPE *)p;
这样强制类型转换的结果是一个新指针,该新指针的类型是TYPE *,

它指向的类型是TYPE,它指向的地址就是原指针指向的地址,
而原来的指针p 的一切属性都没有被修改。(切记)
一个函数如果使用了指针作为形参,那么在函数调用语句的实参和
形参的结合过程中,必须保证类型一致,否则需要强制转换


(girl*)&A1Pass是一个girl*类型的指针,它指向girl类型,指针指向的地址是&A1Pass,等号右边的(girl*)代表着一种强制类型转换

这个表达式则代表着用girl类型来解释&A1Pass中的数据

也许你又会产生疑问, &A1Pass是基类,它的地址中不应该有派生类girl中的函数,调用更是无从说起,这是怎么回事呢

这涉及到C++在物理层面的优化

谭浩强老师在《C++面向对象程序设计(第二版)》中成员函数的存储方式那一节写道:

用类定义对象时,系统会为每一个对象分配存储空间。如果一个类包括了数据和函数,按理说,要分别为数据和函数代码(指经过编译的目标代码)分配存储空间。如果用同一个类定义了10个对象,那么是否需要为每一个对象的数据和函数代码分别分配存储单元,并把它们封装在一起呢?

事实上不是这样的。经过分析可知:同一类的不同对象中的数据成员的值一般是不相同的,而不同对象的函数的代码是相同的,不论调用哪一个对象的函数的代码,其实调用的都是同样内容的代码。既然这样,在内存中开辟10段空间来分别存放10个相同内容的函数代码段,显然是不必要的。人们自然会想,能否只用一段空间来存放这个共同的函数的目标代码,在调用各对象的函数时,都去调用这个公用的函数代码。

C++编译系统正是这样做的,因此每个对象所占用的存储空间只是该对象的数据成员所占用的存储空间,而不包括函数代码所占用的存储空间。

…………

那么,就发生了一个问题:不同的对象使用的是同一个函数代码段,它怎么能够分别对不同对象中的数据进行操作呢?原来C++为此专门设立了一个名为this的指针,用来指向不同的对象,当调用对象studl的成员函数时,this指针就指向studl,成员函数访问的就是studl的成员。当调用对象stud2的成员函数时,this指针就指向stud2,此时成员函数访问的就是stud2。

关于this指针的内容可以查书3.5.3节


现在明白了吗,&A1Pass地址中存储的,是boy类中的数据成员。(girl*)&A1Pass的真正意义就是用girl类的中的函数去解释&A1Pass中的数据。在我们这里定义的boy类中,没有数据成员,更没有成员函数与数据成员的交互,所以它指向的是什么根本就不重要,只要在语法上能编译通过就可以了。

这正好也能解释最后一种情况,为什么boy  *the_A1Pass_home  = NULL;girl *the_Jessica_home = NULL;都能成立,即使它们指向了NULL

我又自己改了改,改成了boy  *the_A1Pass_home =(boy*)1 ;girl *the_Jessica_home =(girl*)12345;通过编译,输出结果依旧不变。

这又进一步验证了我的想法,在成员函数与数据成员没有交互的情况下,右值只要跟左值的类型对应即可,至于具体数值并不影响输出结果。

假如类中有数据成员且有成员函数与数据成员的交互,那么这个程序就很有可能崩溃。

因为大家的函数都是公用的,假如成员函数要和数据成员交互的话,

看过this指针的作用你就会知道,程序是通过this指针去具体的对象中寻找值,然后再传入函数

那我们写一个比如boy *example=NULL或者boy *example=(boy*)12345,

this指针一旦找不到所需要的值,那么程序就会报错崩溃。



写到这里,我想到大家对这个程序的运行结果应该不会再感到诧异了

想真正了解C++,还有很长的路要走,其中,硬件的知识也是必不可少的。


















评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值