您现在的位置是:首页 > 正文

用汇编的眼光看C++(开篇)

2024-02-01 02:09:44阅读 3

 很多朋友,包括我自己在内,对C++语言的很多特性不是很明白。特别是几年前找工作的时候,为了应付来自工作单位的考试,我经常逼着自己的去记住一些复杂的试题和答案。可是常常时间已过,一切又回到了原点。原来没有弄清楚的问题还是没有弄明白,一切都没有发生改变。直到若干年后,当我在编码过程中不断积累经验,尝试用汇编代码和内存数据来解释一些现象的时候,才明白有些东西其实并不复杂。也许有的朋友对汇编语言会有畏惧,其实没有必要。只要你对C语言有一些基础,对堆栈有一些印象,那么你已经拥有汇编语言的基础了。在接下来的数篇博客中,我们就会就x86汇编、数据类型、数据运行逻辑、指针、数据、类、重载运算符在汇编下是如何展开的做一些介绍,谈一些个人的看法。下面,我们就进行一些小测试,同时用汇编语言来说明一下。大家可以一起做一下。


(1) char name[] 和 char* name

[cpp]  view plain  copy
  1. 1:  
  2. 2:    void process()  
  3. 3:    {  
  4. 00401020   push        ebp  
  5. 00401021   mov         ebp,esp  
  6. 00401023   sub         esp,4Ch  
  7. 00401026   push        ebx  
  8. 00401027   push        esi  
  9. 00401028   push        edi  
  10. 00401029   lea         edi,[ebp-4Ch]  
  11. 0040102C   mov         ecx,13h  
  12. 00401031   mov         eax,0CCCCCCCCh  
  13. 00401036   rep stos    dword ptr [edi]  
  14. 4:        char name_tmp[] = {"hello"};  
  15. 00401038   mov         eax,[string "hello" (0042201c)]  
  16. 0040103D   mov         dword ptr [ebp-8],eax  
  17. 00401040   mov         cx,word ptr [string "hello"+4 (00422020)]  
  18. 00401047   mov         word ptr [ebp-4],cx  
  19. 5:        char* name_glb = "hello";  
  20. 0040104B   mov         dword ptr [ebp-0Ch],offset string "hello" (0042201c)  
  21. 6:    }  
  22. 00401052   pop         edi  
  23. 00401053   pop         esi  
  24. 00401054   pop         ebx  
  25. 00401055   mov         esp,ebp  
  26. 00401057   pop         ebp  
  27. 00401058   ret  
    通过上面的代码,我们可以清楚地看出两者之间的差别。"hello"字符串是一个全局只读变量,空间地址为0x0042201C。name_tmp是函数内的char数组,第4行语句下面四行表示全局数据“hello”是分 两次拷贝到name_tmp的 ,第一次是dword、四个字节,第二次是word、两个字节。所以name_tmp共有6个字节。相比较而言,name_glb什么也没有,它只是把自己指向了全局变量而已,所以它只是一个指针而已。

(2)apple a()和apple b

假设class apple的定义为:

[cpp]  view plain  copy
  1. class apple  
  2. {  
  3. public:  
  4.     apple() {}  
  5.     ~apple() {}  
  6. };  

那么apple a()和apple b是分别怎么编译的呢?

[cpp]  view plain  copy
  1. 9:    void process()  
  2. 10:   {  
  3. 00401020   push        ebp  
  4. 00401021   mov         ebp,esp  
  5. 00401023   sub         esp,44h  
  6. 00401026   push        ebx  
  7. 00401027   push        esi  
  8. 00401028   push        edi  
  9. 00401029   lea         edi,[ebp-44h]  
  10. 0040102C   mov         ecx,11h  
  11. 00401031   mov         eax,0CCCCCCCCh  
  12. 00401036   rep stos    dword ptr [edi]  
  13. 11:       apple a();  
  14. 12:       apple b;  
  15. 00401038   lea         ecx,[ebp-4]  
  16. 0040103B   call        @ILT+20(apple::apple) (00401019)  
  17. 13:   }  
  18. 00401040   lea         ecx,[ebp-4]  
  19. 00401043   call        @ILT+10(apple::~apple) (0040100f)  
  20. 00401048   pop         edi  
  21. 00401049   pop         esi  
  22. 0040104A   pop         ebx  
  23. 0040104B   add         esp,44h  
  24. 0040104E   cmp         ebp,esp  
  25. 00401050   call        __chkesp (004010b0)  
  26. 00401055   mov         esp,ebp  
  27. 00401057   pop         ebp  
  28. 00401058   ret  

    为什么apple a()这边什么也没有编译呢?原因很简单,因为 编译器把apple a()看成是一个extern的函数,返回值为apple 。与此相对应的apple b才是函数中真正定义的临时变量,因为在下面不远处有apple的两个函数——apple的构造函数和apple的析构函数哦。

