彻底解剖C++引用机制-汇编级别的深层探索

来源:互联网 发布:循环冗余检查算法 编辑:程序博客网 时间:2024/04/30 17:26

彻底解剖C++引用机制-汇编级别的深层探索

此文是我与陈翼男博士共同研究,此文深究其理,欲精通C++者不可不察。

C++的引用是一个非常重要的概念。它是被引用的变量的别名,它本身不是变量,它应该与被引用的变量代表同一个地址的内容;而对它的读写的方式却又与变量的一样,所以国内有的教科书甚至称其为‘引用变量’。引用到底是什么,它的内在实现是怎样的,只有剖析使用引用的C++程序的反汇编源码才能揭开它的神秘面纱。

1.一个旨在对比指针与引用内在实现的C++程序的反汇编源码的剖析

下面的C++程序的反汇编源码,是从VC++6.0的调试器的汇编窗口中拷贝而来。其中分号后面的汉字是笔者对汇编码的分析。在这里我们将会看到,编译器实际上是把引用作为指针变量来处理的,它确实占有独立的存储,编译器对引用的读写实际上是对这个‘指针变量’进行取内容运算‘*’后结果的读写。

--- C:\ProgramFiles\Microsoft Visual Studio\MyProjects\chen\my0.cpp -------------------------

1:   //引用机制与指针机制对比测试用例

2:   void main()

3:   {

0040B460  push       ebp

0040B461  mov        ebp,esp      ;使ebp指向当前栈顶,它将相当于本函数的数据现场的底

0040B463  sub        esp,4Ch      ;使esp向低址移76个字节,每单位4字节,共19个单位

0040B466  push       ebx             ;保存寄存器

0040B467  push       esi               ;保存寄存器

0040B468  push       edi              ;保存寄存器

0040B469  lea        edi,[ebp-4Ch]        ;准备循环填充本函数的数据现场,这是开始地址

0040B46C  mov        ecx,13h              ;这是循环次数,为19

0040B471  mov        eax,0CCCCCCCCh;这是用来填充每个单位的材料

0040B476  rep stos   dword ptr [edi]                ;开始填充,填充后的内存映像如图1所示

4:       inta='A';

0040B478  mov        dword ptr [ebp-4],41h ;如图2所示,ebp-4就是变量a

5:       int&ra=a; //旨在看引用如何联系变量a

0040B47F  lea        eax,[ebp-4]   ;以下请看图2所示

0040B482  mov        dword ptr [ebp-8],eax ebp-8就是引用ra,它与指针变量完全相同

6:       int *p=&a;//旨在看指针如何联系变量a,做一个对比

0040B485  lea        ecx,[ebp-4]

0040B488  mov        dword ptr [ebp-0Ch],ecx ;如图2所示,ebp-0Ch就是指针变量p

7:       ra='B';//旨在看如何通过引用读写变量a

0040B48B  mov        edx,dword ptr [ebp-8] ;这两条指令是把ra作为指针变量对待,-

0040B48E  mov        dword ptr [edx],42h  ;对这个‘指针变量’作取内容‘*’运算

8:       *p='C';//旨在看如何通过指针读写变量a,做一个对比

0040B494  mov        eax,dword ptr [ebp-0Ch] ;这两条指令才是真正的指针取内容运算,

0040B497  mov        dword ptr [eax],43h    ;与上面引用的两条指令完全相同

9:       p=&ra;//旨在看是否把a的地址赋值给指针p,请看稍后的分析

0040B49D  mov        ecx,dword ptr [ebp-8] ;这两条指令完全是把引用ra‘指针变量’的值-

0040B4A0  mov        dword ptr[ebp-0Ch],ecx ;直接赋值给指针变量p

10:  }

0040B4A3  pop        edi    ;以下为恢复栈平衡

0040B4A4  pop        esi

0040B4A5  pop        ebx

0040B4A6  mov        esp,ebp

0040B4A8  pop        ebp

0040B4A9  ret

 

C++的理论,对引用ra取址就是对变量a取址。但是我们从上面C++程序的第9行语句反汇编出的汇编码看出,并没有像我们所想象的把变量a的地址ebp-4直接赋值给指针变量p,按道理说编译器此时是完全知道变量a的地址。那么编译器为什么舍简而求繁呢?

