C/C++ return 如何实现的?return 的内部机制

本篇博客,我们来看看,在C/C++里面函数的return 关键字究竟做了什么工作,我们从return 基本的数据类型 像int/char/void */,到带构造函数的类,一步步分析。

return int/char,void* 以及他们的引用···

#include <stdio.h>
#include <stdlib.h>
int bfunc()
{
    int rst = 0;
    return rst;
}

void * bfunc2()
{
    void * p = NULL;
    return p;
}
char & bfun3()
{
    char ch = 0;
    return ch;
}
int main()
{

    int ret1 = bfunc();
    bfunc2();
    void * p = bfunc2();
    bfun3();
    char ch = bfun3();
    system("pause");
}

代码很简单,从int ret1=bfunc()开始,该函数返回一个基本类型int,我们来看看汇编结果:

    45:     int ret1 = bfunc();
00DD15EE  call        bfunc (0DD11FEh)  
00DD15F3  mov         dword ptr [ret1],eax  

在00DD15EE 调用bfunc()
我们进入bfun():

    12: int bfunc()
    13: {
00DD1490  push        ebp  
00DD1491  mov         ebp,esp  
00DD1493  sub         esp,0CCh  
00DD1499  push        ebx  
00DD149A  push        esi  
00DD149B  push        edi  
00DD149C  lea         edi,[ebp-0CCh]  
00DD14A2  mov         ecx,33h  
00DD14A7  mov         eax,0CCCCCCCCh  
00DD14AC  rep stos    dword ptr es:[edi]  
    14:     int rst = 0;
00DD14AE  mov         dword ptr [rst],0  
    15:     return rst;
00DD14B5  mov         eax,dword ptr [rst]  
    16: }
00DD14B8  pop         edi  
00DD14B9  pop         esi  
00DD14BA  pop         ebx  
00DD14BB  mov         esp,ebp  
00DD14BD  pop         ebp  
00DD14BE  ret  

我们看最后的几步:

    15:     return rst;
00DD14B5  mov         eax,dword ptr [rst]  

可知 return rst是把局部变量rst的值保存在eax寄存器;
最后,函数调用完毕,执行

00DD15F3  mov         dword ptr [ret1],eax  

将刚刚寄存器eax里面的内容给[ret1].

这个例子相当简单,但是不妨碍它说明了函数return 一个基本数据类型的时候是先把返回结果放在eax这一规则。

ok,继续看。
接下来是

    bfunc2();
    void * p = bfunc2();

其中bfunc2()的返回值并没有保存下来,void *p =bfun2()则将返回值保存到p里面了。
我们看看汇编结果:

    47:     bfunc2();
00FA15F6  call        bfunc2 (0FA1208h)  
    48:     void * p = bfunc2();
00FA15FB  call        bfunc2 (0FA1208h)  
00FA1600  mov         dword ptr [p],eax  

首先 bfun();和void * p=bfunc2()区别在于 是否有把eax的值mov到指定的内存。
然后 我们深入bfun():

   18: void * bfunc2()
    19: {
00FA1440  push        ebp  
00FA1441  mov         ebp,esp  
00FA1443  sub         esp,0CCh  
00FA1449  push        ebx  
00FA144A  push        esi  
00FA144B  push        edi  
00FA144C  lea         edi,[ebp-0CCh]  
00FA1452  mov         ecx,33h  
00FA1457  mov         eax,0CCCCCCCCh  
00FA145C  rep stos    dword ptr es:[edi]  
    20:     void * p = NULL;
00FA145E  mov         dword ptr [p],0  
    21:     return p;
00FA1465  mov         eax,dword ptr [p]  
    22: }
00FA1468  pop         edi  
00FA1469  pop         esi  
00FA146A  pop         ebx  
00FA146B  mov         esp,ebp  
00FA146D  pop         ebp  
00FA146E  ret  

与return int的类似,也是在return的时候把return 后面的表达式的值保存到eax寄存器中。

