一. 简介
最近有同学问为什么函数返回值选择返回对象指针而不是对象,所以就此展开来深入分析一下。
二. 一个简单的小例子
首先从一个最简单的例子开始开。在如下代码中,foo()
返回整型123,main()
函数调用foo()
之后返回。
1 |
|
下面分别是windows下和Linux下的反汇编结果,可见其汇编执行是类似的,均将123(十六进制为0x7b)存放在eax
之中。即先用寄存器eax
临时保存返回值,然后将该返回值再从eax
中取出。
这里很有趣的是在windows下,可以看到有一步给eax
赋初始值的步骤,而初始值为00CCCCCCCCh
。这就是众所周知的“烫烫烫”的来源了:以文本形式显示的话,cccc
恰好对应“烫”这个字。
1 | // Win10 |
1 | // Ubuntu 16.04 |
三. 复杂点的小例子
第二个例子将返回值从一个整型扩大到了128字节,由此观察eax
存放不下的时候该如何是好。一样是很简单的代码,如下所示。
1 |
|
下面照例是Windows和Linux的对比结果。可以看到这里Windows
的反汇编相较于Linux
简洁了很多,其原因是Windows
下使用了复合指令rep movs
指令来循环执行,而Linux
下则将循环展开直接写了出来,其实做的工作是一样的,下面拿Windows为例介绍。
rep movs
含义为重复movs
指令直到ecx
寄存器为0,rep movs a, b
意思就是将b
指向位置上的若干个字节拷贝到由a指定的位置上,循环拷贝的计数器为ecx
。在我们这里ecx
赋值为0x80,即128。rsi
指向rax
,rdi
指向rbp + 180
的位置。这里注意,我们一般认为栈是从高地址向低地址增长,但是在windows下并非一定如此,这个是很多人都忽视的重要一点(与之相对,可见Linux指向的是rbp - 0x90
)。
回归正题,这里rep movs
实际是将rax
存储的结构体b
的值传出到指定地址rbp + 180h
开始的一块区域临时存储,然后return_test()
将该临时区域的首地址赋值给rax
并返回main()
函数,再从rax
读取到临时存储区域,赋值给n
。所以在这里big_thing
其实总共存储了三份:b
,n
以及临时存储区域(rbp + 180h
)。换句话说,b
并未直接返回赋值给n
,而是需要多存储一份在临时区域作为中转。
1 | // Win10 |
1 | // Ubuntu 16.04 |
三. C++的小例子
说了这么多,大家应该已经明白了。C/C++的函数返回值会暂时存在一个临时空间,然后再赋给我们指定的变量,从而完成返回值的获取。因此如果是C++的对象,就会占用很大的临时空间,而且需要多一次拷贝构造和析构的过程,代价很大,所以尽量避免。如下代码是一个小例子来验证我们的想法是否正确。
1 |
|
如果没有使用临时空间,那么应该会有两次构造和两次析构过程(b
和obj
),而实际上这里的输出如下所示
1 | constructor // obj构造 |
同样打开反汇编看看,会发现的确是调用了拷贝构造函数保存了一份临时对象数据在临时空间里。
1 | obj = returnTest(); |
总结
分析到这里,大家应该都已经清楚了,当rax
无法直接存储返回值的时候,会在栈上分配一块空间存储返回值,用rax
返回其地址,然后再赋值给我们指定的对象接收返回值,因此代价较为高昂,建议尽量少用以提高代码性能。另外,希望大家能通过本文学会一点汇编的简单使用和分析方法。汇编不是返祖,更不是装逼,而是为了帮助我们更好地学习操作系统及硬件知识,更好的了解高级语言背后究竟做了什么,以便于写出更高效的代码,成为更好的程序员。