通过汇编语言分析C++函数的调用过程,以及引用传递、形式传递和以数组为参数时的内存分配分析。

在不同的C/C++编译器中,由同样的C++代码编译成的(机器)汇编代码是不同的。在本文中,主要讨论Microsoft Visual C++ .Net编译器生成的机器代码。

本文以下面三个函数为分析基础,从主函数的调用到对应函数的执行,逐步分析得出结论。
int add_copy(int a, int b) {
    return a + b;
 }

int add_citation(int& a, int& b) {
    return a + b;
}

int add_arry(int arry[]) {
    return arry[0] + arry[1];
}

1、调用add_copy()函数,形式传递

在主函数中,代码int result_1 = add_copy(a, b);编译成汇编语言的代码如下,分别将b,a的值传送给eax,ecx压入栈中,注意书写形参的顺序为add_copy(a, b)但是压栈的顺序为从右至左

int result_1 = add_copy(a, b);
000A64CF 8B 45 E8             mov         eax,dword ptr [b]  
000A64D2 50                   push        eax  
000A64D3 8B 4D F4             mov         ecx,dword ptr [a]  
000A64D6 51                   push        ecx  
000A64D7 E8 7A AF FF FF       call        add_copy (0A1456h)  
000A64DC 83 C4 08             add         esp,8  
000A64DF 89 45 B0             mov         dword ptr [result_1],eax  

接下来我们按照上面的汇编代码执行到call add_copy (0A1456h) ,进入函数真正的调用区域,首先函数为自己的调用进行了一系列的准备工作,请阅读下面的汇编代码以及注释:

int add_copy(int a, int b) {
//执行函数调用前的准备工作,包括准备现场,分配空间等操作
000A60C0 55                   push        ebp  //将ebp压栈,用于在函数运行结束后返回调用之前的函数
000A60C1 8B EC                mov         ebp,esp //将栈的栈 
000A60C3 81 EC C0 00 00 00    sub         esp,0C0h //为函数分配0C0H字节的局部变量空间。
//因为在C++语言当中,程序栈是向下生长的,即在堆栈空间内,变量是从高地址向低地址方向依次分配的。
//所以,我们在前面的看到的局部变量内存分配是通过sub指令完成的,而不是add指令。
000A60C9 53                   push        ebx  
000A60CA 56                   push        esi  
000A60CB 57                   push        edi  
//保存现场
000A60CC 8D BD 40 FF FF FF    lea         edi,[ebp-0C0h]  
000A60D2 B9 30 00 00 00       mov         ecx,30h  
000A60D7 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
000A60DC F3 AB                rep stos    dword ptr es:[edi]  
//通过上面的指令,程序获得局部变量空间的起始地址(低地址)并将它送入edi寄存器,
//设置ecx寄存器为变量空间长度30H(DWORD型48双字长度,也即192字节),eax为0CCCCCCCCh值,
//然后通过循环指令rep stos将eax中的值存入以edi为起始地址的192字节的地址空间内。
//这里我们可以发现一个问题就是在Visual C++中,局部变量一律以0CCCCCCCCh来填充。
//这就不怪我们在程序调试时经常出现的数据就是“0CCCCCCCC”了。
000A60DE B9 2A F0 0A 00       mov         ecx,offset _B69F9E72_Study@cpp (0AF02Ah)  
000A60E3 E8 A1 B2 FF FF       call        @__CheckForDebuggerJustMyCode@4 (0A1389h)  
//执行函数的运算
    return a + b;
000A60E8 8B 45 08             mov         eax,dword ptr [a]  
000A60EB 03 45 0C             add         eax,dword ptr [b]  
 }
  //恢复现场
000A60EE 5F                   pop         edi  
000A60EF 5E                   pop         esi  
000A60F0 5B                   pop         ebx  
000A60F1 81 C4 C0 00 00 00    add         esp,0C0h  
000A60F7 3B EC                cmp         ebp,esp  
000A60F9 E8 9B B1 FF FF       call        __RTC_CheckEsp (0A1299h)  
000A60FE 8B E5                mov         esp,ebp  
000A6100 5D                   pop         ebp  
000A6101 C3                   ret  