这是因为void * 指针 变量其实也是c/C++的基本数据类型。

ok,那我们看看返回引用的那个函数的调用:

    49:     bfun3();
00FA1603  call        bfun3 (0FA1203h)  
    50:     char ch = bfun3();
00FA1608  call        bfun3 (0FA1203h)  
00FA160D  mov         al,byte ptr [eax]  
00FA160F  mov         byte ptr [ch],al  

首先在调用上,char ch=bfun3()比bfunc3()多了

mov         al,byte ptr[eax];
mov         byte ptr [ch],al 

为什么是与al相关呢?这是因为 char是8位的,只需要al就可以了。
我们看内部:

    23: char & bfun3()
    24: {
00FA1560  push        ebp  
00FA1561  mov         ebp,esp  
00FA1563  sub         esp,0D0h  
00FA1569  push        ebx  
00FA156A  push        esi  
00FA156B  push        edi  
00FA156C  lea         edi,[ebp-0D0h]  
00FA1572  mov         ecx,34h  
00FA1577  mov         eax,0CCCCCCCCh  
00FA157C  rep stos    dword ptr es:[edi]  
00FA157E  mov         eax,dword ptr ds:[00FA8000h]  
00FA1583  xor         eax,ebp  
00FA1585  mov         dword ptr [ebp-4],eax  
    25:     char ch = 0;
00FA1588  mov         byte ptr [ch],0  
    26:     return ch;
00FA158C  lea         eax,[ch]  
    27: }
00FA158F  push        edx  
00FA1590  mov         ecx,ebp  
00FA1592  push        eax  
00FA1593  lea         edx,ds:[0FA15B4h]  
00FA1599  call        @_RTC_CheckStackVars@8 (0FA108Ch)  
00FA159E  pop         eax  
00FA159F  pop         edx  
00FA15A0  pop         edi  
00FA15A1  pop         esi  
00FA15A2  pop         ebx  
00FA15A3  mov         ecx,dword ptr [ebp-4]  
00FA15A6  xor         ecx,ebp  
00FA15A8  call        @__security_check_cookie@4 (0FA1023h)  
00FA15AD  mov         esp,ebp  
00FA15AF  pop         ebp  
00FA15B0  ret  

哈哈 关键来啦:

    26:     return ch;
00FA158C  lea         eax,[ch]  

lea eax,[ch] ====>emmmmm,取ch的有效地址。
so,这下子应该心里有底了吧!返回引用的时候,其实是返回 ch变量的有效地址,而这个变量是个局部变量,该有效地址所指向的内存是有可能被下一次函数调用的函数栈帧破坏的,所以返回一个局部变量的引用可以说是很危险的。

返回地址 后那怎么搞嘞?

    50:     char ch = bfun3();
00FA1608  call        bfun3 (0FA1203h)  
00FA160D  mov         al,byte ptr [eax]  
00FA160F  mov         byte ptr [ch],al  

注意看:调用完 bfun3后,eax存着bfun3()函数里面ch的有效地址,接着首先通过mov al, byte ptr[eax],指令将eax的值视为一个byte类型的有效地址,取出这个地址下的内存的值,保存到al中,然后再将al的值mov到 main函数的ch变量中。

下面我们看一点高级的:

return struct/class,以及它们的引用

先看实例代码:

#include <stdio.h>
#include <stdlib.h>
#pragma pack(1) 
struct Person
{
    int age;
    int id;
    int somethingelse;
    char name[12];
    void display()
    {
        printf("age :%X id:%X\n", age, id);
        printf("age:%X \n", &age);
        printf("id:%X \n", &id);
        printf("somethingelse:%X \n", &somethingelse);
        printf("name:%X \n", name);
    }
    Person()
    {
        printf("Call Person()\n");
    }
    Person(const Person&)
    {
        printf("Call Person(const Person&)\n");
    }
};

