从汇编角度看函数参数传递

本文探讨了函数参数传递的两种基本机制:值传递和引用传递。值传递创建实参的副本,而引用传递允许直接操作主函数的实参。在C语言中,值传递是唯一的机制,尽管可以通过指针传递地址,但这不同于引用传递。C++支持引用传递,通过在调用时将变量的地址传入。通过分析汇编代码,可以更好地理解这两种传递方式的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基本理论:

  • 函数参数传递机制问题在本质上是调用函数过程和被调用函数在调用发生时进行同的方法问题。基本的参数传递机制有两种,值传递、引用传递。

    • 值传递: 在值传递过程中,被调函数的形参作为被调函数的局部变量,即在该函数栈中开辟内存空间以存放由主函数传递进来的实参的值,从而成为实参的一个副本。值传递的特点是被调函数对形参的任何操作都是作为局部变量进行,不会影响主函数实参的值。

    • 引用传递:引用传递过程中,被调函数的形参虽然也作为局部变量在该函数栈中开辟了内存空间,但这时存放的是由主函数传递进来的实参的地址。被调函数的形对形参的任何操作都被处理成间接寻址,即通过在栈中存放的地址访问主函数中的实参变量。因此,被调函数对形参做的任何操作都会影响主函数中的实参变量。

  • 在C语言中,值传递是唯一可用的参数传递机制。但这时我们就要问,不是还有指针的地址传递吗?由于受指针变量作为函数参数的影响,有许多朋友认为指针作参数传递和引用传递一样。这是错误的。请看下面的代码:

int swap(int *x, int *y)
{
   int temp;
   temp = *x;
   *x = *y; 
   *y = temp;
   return temp;
}
int main()
{
    int a = 1, b = 2;
    int *p1 = &a;
    int *p2 = &b;
    swap(p1, p2);
    printf("a=%d  b=%d\n", a, b);
    return 0;
}
  • 函数swap以两个指针变量作为参数,当main()调用swap时,是以值传递的方式将指针变量p1、p2的值(也就是变量a、b的地址)放在了swap在堆栈中为形式参数x、y开辟的内存单元中。这一点从以下的汇编代码可以看出:
//main函数汇编
int main()
{
00271420  push        ebp
00AA1421  mov         ebp,esp  
00AA1423  sub         esp,0F4h //主函数栈桢空间总大小f4 --->244  
00AA1429  push        ebx  
00AA142A  push        esi  
00AA142B  push        edi  
00AA142C  lea         edi,[ebp-0F4h]  
00AA1432  mov         ecx,3Dh  
00AA1437  mov         eax,0CCCCCCCCh //将内存空间循环赋予初始值cccccccc 
00AA143C  rep stos    dword ptr es:[edi]  
00AA143E  mov         eax,dword ptr ds:[00AA8000h]  
00AA1443  xor         eax,ebp  
00AA1445  mov         dword ptr [ebp-4],eax  
    int a = 10, b = 20;
00AA1448  mov         dword ptr [a],0Ah //a内存空间放入10
00AA144F  mov         dword ptr [b],14h //b内存空间放入20 
    int *p1 = &a;
00AA1456  lea         eax,[a]            //将a的地址放入eax  
00AA1459  mov         dword ptr [p1],eax //将eax里面的值放入p1内存空间中,下面类似 
    int *p2 = &b;
00AA145C  lea         eax,[b]  
00AA145F  mov         dword ptr [p2],eax  
    swap(p1, p2);
00AA1462  mov         eax,dword ptr [p2] //参数p2的值进栈  
00AA1465  push        eax  
00AA1466  mov         ecx,dword ptr [p1] //参数p1的值进栈  
00AA1469  push        ecx  
00AA146A  call        swap (0AA100Fh)    //调用swap函数 
00AA146F  add         esp,8              //清理栈中的参数
    printf("a=%d  b=%d\n", a, b);//下面是printf函数的汇编
00AA1472  mov         esi,esp  
00AA1474  mov         eax,dword ptr [b]  
00AA1477  push        eax  
00AA1478  mov         ecx,dword ptr [a]  
00AA147B  push        ecx  
00AA147C  push        0AA5858h  
00AA1481  call        dword ptr ds:[0AA9114h]  
00AA1487  add         esp,0Ch  
00AA148A  cmp         esi,esp  
00AA148C  call        __RTC_CheckEsp (0AA113Bh)  
    return 0;
00AA1491  xor         eax,eax  
}
  • 阅读上述代码要注意,INTEL80x86系列的CPU对堆栈的处理是向下生成,即从高地址单元向低地址单元生成。从上面的汇编代码可知,main()在调用swap之前,先将实参的值按从右至左的顺序压栈,即先p2进栈,再p1进栈。调用结束之后,主调函数main()负责清理堆栈中的参数。Swap 将使用这些进入堆栈的变量值。