笔者的一个猜测是为了追求一致性,是为了与引用作为函数形参的内在实现相一致。引用的用处主要是作为函数的形参,当这样的函数被调用的次数是多次时,一个形参所要引用的实参也就因此多矣,那么在函数体里出现的对引用取址运算被编译成哪个实参的地址呢?这是行不通的。但是,像现在这样把引用实际上作为指针来处理就行得通。请看下面的一个引用传参的C++程序反汇编码的分析。

2.一个旨在揭示C++程序引用传参内在实现的反汇编源码的剖析

--- C:\ProgramFiles\Microsoft Visual Studio\MyProjects\chen\my0.cpp ----------------

6:

7:   void main()

8:   {

0040B490  push       ebp

0040B491  mov        ebp,esp      ;使ebp指向当前栈顶,它将相当于本函数的数据现场的底

0040B493  sub        esp,44h       ;使esp向低址移68个字节,每单位4字节,共17个单位

0040B496  push       ebx             ;保存寄存器

0040B497  push       esi               ;保存寄存器

0040B498  push       edi              ;保存寄存器

0040B499  lea        edi,[ebp-44h]        ;准备循环填充本函数的数据现场,这是开始地址

0040B49C  mov        ecx,11h              ;这是循环次数,为17

0040B4A1  mov        eax,0CCCCCCCCh ;这是用来填充每个单位的材料

0040B4A6  rep stos   dword ptr [edi];填充,填充后内存映像如图1所示类似,仅少两个单位

9:       charch='A';

0040B4A8  mov        byte ptr [ebp-4],41h

10:      proc(ch);

0040B4AC  lea        eax,[ebp-4]          ;获得变量ch的地址

0040B4AF  push       eax                      ;传参,现在的esp-8就是形参r,它实际上竟是指针变量

0040B4B0  call       @ILT+30(proc) (00401023);开始调用,返回地址自动进栈,如图3所示

0040B4B5  add        esp,4

11:  }

0040B4B8  pop        edi

0040B4B9  pop        esi

0040B4BA  pop        ebx

0040B4BB  add        esp,44h

0040B4BE  cmp        ebp,esp

0040B4C0  call       __chkesp (0040b530)

0040B4C5  mov        esp,ebp

0040B4C7  pop        ebp

0040B4C8  ret

--- No sourcefile ------------------------------------------------------------------

--- C:\ProgramFiles\Microsoft Visual Studio\MyProjects\chen\my0.cpp -------------------------

1:   #include<stdio.h>

2:   voidproc(char &r)

3:   {

0040B460  push       ebp

0040B461  mov        ebp,esp      ;使ebp指向当前栈顶,如图4所示。注意,形参r等变量的

;地址相对于当前ebp中的值的相对位移变了

0040B463  sub        esp,40h       ;使esp向低址移64个字节,每单位4字节,共16个单位

0040B466  push       ebx             ;保存寄存器

0040B467  push       esi              ;保存寄存器

0040B468  push       edi              ;保存寄存器

0040B469  lea        edi,[ebp-40h]       ;准备循环填充本函数的数据现场,这是开始地址

0040B46C  mov        ecx,10h              ;这是循环次数,为16

0040B471  mov        eax,0CCCCCCCCh ;这是用来填充每个单位的材料

0040B476  rep stos   dword ptr [edi] ;填充,填充后的示意图于本问题无关,故省略

4:       r='B';

0040B478  mov        eax,dword ptr [ebp+8]  ;把变量r中的值取出来,其实是把r当作指针变量对待

0040B47B  mov        byte ptr [eax],42h        ;现在是对这个指针变量做取内容(*)运算后赋值

5:   }

0040B47E  pop        edi    ;以下为恢复栈平衡

0040B47F  pop        esi

0040B480  pop        ebx

0040B481  mov        esp,ebp

0040B483  pop        ebp

0040B484  ret

--- No sourcefile ---------------------------------------------------------------------------

3.结论

通过前面对反汇编码的分析我们清楚地看到,编译器实际上是把引用作为指针变量来对待,而它同时自动地把对引用的读写作为对这个‘指针变量’的取内容运算后得到的结果进行读写。那么,我们是否可以因此把引用称作是一种变量呢?笔者认为这万万是使不得的。这样做完全有悖于引用概念的定义,其原因是C++并没有把引用作为一种数据类型,既然不是数据类型那么引用名就不可能是变量。引用名仅仅与变量的名字有关系,它是一种别名。别名的地址必须看成是所引用的变量的地址,或者不准确但很方便地说,引用与所引用的变量同地址。笔者认为,编译器的实际做法与C++的理论不一致,完全是不得已而为之。打个比方说,电影实际上是由许多静止的图片实现的,但是我们必须抽象地说电影是动的。引用是什么?一言而蔽之,引用者,引用也。