Person sfunc2()
{
    Person p ;
    return p;
}
Person sfunc3()
{
    int length = sizeof(Person);
    char * rst = new char[length];
    for (int i = 0; i < length; i++)
    {
        rst[i] = 0xBB;
    }
    return *((Person*)(rst));
}
Person& sfunc4()
{
    Person* p = new Person();
    return *p;
}
int main()
{

    Person p;
    sfunc2().display();
    p = sfunc2();
    p.display();
    sfunc3().display();
    sfunc4().display();
    p = sfunc4();
    p.display();
    system("pause");
    return 0;
}

其中有一个Person的结构体,里面有4+4+4+12=24个字节的变量,以及定义了一个默认构造函数和复制构造函数。
然后分别有三个函数sfunc(),sfunc1(),sfunc2(),代表了返回栈对象,返回动态对象,返回动态对象的引用的情况。
main函数里面,获取返回值的也有不获取返回值的。

ok,我们先看sfunc2().display()的情况。
这个过程先调用sfunc2(),sfunc2()返回一个Person对象,然后再调用这个对象的display()方法。

    51:     sfunc2().display();
013F1890  lea         eax,[ebp-140h]  
013F1896  push        eax  
013F1897  call        sfunc2 (013F11E0h)  
013F189C  add         esp,4  
013F189F  mov         ecx,eax  
013F18A1  call        Person::display (013F1046h)  

其过程为:
取[ebp-140h]的有效地址保存到eax,然后把eax压栈,然后再去调用sfunc2()。我们知道一般调用函数前的push的都是函数的参数,但是明明sfunc2()无须参数,那为什么会把ebp-140hpush进去嘞?我们接着看:
先记录一下刚刚压入的ebp-140H的值为0x00eff9f8,待会我们还会看到这个地址。
进入Person sfunc2()内部:

    28: Person sfunc2()
    29: {
013F1610  push        ebp  
013F1611  mov         ebp,esp  
013F1613  sub         esp,0E4h  
013F1619  push        ebx  
013F161A  push        esi  
013F161B  push        edi  
013F161C  lea         edi,[ebp-0E4h]  
013F1622  mov         ecx,39h  
013F1627  mov         eax,0CCCCCCCCh  
013F162C  rep stos    dword ptr es:[edi]  
013F162E  mov         eax,dword ptr ds:[013FA000h]  
013F1633  xor         eax,ebp  
013F1635  mov         dword ptr [ebp-4],eax  
    30:     Person p ;
013F1638  lea         ecx,[p]  
013F163B  call        Person::Person (013F11FEh)  
    31:     return p;
013F1640  lea         eax,[p]  
013F1643  push        eax  
013F1644  mov         ecx,dword ptr [ebp+8]  
013F1647  call        Person::Person (013F102Dh)  
013F164C  mov         eax,dword ptr [ebp+8]  
    32: }
013F164F  push        edx  
013F1650  mov         ecx,ebp  
013F1652  push        eax  
013F1653  lea         edx,ds:[13F1680h]  
    32: }
013F1659  call        @_RTC_CheckStackVars@8 (013F1096h)  
013F165E  pop         eax  
013F165F  pop         edx  
013F1660  pop         edi  
013F1661  pop         esi  
013F1662  pop         ebx  
013F1663  mov         ecx,dword ptr [ebp-4]  
013F1666  xor         ecx,ebp  
013F1668  call        @__security_check_cookie@4 (013F101Eh)  
013F166D  add         esp,0E4h  
013F1673  cmp         ebp,esp  
013F1675  call        __RTC_CheckEsp (013F1154h)  
013F167A  mov         esp,ebp  
013F167C  pop         ebp  
013F167D  ret  

重点看return p后面的代码:

   30:  Person p ;
013F1638  lea         ecx,[p]  
013F163B  call        Person::Person (013F11FEh)  
    31:     return p;
013F1640  lea         eax,[p]  
013F1643  push        eax  
013F1644  mov         ecx,dword ptr [ebp+8]  
013F1647  call        Person::Person (013F102Dh)  
013F164C  mov         eax,dword ptr [ebp+8]  
    32: }