//swap函数的汇编代码:
int swap(int *x, int *y)
{
002713C0  push        ebp  
002713C1  mov         ebp,esp  
002713C3  sub         esp,0CCh//swap函数栈桢空间大小cc ---> 204  
002713C9  push        ebx  
002713CA  push        esi  
002713CB  push        edi  
002713CC  lea         edi,[ebp-0CCh]
00AA13D2  mov         ecx,33h  
00AA13D7  mov         eax,0CCCCCCCCh//将内存空间循环赋予初始值cccccccc  
00AA13DC  rep stos    dword ptr es:[edi]  
    int temp;
    temp = *x;
00AA13DE  mov         eax,dword ptr [x]//操作已存放在堆栈中的p1,将p1置入eax 
00AA13E1  mov         ecx,dword ptr [eax]//通过寄存器间址将*p1置入ecx  
00AA13E3  mov         dword ptr [temp],ecx//经由ecx将*p1置入temp变量的内存单元。以下类似 
    *x = *y;
00AA13E6  mov         eax,dword ptr [x]  
00AA13E9  mov         ecx,dword ptr [y]  
00AA13EC  mov         edx,dword ptr [ecx]  
00AA13EE  mov         dword ptr [eax],edx  
    *y = temp;
00AA13F0  mov         eax,dword ptr [y]  
00AA13F3  mov         ecx,dword ptr [temp]  
00AA13F6  mov         dword ptr [eax],ecx   
}
  • 由上述汇编代码基本上说明了C语言中值传递的原理,只不过传递的是指针的值。本文后面还要论述使用引用传递的swap函数。从这些汇编代码分析,这里我们可以得到以下几点:

    • 进程的堆栈存储区是主调函数和被调函数进行通信的主要区域。
    • C语言中参数是从右向左进栈的。
    • 被调函数使用的堆栈区域结构为:局部变量(如temp)、返回地址、函数参数、低地址 、高地址。
    • 由主调函数在调用后清理堆栈。
    • 函数的返回值一般是放在寄存器中的。

    • 这里尚需补充说明几点:一是参数进栈的方式。对于内部类型,由于编译器知道各类型变量使用的内存大小故直接使用push指令;对于自定义的类型(如structure),采用从源地址向目的(堆栈区)地址进行字节传送的方式入栈。二是函数返回值为什么一般放在寄存器中,这主要是为了支持中断;如果放在堆栈中有可能因为中断而被覆盖。三是函数的返回值如果很大,则从栈向存放返回值的地址单元(由主调函数在调用前将此地址压栈提供给被调函数)进行字节传送,以达到返回的目的。如果在被调函数中返回局部变量的地址是毫无意义的;因为局部变量存于栈中,调用结束后栈将被清理,这些地址就变得无效了。

  • C++既有C的值传递又有引用传递。在值传递上与C一致,这里着重说明引用传递。如本文前面所述,引用传递就是传递变量的地址到被调函数使用的堆栈中。在C++中声明引用传递要使用”&”符号,而调用时则不用。

代码:

int& swap2(int& x, int& y) 
{
   int temp;
   temp = x;
   x = y;
   y = temp;
   return x;
}
void main()
{
   int a = 1, b = 2;
   swap2(a, b);
}
main函数汇编

 void main()
 {
……
……
    int a = 1, b = 2;
00401088   mov         dword ptr [ebp-4],1 ;变量a
0040108F   mov         dword ptr [ebp-8],2 ;变量b
    swap2(a, b);
00401096   lea         eax,[ebp-8] //将b的偏移地址送入eax
00401099   push        eax //b的偏移地址压栈
0040109A   lea         ecx,[ebp-4] //将a的偏移地址送入ecx
0040109D   push        ecx //将a的偏移地址压栈
0040109E   call        @ILT+20(swap2) (00401019) //调用swap函数
004010A3   add         esp,8 //清理堆栈中的参数
……
……

}
  • 可以看出,main函数在调用swap2之前,按照从右至左的顺序将b和a的偏移地址压栈,这就是在传递变量的地址swap(int &x, int &y)