4.附录

为了便于理解引用与指针实现上的相同,笔者编了两个程序,分别列出这两个程序的反汇编码。请注意观察,虽然C++程序不同,而它们的反汇编码却完全相同。

 

1:   void main()                                                                             1:    void main()  

2:   {                                                                                           2:    {

00401010  push       ebp                                                           00401010   push       ebp

00401011  mov        ebp,esp                                                    00401011   mov        ebp,esp

00401013  sub        esp,48h                                                     00401013   sub        esp,48h

00401016  push       ebx                                                           00401016   push       ebx

00401017  push       esi                                                            00401017   push       esi

00401018  push       edi                                                            00401018   push       edi

00401019  lea        edi,[ebp-48h]                                               00401019   lea        edi,[ebp-48h]

0040101C  mov         ecx,12h                                                   0040101C  mov         ecx,12h

00401021  mov        eax,0CCCCCCCCh                                   00401021   mov        eax,0CCCCCCCCh

00401026  rep stos   dword ptr [edi]                                              00401026   rep stos   dword ptr [edi]

3:       int a='A';                                                                         3:        int a='A';

00401028  mov        dword ptr [ebp-4],41h                                 00401028   mov        dword ptr [ebp-4],41h

4:       int &ra=a;                                                                        4:        int *p=&a;

0040102F  lea         eax,[ebp-4]                                                  0040102F  lea         eax,[ebp-4]

00401032  mov        dword ptr [ebp-8],eax                                  00401032   mov        dword ptr [ebp-8],eax

5:       ra='B';                                                                             5:        *p='B';

00401035  mov        ecx,dword ptr [ebp-8]                                  00401035   mov        ecx,dword ptr [ebp-8]

00401038  mov        dword ptr [ecx],42h                                    00401038   mov        dword ptr [ecx],42h

6:   }                                                                                           6:    }

0040103E  pop        edi                                                           0040103E   pop        edi

0040103F  pop         esi                                                           0040103F  pop         esi

00401040  pop        ebx                                                           00401040   pop        ebx

下面的两个C++程序的反汇编码也是完全相同的,笔者已经验证,限于篇幅没有列出,其分析无庸赘述。

 

void proc(char &r)                                                     void proc(char*p)

{                                                                                 {

        r='B';                                                                          *p='B';

}                                                                                 }

void main()                                                                 voidmain()

{                                                                                 {

        charch='A';                                                                char ch='A';

        proc(ch);                                                                    proc(&ch);

}      

                                                                          

----------------------------------------------作者声明-----------------------------------------

我是一个老头,65岁,编过10年程序,在大学教过15年书,写了一本书,名为《C++释难解惑》,这几年来一直在投稿。出版社对我的书稿评价很好,但他们就是由于经济上的风险。总是在出版的最后关头反悔。其中北京****出版社已经签字画押的情况下反悔;北京****出版社与我签过电子合同;书稿在**大学出版社放了9个月,还让我请名人写序言,最后说现在书很难卖,并且还给我发了个近半年的图书销售的excel文档让我看,希望我理解出版社。我只好仰天嗟叹了。

有编辑朋友建议我把书稿放到网上让读者试读,以印证读者的人数和满意度。我想这是一个办法,即使永远不能出版,也不至于烂在肚子里。并且我在网上也看到这个办法成功的先例,例如《C语言深度解剖》,后来就由北航出版社出版了。

C语言深度解剖》的作者采用的方法是把书稿上传到网上。我想采用化整为零的方法,每周起码在我的CSDN博客里发一篇文章,是书稿里的内容稍加改造的。之所以这样做确实出自于私心,就是想借此赚取CSDN的积分。因为我以前忙于教书,无暇经常光顾CSDN,所以几乎没有积分,这样是不能被大家所认识的。

博文与书稿上的文字可能有极少的字面上的不同。

如果您对我的书稿和博文感兴趣,请您推荐给您的朋友,谢谢。

本书稿已经国家版权保护中心进行了作品登记,登记号为:2010-A-023237

下面我把一些出版社给我的来信摘录作为参考。您可以看到,我的书名在不同的出版社和编辑面前改了好几次名了,现在书稿的名字是我自己的。

//-----------------------------------以下是**大学出版社编辑的来信---------

陈老师,

   选题我暂以《C++释疑解惑:课本没讲透的131个问题》的名字报上去了。通过审批应该问题不大。下面几点,是咱们电话里说过的,麻烦你着手进行啊:)

 