首先 取p的地址保存到eax寄存器,然后把eax的地址压栈。
接着取ebp+8的地址到ecx寄存器,我们都知道ebp+4是函数返回地址,而ebp+8其实是函数的传入参数,也就是在main函数中汇编代码压入的
0x00eff9f8
接下来call Person::Person(013F102Dh)

    22:     Person(const Person&)
013F1420  push        ebp  
013F1421  mov         ebp,esp  
013F1423  sub         esp,0CCh  
013F1429  push        ebx  
013F142A  push        esi  
013F142B  push        edi  
013F142C  push        ecx  
013F142D  lea         edi,[ebp-0CCh]  
013F1433  mov         ecx,33h  
013F1438  mov         eax,0CCCCCCCCh  
013F143D  rep stos    dword ptr es:[edi]  
013F143F  pop         ecx  
013F1440  mov         dword ptr [this],ecx  
    23:     {
    24:         printf("Call Person(const Person&)\n");
013F1443  mov         esi,esp  
013F1445  push        13F78BCh  
013F144A  call        dword ptr ds:[13FB118h]  
013F1450  add         esp,4  
013F1453  cmp         esi,esp  
013F1455  call        __RTC_CheckEsp (013F1154h)  
    25:     }
013F145A  mov         eax,dword ptr [this]  
013F145D  pop         edi  
013F145E  pop         esi  
013F145F  pop         ebx  
013F1460  add         esp,0CCh  
013F1466  cmp         ebp,esp  
013F1468  call        __RTC_CheckEsp (013F1154h)  
013F146D  mov         esp,ebp  
013F146F  pop         ebp  
013F1470  ret         4  

关键代码在:


013F142C  push        ecx  
013F142D  lea         edi,[ebp-0CCh]  
013F1433  mov         ecx,33h  
013F1438  mov         eax,0CCCCCCCCh  
013F143D  rep stos    dword ptr es:[edi]  
013F143F  pop         ecx  
013F1440  mov         dword ptr [this],ecx  

首先把ecx的值压入栈顶,后面又给弹回来到ecx寄存器,而此时这个ecx的值其实就是0x00eff9f8,
然后就是 013F1440 mov dword ptr [this],ecx 这一句揭示了ecx的0x00eff9f8其实就当前构造函数的this指针。

我们回想一下0x00eff9f8这个地址其实是在刚刚main()函数栈帧里面的某一个地址,因为调用的是返回对象的函数,所以编译器会先在调用这个函数(在上面的例子中是main函数)的所在的函数栈上搞出一个匿名的对象出来,并将这个对象的指针动作这个返回对象函数(如sfunc2())的一个参数压入栈中。在sfunc2()函数的return的时候,就要调用这个调用这个匿名对象的复制构造函数将匿名对象的各成员进行初始化或者赋值。

我们再看sfunc2()在调用完这个复制构造函数后:

    31:     return p;
013F1640  lea         eax,[p]  
013F1643  push        eax  
013F1644  mov         ecx,dword ptr [ebp+8]  
013F1647  call        Person::Person (013F102Dh)  
013F164C  mov         eax,dword ptr [ebp+8]  
    32: }

mov eax, dword ptr [ebp+8]
将匿名对象的地址放到eax寄存。这样sfunc2()就执行完了,接着:

    51:     sfunc2().display();
013F1890  lea         eax,[ebp-140h]  
013F1896  push        eax  
013F1897  call        sfunc2 (013F11E0h)  
013F189C  add         esp,4  
013F189F  mov         ecx,eax  
013F18A1  call        Person::display (013F1046h)  

将匿名对象的地址由eax存到ecx,再调用display()函数,在display函数中通过ecx寄存可以知道当前display内部的this指针是0x00eff9f8

013F189F  mov         ecx,eax  
013F18A1  call        Person::display (013F1046h)  。

