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会将待引用的地址返回,无匿名对象,无匿名对象的复制构造函数一说。
C/C++ return 如何实现的?return 的内部机制相关推荐
- 你真的了解try{ return }finally{}中的return?
你真的了解try{ return }finally{}中的return? 今天去逛论坛 时发现了一个很有趣的问题: 谁能给我我解释一下这段程序的结果为什么是:2.而不是:3 代码如下: class T ...
- servlet中实现页面跳转return “r:”和return “f:
servlet中实现页面跳转return "r:"和return "f:"的区别和作用 2015-07-28 14:22741830480 | 浏览 48 次 ...
- python返回值return用法_Python中return函数返回值代码实例用法
本篇文章小编给大家分享一下Python中return函数返回值代码实例用法,文章代码介绍的很详细,小编觉得挺不错的,现在分享给大家供大家参考,有需要的小伙伴们可以来看看. return 添加返回值 r ...
- 'main' : function should return a value; 'void' return type assumed/////undeclared identifier
'main' : function should return a value; 'void' return type assumed 'c' : undeclared identifier 在调试c ...
- python3 return用法_Python中return语句用法实例分析
本文实例讲述了Python中return语句用法.分享给大家供大家参考.具体如下: return语句: return语句用来从一个函数 返回 即跳出函数.我们也可选从函数 返回一个值 . 使用字面意义 ...
- python if return语句_Python: return语句
人生苦短,我用Python 环境:Windows 10 64-bit, python == 3.6.4 , PyCharm CE == 2018.1 声明:学习资源来自于网络,这里是自己学习笔记总结与 ...
- python中return的理解-Python return语句 函数返回值
return语句是从python 函数返回一个值,在讲到定义函数的时候有讲过,每个函数都要有一个返回值.Python中的return语句有什么作用,今天就来仔细的讲解一下. python 函数返回值 ...
- python if写在return 后面_python中return如何写
python中return的用法 1.return语句就是把执行结果返回到调用的地方,并把程序的控制权一起返回 程序运行到所遇到的第一个return即返回(退出def块),不会再运行第二个return ...
- 云漫圈 | finally到底是在return之前执行还是return之后执行?
戳蓝字"CSDN云计算"关注我们哦! 文章来自:程序员乔戈里作者:乔戈里qgl --下课后-- public class Main { public static void mai ...
最新文章
- centos 7 安装jdk1.8
- python3 列表倒叙_python的列表、元组、字典、集合
- 基本类型与字符串之间的转换
- Android 中文 API ——对话框 AlertDialog.Builder
- 【SQLAlchemy】MySQL server has gone away 原因分析、解决方法
- 图像相似性搜索的原理
- 面向对象与基于对象 区别
- .NET Core 2.0迁移技巧之web.config配置文件
- 新手向:如何理解OpenGL中着色器,渲染管线,光栅化等概念
- Python爬虫实战01:Requests+正则表达式爬取猫眼电影
- 基于SSM的驾校网站
- 今天居然中了MSN病毒。
- Learun框架的入门问题
- 【转载】完全二叉树的高度为什么是对lgN向下取整
- 全国青少年软件编程等级考试标准 (预备级)1-4级
- Facebook 企业广告账户开户流程、资料准备、开户时间、开户须知及OE链接
- 【19调剂】其它调剂信息(计算机/软件专业)【3.56】
- Java包括jvm及API,Java基础(下)(JVM、API)
- edge阅读文章实用技巧
- 常见的几种web容器(Apache、Nginx、Tomcat)
热门文章
- php拓展so,PHP扩展开发之动态加载so模块与静态重编译PHP(上)-Go语言中文社区...
- Mesh(802.11s)组网 — 基于OpenWRT路由器
- 使用Marked库出现bug:Uncaught TypeError: marked is not a function at <anonymous>:1:14
- URL 重写的常见用法
- House of sprit一谈
- Android maxEms和maxLength
- OpenWrt 软路由介绍
- 如何搭建vue项目,完整搭建vue项目
- java 对象查找_Java如何从数组中查找对象元素?
- 【C语言经典100例】 -- 28 有5个人坐在一起,问第五个人多少岁?他说比第4个人大2岁。问第4个人岁数,他说比第3个人大2岁。问第三个人,又说比第2人大两岁。问第2个人,说比第一个人大两岁。最后