//-----------------------------------------------------------------------------------------------------------

陈老师好

从您的来信,我觉得您的文字驾驭能力挺好的,如果您的书,能写成您给我来信的风格,也不错,现在市场上还就缺这样的书。那种一步步讲操作,枯燥的原理,确实没人愿意看,而那种语言比较生动,穿插一些开发故事,体会心得之类的文风,让冰冷的技术活了起来,容易引起读者的共鸣。所以,建议书至少300页,写得生动些,多配些图,甚至每章最后都可以增加上一些问答,比如您历年教学收集到的经典问题等等。当然,其他的一些方法,也都可以尝试,我这里只是抛砖引玉。

 

//----------------------------------------------------------------------------------------------------------

尊敬的陈老师:您好!

由于负责这本书的编辑已经离职,这本书现由我负责与您沟通及出版事宜。

这本稿件从市场上鲜有的写作角度出发,揭示了C++的很多难点,有些内容具有一定的深度,同时又力求在写作上深入浅出,会有一定量的读者关注书中的问题,这是本书的一大特色

//-------------------------------------------------------

 

陈老师:您好!

 今天,就这部书稿又与领导进行了沟通。从这部书稿可看出,其中积淀了您多年的经验和大量心血,力图为读者学习C++排疑解惑,实属难能可贵。

 从现在的销售数据(开卷数据:专门的图书销售调查数据,采集自全国2000多家书店,随邮附上)看,目前市场上同类书整体销量不理想(月销200本以上较好),即便在这样的数据面前,相对好销的书还属于基础入门的书或教材。在此,也冒昧地想和您探讨,如何将此书出销好的办法,比如:您能帮助出版社做哪些宣传推广,或者利用院校资源推广销售,以及在零售书店怎样做宣传,才能让读者广泛知晓本书并认可,从而形成销售。

 

 

//------------------------------------------------------------------------------------------

 

陈老师:您好!

 

    从来信看到您对开卷销售数据进行了详细分析,相信您也看出零售图书市场的销售状况,即便是第1名的图书也不过160/月,假设书店与网店销量平分秋色,每月的总销量也只有320本,一年的销量为3840册,按计算机书的平均销售周期一年半计算,预计为5760册(纯零售不包括教材)。以此推算,当月销量在100本的书,预计3600册。这个销量如果没有教材的销量支撑,从出版社的成本计算基本上是没有什么利润的,如果计算上在途、退货等因素,稍有不慎恐怕就会亏本,这基本上就是纯零售书的状况。

 

    信中提到的几位作者的书,销量多则50多本,少则几本,如果纯靠零售销售的话,简单可以看出最终销量的情况。

 

    社里对您这本书的含金量一直都是肯定的,只是苦于店销图书的销售状况,也深知您的身体状况在写作上将要付出的巨大努力,觉得再让您改稿实在是有些不合适。

 

 

//-------------------**大学出版社电子出版物编辑的来信--------------------------

陈老师您好:

    我与图书编辑沟通了,就如您告诉我的,他对本书的质量不持疑义,主要考虑的是本书的经济效益。我们作为电子出版和网络出版部门,还没出过单独的网络出版物,这条路暂时不考虑。电子出版物5000左右的成本,包括生产成本和人力成本,要保证持平的话,恐怕要由作者承担。这是我向领导报选题时的一项。您作为作者,据我理解,如果没有相关经费支持,由您个人承担是有问题的。所以这个选题的前景还要您考虑。

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

参考书目

1.   []TedFaisonBorland C++3.1编程指南,蒋维杜等译,清华大学出版社,1993

2.   侯俊杰,深入浅出MFC,华中科技大学出版社,2001

3.   []JessLiberty21天学通C++,人民邮电出版社,2002

4.   [] StanleyB. Lippman,深度探索C++对象模型,侯捷译,华中科技大学出版社,第12007 

5.   张海藩等,软件工程,人民邮电出版社2010

6.   宛延闿,C++语言和面向对象程序设计,第二版,清华大学出版社1997

7.   网上下载的全国高自考上机考试题目

8.   陈树振、陈翼男,C++文本文件的一种读写方法,《电脑开发与应用》2008年第10期,中国北方自动控制研究所

9.   陈树振,CC++的图示教学法,《教学研究》,200510月,北京科技职业学院

10.             [] Peter Van Der Linden C专家编程,徐波译,人民邮电出版社,2002,12

 

 

 

原创粉丝点击