return 栈对象的流程为:
1。在调用返回一个对象的函数前,会在当前的函数所在的作用域生成一块栈内存区域用于存储一个匿名的对象,这个对象没有调用过构造函数。编译器将这个匿名对象的内存首地址作为一个参数压入栈顶,共该返回对象的函数使用。
2。return的时候,用return 后面表达式的对象作为参数来调用 1 中的匿名对象的复制构造函数。
3。把匿名对象的地址存入eax
4。ret 返回原函数。

然后原函数通过eax里面的匿名对象的指针来获取这个对象的数据。

    p = sfunc2();
    p.display();

比sfunc2().display()多了一步将函数调用后eax所指向的匿名对象赋值给p的过程。其他一样。
sfunc2()与sfunc3()一样,
但是sfunc4()有点特殊。
调用过程:

    55:     sfunc4().display();
013F18F6  call        sfunc4 (013F1082h)  
013F18FB  mov         ecx,eax  
013F18FD  call        Person::display (013F1046h)  

没有像上面返回对象的函数那样将 某个匿名对象的地址压栈。
函数内部:

    43: Person& sfunc4()
    44: {
013F1770  push        ebp  
013F1771  mov         ebp,esp  
013F1773  push        0FFFFFFFFh  
013F1775  push        13F512Eh  
013F177A  mov         eax,dword ptr fs:[00000000h]  
013F1780  push        eax  
013F1781  sub         esp,0E8h  
013F1787  push        ebx  
013F1788  push        esi  
013F1789  push        edi  
013F178A  lea         edi,[ebp-0F4h]  
013F1790  mov         ecx,3Ah  
013F1795  mov         eax,0CCCCCCCCh  
013F179A  rep stos    dword ptr es:[edi]  
013F179C  mov         eax,dword ptr ds:[013FA000h]  
013F17A1  xor         eax,ebp  
013F17A3  push        eax  
013F17A4  lea         eax,[ebp-0Ch]  
013F17A7  mov         dword ptr fs:[00000000h],eax  
    45:     Person* p = new Person();
013F17AD  push        18h  
013F17AF  call        operator new (013F11A4h)  
013F17B4  add         esp,4  
013F17B7  mov         dword ptr [ebp-0E0h],eax  
013F17BD  mov         dword ptr [ebp-4],0  
013F17C4  cmp         dword ptr [ebp-0E0h],0  
013F17CB  je          sfunc4+70h (013F17E0h)  
013F17CD  mov         ecx,dword ptr [ebp-0E0h]  
013F17D3  call        Person::Person (013F11FEh)  
013F17D8  mov         dword ptr [ebp-0F4h],eax  
013F17DE  jmp         sfunc4+7Ah (013F17EAh)  
013F17E0  mov         dword ptr [ebp-0F4h],0  
013F17EA  mov         eax,dword ptr [ebp-0F4h]  
013F17F0  mov         dword ptr [ebp-0ECh],eax  
013F17F6  mov         dword ptr [ebp-4],0FFFFFFFFh  
013F17FD  mov         ecx,dword ptr [ebp-0ECh]  
013F1803  mov         dword ptr [p],ecx  
    46:     return *p;
013F1806  mov         eax,dword ptr [p]  
    47: }

它没有调用匿名对象的复制构造函数,(因为它根本不需要匿名对象了),而是直接将p的有效地址保存到eax.
这也就解释了为什么 函数返回对象的引用一般比返回对象更高效一些:

返回引用无匿名对象产生,无须调用匿名对象的构造函数,自然也就没有匿名对象的析构问题。

总结

1.return 基本数据类型(char,int,void*,float,double)是,将中间结果保存至eax,调用者通过访问eax寄存器来获取函数返回结果。

2.返回引用其实返回的是被引用的对象的地址

3.A函数调用返回对象的函数F时,会在A的栈帧生成一个匿名对象,然后再F return的时候调用这个匿名对象的复制构造函数将return 表达式里面对象的成员变量的值传送到匿名对象中。

4.A函数调用返回对象引用的函数F时,F会将待引用的地址返回,无匿名对象,无匿名对象的复制构造函数一说。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值