当前位置:编程学习 > C/C++ >>

用汇编的眼光看C++ (之x86汇编)

 

【 声明:版权所有,欢迎转载,请勿用于商业用途。  联系信箱:feixiaoxing @163.com】

 

 

    说到用汇编的眼光看C++语言,那么怎么阅读汇编代码就成了我们需要解决的一个问题。其实,实话说,汇编其实不难。只是我们需要明白这样几个问题:

 

    (1)汇编是什么语言?

 

    (2)汇编中的主要内容有哪些?

 

    (3)汇编语言是怎么和实际C/C++语言代码一一对应的?

 

 

    (1)汇编是什么语言

 

    其实汇编语言是CPU指令码的一种标记符号。不同的CPU具有不同的指令集,普通PC上的CPU一般来自AMD或者是INTEL,使用的也就是我们今天所要说的X86指令集。其他类似的CPU还有POWERPC,主要来自电信企业的交换机、路由器使用;ARM类型,主要是智能终端或者仪器仪表类的设备使用;SUN SPARC类型,主要来供SUN服务器使用。因为CPU指令集和二进制码几乎是一一对应的,所以汇编语言不但可以帮助我们快速了解机器的硬件,也方便我们了解程序是怎么在设备上面运行的。

 

 

    (2)汇编语言的内容有哪些?

 

    汇编语言的内容有很多,但是真正和我们C/C++语言相关的内容其实并不多。大体上你需要了解的只有寄存器、段地址、堆栈、寄存器之间的基本操作、地址访问这些就足够了。

 

 

    (3)汇编语言是怎么和实际语言一一对应的?

 

    我们从一个范例开始。一般来说,一条语句都需要拆分成若干条汇编语句。比如说下面这一段话:

 

 

int m = 10; 

int n = 20; 

int p = m + n; 

 

    我们这里假设m、n、p都是处在一个函数之中,所以事实上三个变量都是临时变量,在进入到函数之前,ebp和esp之间都要腾出空间为这些临时变量做准备。那么这三句话应该是这样解释的。

 

43:       int m = 10; 

004012E8   mov         dword ptr [ebp-4],0Ah 

44:       int n = 20; 

004012EF   mov         dword ptr [ebp-8],14h 

45:       int p = m + n; 

004012F6   mov         eax,dword ptr [ebp-4] 

004012F9   add         eax,dword ptr [ebp-8] 

004012FC   mov         dword ptr [ebp-0Ch],eax 

    我们可以通过上面的代码直观地看到汇编语句和C语言之间的对应关系。第一句,m赋值为10,内存正是ebp向下的内存;第二句和第一句类似;第三句稍微有点复杂,我们可以分析一下。首先我们看到,CPU从堆栈中把m的数据找了出来,也就是[ebp-4]地址处的数据,接着CPU用同样的方法把n的数据也找了出来,直接加到寄存器eax上,最后一步比较简单,就是把eax的数据保存在[ebp-0c]处的地址上。只要是函数内部的临时变量,就会看到这样的形式。临时变量就是依靠ebp的偏移地址获取的。

    大家有没有想过,如果p是全局变量呢?

 

 

45:       int m = 10; 

004012E8   mov         dword ptr [ebp-4],0Ah 

46:       int n = 20; 

004012EF   mov         dword ptr [ebp-8],14h 

47:       p = m + n; 

004012F6   mov         eax,dword ptr [ebp-4] 

004012F9   add         eax,dword ptr [ebp-8] 

004012FC   mov         [p (0042b0b4)],eax 

    看到上面的代码,我们发现m、n的赋值方向没有发生什么样的变化。变化的是,最后寄存器eax的数值被赋值给了一个绝对地址0x42b0b4。这说明了一个问题,在程序加载到内存后,全局变量是有独立地址空间,并不会随着堆栈的浮动发生变化。

    前面我们说过,在函数内部的所有变量都会保存在ebp到esp之间的堆栈空间上,那么代码是怎么做的呢?我们可以看这样一段汇编代码?

 

 

41:   void process() 

42:   { 

004012D0   push        ebp 

004012D1   mov         ebp,esp 

004012D3   sub         esp,4Ch 

004012D6   push        ebx 

004012D7   push        esi 

004012D8   push        edi 

004012D9   lea         edi,[ebp-4Ch] 

004012DC   mov         ecx,13h 

004012E1   mov         eax,0CCCCCCCCh 

004012E6   rep stos    dword ptr [edi] 

43:       int m = 10; 

004012E8   mov         dword ptr [ebp-4],0Ah 

44:       int n = 20; 

004012EF   mov         dword ptr [ebp-8],14h 

45:       int p = m + n; 

004012F6   mov         eax,dword ptr [ebp-4] 

004012F9   add         eax,dword ptr [ebp-8] 

004012FC   mov         dword ptr [ebp-0Ch],eax 

46:   } 

    我们把刚才一段函数的完整代码打印出来。我们发现,事实上在临时变量m运算之前,函数做了很多预备操作,其主要目的有两个:(1)为临时变量准备空间;(2)对函数运算中使用到的寄存器进行保存处理,这是因为寄存器是所有函数共有的资源,如果不记录好原来的数据,那么在函数返回后,寄存器就会忘记原来的数值,不能在原来的状态下继续正确地运算了。从地址0x4012D0到地址0x4012E6之间共有10句话,其实意思并不困难。第一句,ebp压栈;第二句esp复制给ebp;第三句esp自减4C大小,这个大小一般是按照函数内部定义了多少临时变量决定的;第四句,ebx压栈;第五句,esi压栈;第六句,edi压栈;第七句到第十句,把[ebp-4C]处向上的0x4C个字节全部设置成CC,edi为起始地址,ecx为循环次数0x13次,dword表示每次设置4个字节。

    那么函数在返回前做了一些什么呢?

 

 

46:   } 

004012FF   pop         edi 

00401300   pop         esi 

00401301   pop         ebx 

00401302   mov         esp,ebp 

00401304   pop         ebp 

00401305   ret 

    其实函数返回的时候,做的内容特别简单。第一句edi出栈;第二句esi出栈;第三句ebx出栈,和前面寄存器进栈的顺序是完全相反的。最后三句特别关键,我们看到ebp复制给esp,ebp出栈,函数返回,这样一切都恢复到函数调用之前的状态了。

    那么函数调用的时候,入参是怎么处理的呢?

 

 

53:       process(20); 

0040EFA4 &

补充:软件开发 , C++ ,
CopyRight © 2022 站长资源库 编程知识问答 zzzyk.com All Rights Reserved
部分文章来自网络,