(3)(apple*) (0) -> print()

其中class apple这样定义:

[cpp]  view plain  copy
  1. class apple  
  2. {  
  3.     int value;  
  4. public:  
  5.     apple() {}  
  6.     ~apple() {}  
  7.     void print() { return;}   
  8. };  

    如果0设置为apple*,那么访问函数print会有问题吗?

[cpp]  view plain  copy
  1. 10:   void process()  
  2. 11:   {  
  3. 00401030   push        ebp  
  4. 00401031   mov         ebp,esp  
  5. 00401033   sub         esp,40h  
  6. 00401036   push        ebx  
  7. 00401037   push        esi  
  8. 00401038   push        edi  
  9. 00401039   lea         edi,[ebp-40h]  
  10. 0040103C   mov         ecx,10h  
  11. 00401041   mov         eax,0CCCCCCCCh  
  12. 00401046   rep stos    dword ptr [edi]  
  13. 12:       ((apple*)(0))->print();  
  14. 00401048   xor         ecx,ecx  
  15. 0040104A   call        @ILT+0(apple::print) (00401005)  
  16. 13:   }  
  17. 0040104F   pop         edi  
  18. 00401050   pop         esi  
  19. 00401051   pop         ebx  
  20. 00401052   add         esp,40h  
  21. 00401055   cmp         ebp,esp  
  22. 00401057   call        __chkesp (004010e0)  
  23. 0040105C   mov         esp,ebp  
  24. 0040105E   pop         ebp  
  25. 0040105F   ret  
    通过运行函数,我们发现没有任何异常产生,为什么呢?因为我们发现ecx是作为0传给print函数的,也就是我们熟悉的this指针为0。但是我们发现在print函数内部没有用到this指针,因为我们根本没有对this->value进行访问,只是一个返回语句return。这说明指针作为class null指针并不可怕,可怕的是用null去访问内存中的数据

(4) int m = 1; int n = m++ + ++m; 那么n是多少呢?

[cpp]  view plain  copy
  1. 10:   void process()  
  2. 11:   {  
  3. 0040D4D0   push        ebp  
  4. 0040D4D1   mov         ebp,esp  
  5. 0040D4D3   sub         esp,48h  
  6. 0040D4D6   push        ebx  
  7. 0040D4D7   push        esi  
  8. 0040D4D8   push        edi  
  9. 0040D4D9   lea         edi,[ebp-48h]  
  10. 0040D4DC   mov         ecx,12h  
  11. 0040D4E1   mov         eax,0CCCCCCCCh  
  12. 0040D4E6   rep stos    dword ptr [edi]  
  13. 12:       int m = 1;  
  14. 0040D4E8   mov         dword ptr [ebp-4],1  
  15. 13:       int n = m++ + ++m;  
  16. 0040D4EF   mov         eax,dword ptr [ebp-4]  
  17. 0040D4F2   add         eax,1  
  18. 0040D4F5   mov         dword ptr [ebp-4],eax  
  19. 0040D4F8   mov         ecx,dword ptr [ebp-4]  
  20. 0040D4FB   add         ecx,dword ptr [ebp-4]  
  21. 0040D4FE   mov         dword ptr [ebp-8],ecx  
  22. 0040D501   mov         edx,dword ptr [ebp-4]  
  23. 0040D504   add         edx,1  
  24. 0040D507   mov         dword ptr [ebp-4],edx  
  25. 14:   }  
  26. 0040D50A   pop         edi  
  27. 0040D50B   pop         esi  
  28. 0040D50C   pop         ebx  
  29. 0040D50D   mov         esp,ebp  
  30. 0040D50F   pop         ebp  
    通过汇编代码,我们看到【ebp-4】就是m在堆栈中的地址,【ebp-8】就是n在堆栈中的地址。 int n = m++ + ++m下面总共有9句汇编。我们可以分析一下:前面三句表示m自己增加1,第四句表示ecx = m,即ecx = 2。第五句ecx和m相加,翻译过来就是ecx = ecx + m。此时ecx = 4。第六句表示 n = ecx。 第七句到第九句表示m自增加1。为什么会出现这样的情况呢,其实道理很简单,主要是因为我们的表达式是从右向左运算的。如果大家这样看就明白了,首先++m,然后 n = m + m,最后 m++。