//swap函数汇编
int& swap2(int& x, int& y)
{
00401030   push        ebp
00401031   mov         ebp,esp
……
……
    int temp;
    temp = x;
00401048   mov         eax,dword ptr [ebp+8]
0040104B   mov         ecx,dword ptr [eax]
0040104D   mov         dword ptr [ebp-4],ecx    
    x = y;
00401050   mov         edx,dword ptr [ebp+8]
00401053   mov         eax,dword ptr [ebp+0Ch]
00401056   mov         ecx,dword ptr [eax]
00401058   mov         dword ptr [edx],ecx 
    y = temp;
0040105A   mov         edx,dword ptr [ebp+0Ch]
0040105D   mov         eax,dword ptr [ebp-4]
00401060   mov         dword ptr [edx],eax
    return x;
00401062   mov         eax,dword ptr [ebp+8] ;返回x,由于x是外部变量的偏移地址,故返回是合法的
9: }
  • 可以看出,swap2与前面的swap函数的汇编代码相似。这是因为前面的swap函数接受指针变量,而指针变量的值正是地址。所以,对于这里的swap2和前面的swap来讲,栈中的函数参数存放的都是地址,在函数中操作的方式是一致的。但是,对swap2来说这个地址是主调函数通过将实参变量的偏移地址压栈而传递进来的—-这是引用传递。而对swap来说,这个地址是主调函数通过将实参变量的值压栈而传递进来的–这是值传递,只不过由于这个实参变量是指针变量所以其值是地址而已。这里的关键点在于,同样是地址,一个是引用传递中的变量地址,一个是值传递中的指针变量的值。我想若能明确这一点,就不至于将C语言中的以指针变量作为函数参数的值传递情况混淆为引用传递了。虽然x是一个局部变量,但是由于其值是主调函数中的实参变量的地址,故在swap2中返回这个地址是合法的。
  • c++ 中经常使用的是常量引用,如将swap2改为:Swap2(const int& x; const int& y),这时将不能在函数中修改引用地址所指向的内容,具体来说,x和y将不能出现在”=”的左边。
函数调用过程是程序中常见的一种操作,它通常涉及到参数传递、栈帧的建立与销毁、返回值的传递等多个方面。从汇编的角度来看,函数调用过程可以分为以下几个步骤: 1. 将函数的参数压入栈中。在调用函数时,需要将函数所需的参数传递给它。这些参数通常以一定的顺序压入栈中,以便在函数内部使用。在 x86 架构中,参数的传递是通过将参数压入栈顶实现的。 2. 调用函数。函数调用的指令通常是 CALL 指令。在调用函数前,需要将函数的入口地址压入栈中,以便在函数执行完毕后返回到调用位置。CALL 指令会将当前的程序计数器(PC)压入栈中,并将函数的入口地址作为新的 PC。 3. 建立栈帧。在函数被调用时,需要为函数建立一个独立的栈帧,以便在函数内部使用局部变量和临时变量。栈帧通常包括以下几个部分:返回地址、旧的基址指针、局部变量和临时变量。在 x86 架构中,栈帧的建立是通过将 ESP 寄存器减去一个固定的值实现的。 4. 执行函数。在函数被调用后,CPU 会跳转到函数的入口地址并开始执行函数。函数内部可以通过栈中的参数和局部变量完成相应的计算和操作。 5. 返回值传递。在函数执行完毕后,需要将函数的返回值传递给调用者。在 x86 架构中,函数的返回值通常通过 EAX 寄存器传递。 6. 销毁栈帧。在函数执行完毕后,需要将栈帧销毁,以便释放栈空间。栈帧的销毁通常是通过将 ESP 寄存器还原到旧的基址指针处实现的。 7. 返回到调用位置。在函数执行完毕后,需要返回到函数被调用的位置。在 x86 架构中,返回指令通常是 RET 指令。RET 指令会将栈顶的返回地址弹出,并将其作为新的 PC。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值