接下来我们单步运行调试程序,观察每一步运行后的内存变化,来进一步理解程序的运行。
1.将参数b压入栈中,因为栈是向下生长的从高地址向低地址生长,所以在图中03 00 00 00下面高地址的部分是主函数的区域,接下来我们开辟的空间会紧接着主函数在低地址开辟调用函数的临时空间!
在这里插入图片描述
2.将参数a压入栈中,具体情况和上一步相同我就只放图片了。
在这里插入图片描述
3.ebp入栈,保护函数入口之前的地址,在return返回之后,还原。
在这里插入图片描述
4.接下来是一系列的保护现场操作,我就不多赘述了,大家应注意它是先改变了esp的值,在入栈寄存器的值,如下图所示:
在这里插入图片描述
5.为调用的函数分配临时的内存空间,如下图所示:
在这里插入图片描述
6.恢复现场,返回eax的值,也就是函数的运算结果,如下图:
在这里插入图片描述
堆栈操作在C++语言中是占到了很大的比重的,C++语言从某种程度上来说也是基于堆栈的语言,因为它其中的好多操作都是基于堆栈的。特别是从面向对象的角度来看,一般在我们的整个程序中,全局变量所占的比例是很小的,其它绝大多数的变量(包括如INT等基本数据类型、自定义类型)都是局部变量。这些局部变量的分配和释放都是通过堆栈操作来完成的。
通过调试我们大概理解了函数调用以及执行的过程,具体的堆栈操作我们还是应该再一次仔细分析一下,参数入栈入上面1、2两点所述的,他们是在主函数和临时函数空间之间的区域,相对于主函数来说是低地址,相对于临时函数来说是高地址,下图所示就是相对于临时函数来说的。

在这里插入图片描述

2、调用add_citation()函数,引用传递

其实大体的思路和形式传递的几乎一样,只是在参数入栈的时候稍有不同,我们就仔细来分析这不同之处,然后总结出结论。
在主函数的参数入栈的部分,我们看到形式传递使用的是mov指令,直接传送的变量的值,而引用传递使用的是lea指令,传送的是变量的偏移地址!!

//形式传递
000A64CF 8B 45 E8             mov         eax,dword ptr [b]  
000A64D2 50                   push        eax  
000A64D3 8B 4D F4             mov         ecx,dword ptr [a]  
000A64D6 51                   push        ecx  
//引用传递
000A64E2 8D 45 E8             lea         eax,[b]  
000A64E5 50                   push        eax  
000A64E6 8D 4D F4             lea         ecx,[a]  
000A64E9 51                   push        ecx  

在这里插入图片描述

通过调试,我们可以发现,这次函数的内存空间分配的位置和上一次相同,开始覆盖上一次空间中的值,来使用新的值,这归咎于上一次的调用的恢复现场操作,如下所示,是上一次函数调用恢复现场的汇编代码,它将esp和ebp两个寄存器的值恢复到主函数调用它之前!!

//上一次add_copy()的恢复现场的操作
000A60F1 81 C4 C0 00 00 00    add         esp,0C0h  
000A60F7 3B EC                cmp         ebp,esp  
000A60F9 E8 9B B1 FF FF       call        __RTC_CheckEsp (0A1299h)  
000A60FE 8B E5                mov         esp,ebp  
000A6100 5D                   pop         ebp  

引用传递的加法操作要比形式传递的加法操作麻烦许多。形式传递只用了两条指令,而引用传递用了四条。
在这里插入图片描述

3、以数组为参数的形式传递,值得注意一下。

如下图所示,数组作为函数参数时,传递的是数组的首地址而不是数组的值。在这里插入图片描述
在这里插入图片描述

4.结束语

从这里我们可以看出一个C++程序,它的局部变量内存分配在底层的实现。很多情况下(可以说在绝大多数一般通用软件的编写情况下),我们并不需要了解这么底层的技术细节。但是,要想做一个优秀的C/C++程序员,深入的了解这些细节又是必要的。只有这样,我们才能在一望无际的代码中立于不败之地,当你的代码出现好多莫名其妙的问题但在逻辑上又无法发现其缺陷或者逻辑上根本没有缺陷时,我们能够胸有成竹地面对它们。不论出现什么样的问题,我们都能够知道为什么会出这样的问题,都能够想出办法去解决这样的问题。这才是我们要达到的目的。

本文引用了该博主的文章中的部分内容,https://blog.csdn.net/cambest/article/details/914252,如有冒犯,请联系。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值