(5) *p++和(*p)++区别是什么

[cpp]  view plain  copy
  1. 10:   void process()  
  2. 11:   {  
  3. 0040D4D0   push        ebp  
  4. 0040D4D1   mov         ebp,esp  
  5. 0040D4D3   sub         esp,48h  
  6. 0040D4D6   push        ebx  
  7. 0040D4D7   push        esi  
  8. 0040D4D8   push        edi  
  9. 0040D4D9   lea         edi,[ebp-48h]  
  10. 0040D4DC   mov         ecx,12h  
  11. 0040D4E1   mov         eax,0CCCCCCCCh  
  12. 0040D4E6   rep stos    dword ptr [edi]  
  13. 12:       char data = 'a';  
  14. 0040D4E8   mov         byte ptr [ebp-4],61h  
  15. 13:       char* p = & data;  
  16. 0040D4EC   lea         eax,[ebp-4]  
  17. 0040D4EF   mov         dword ptr [ebp-8],eax  
  18. 14:       *p++;  
  19. 0040D4F2   mov         ecx,dword ptr [ebp-8]  
  20. 0040D4F5   add         ecx,1  
  21. 0040D4F8   mov         dword ptr [ebp-8],ecx  
  22. 15:       (*p)++;  
  23. 0040D4FB   mov         edx,dword ptr [ebp-8]  
  24. 0040D4FE   mov         al,byte ptr [edx]  
  25. 0040D500   add         al,1  
  26. 0040D502   mov         ecx,dword ptr [ebp-8]  
  27. 0040D505   mov         byte ptr [ecx],al  
  28. 16:   }  
  29. 0040D507   pop         edi  
  30. 0040D508   pop         esi  
  31. 0040D509   pop         ebx  
  32. 0040D50A   mov         esp,ebp  
  33. 0040D50C   pop         ebp  
  34. 0040D50D   ret  
    我们首先创建局部变量data。然后把data的指针复制给p。从汇编代码可以清楚的看出来:*p++就等于p++;(*p)++首先把指针复制给edx,然后获取edx地址指向的char数据复制给al,al自增加1,同时p地址复制给ecx,al复制给ecx指向的地址,就是这么简单。


类似的问题还有很多,大家不妨自己试一试:

(1) 下面的union在内存是怎么安排的?gcc和vc编译的时候,分配的内存size是一样的吗?

[cpp]  view plain  copy
  1. typedef union   
  2. {  
  3. char m:3;  
  4. char n:7;  
  5. int data;  
  6. }value;  

(2) 下面地址一致吗?

[cpp]  view plain  copy
  1. char value1[] = {"hello"};  
  2. char value2[] = {"hello"};  
  3. char* pValue1 = “hello”;  
  4. char* pValue2 = "hello";  
  5. value1和value2地址一致吗?pValue1和pValue2呢?  

(3)下面一段话为什么运行错误?为什么内存泄露了?怎么修改?

[cpp]  view plain  copy
  1. class apple  
  2. {  
  3.     char* pName;  
  4. public:  
  5.     apple() { pName = (char*)malloc(10);}  
  6.     ~apple() {if(NULL != pName) free(pName);}  
  7. };  
  8.   
  9. void process()  
  10. {  
  11.     apple a, b;  
  12.     a = b;  
  13. }  

网站文章

  • C#上位机:现代编程中的全能工具?

    C#上位机:现代编程中的全能工具?

    C#作为现代、安全、易用的编程语言,在多领域具有广泛应用,尤其在上位机开发中脱颖而出。输入“777”,即刻获取关于上位机开发和数据可视化的专业学习资料,工业自动化、物联网、数据分析、科学计算等领域均有...

    2024-02-01 02:09:37
  • Day26 洛谷P1567 统计天数(不用数组)

    题目描述 炎热的夏日,KC 非常的不爽。他宁可忍受北极的寒冷,也不愿忍受厦门的夏天。最近,他开始研究天气的变化。他希望用研究的结果预测未来的天气。 经历千辛万苦,他收集了连续 N天(0<10^6...

    2024-02-01 02:09:33
  • Java Socket 示例代码

    Socket服务端public class SocketServer { public static void main(String[] args) throws IOException { ...

    2024-02-01 02:09:03
  • Error(20) 解决jenkins在Execute shell中启动jar失败问题

    Error(20) 解决jenkins在Execute shell中启动jar失败问题

    解决jenkins在Execute shell中启动jar失败问题 BUILD_ID=dontKillMe jenkins shell产生进程自动终止问题

    2024-02-01 02:08:55
  • Android Framework底层原理——WMS机制

    Android Framework底层原理——WMS机制

    在WindowManagerService#WindowManagerService()构造方法中。WMS的启动中WMS创建完成后会调用 wm.onInitReady 方法。在WindowManagerService#main方法中。

    2024-02-01 02:08:47
  • Alan Turing阿兰.图灵---计算机之父/人工智能之父

    提出图灵机理论,二战时破解德国通讯密码,挽救无数生命。提出仿真系统和自动程序设计概念,设计了"图灵测试",有"计算机之父","人工智能之父","破译之父"等美誉。 一、生平1912年6月23日,出生于英国伦敦1931年-1934年,在英国剑桥大学国王学院学习1932年-1935年,研究量子力学,概率论和逻辑学1935年,由于独立发现中心极限定理,获Smith奖,年仅23岁被选为剑桥大学国王

    2024-02-01 02:08:19
  • OpenGL ES 3.0 开发(十九):相机抖音滤镜

    OpenGL ES 3.0 开发(十九):相机抖音滤镜

    该原创文章首发于微信公众号:字节流动 OpenGLES 相机抖音滤镜 最近几篇文章主要是利用 OpenGL 实现相机预览的一些常见的滤镜,上一篇主要介绍了 LUT 滤镜的原理及简单实现方法,而本文主要介绍抖音短视频 App 里面一些常见滤镜的实现,这里只做抛砖引玉,玩滤镜主要靠想象力去实现一些酷炫的效果。 分色偏移 分色偏移滤镜原理:基于原纹理坐标进行偏移,分别采样后再按照 RGBA 通道进...

    2024-02-01 02:08:14
  • 【图片素材】小程序图片

    【图片素材】小程序图片

    iconfont 阿里巴巴矢量图标库iconfont-阿里巴巴矢量图标库https://www.iconfont.cn/

    2024-02-01 02:08:07
  • ansible点对点模块练习

    ping模块[root@S01 ~]# ansible all -m ping192.168.137.129 | SUCCESS => { "changed": false, "ping": "pong"}command模块[root@S01 ~]# ansible web -m command -a 'chdir=/tmp ls'192.168.137...

    2024-02-01 02:07:36
  • 关于MyBatis 一级缓存详解

    关于MyBatis 一级缓存详解

    缓存就是内存中的一个对象,用于对数据库查询结果的保存,用于减少与数据库的交互次数从而降低数据库的压力,进而提高响应速度。MyBatis 中的缓存就是说 MyBatis 在执行一次SQL查询或者SQL更...

    2024-02-01 02:07:30