【前言】写作本文,源于最近回复的 《汇编中函数返回结构体的方法》 一文。在网络上也已经有一些相关文章和相关问题,有的文章已经给出了一部分结果,但总体而言还缺少比较重要的结论。本文以分析 VC6 编译器,32 位架构为主来重复性分析这个话题。
(一)不超过 8 bytes 的小结构体可以通过 EDX:EAX 返回。
本文的范例代码取材于 《汇编中函数返回结构体的方法》一文,并在此基础上进行修改和试验。要研究的第一份代码如下,定义一个不超过 8 bytes 的小结构体,不超过 8 bytes 是因为这个结构体能够用 EDX:EAX 容纳,我们之后将看到在 release 编译时,编译器能够向返回普通基础类型那样进行返回。
#include <stdio.h>//不超过 8 bytes 的“小结构体” struct A {int a;int b; };//返回结构体的函数 struct A add(int x, int y) {struct A t;t.a = x * y;return t; }int main() {struct A t = add(3, 4);printf("t.a = %ld\n", t.a);return 0; }
首先,我们需要解决一个常见困惑,就是要明确这段代码和下面的典型错误代码的区别:
char* get_buffer()
{
char buf[8];
return buf;
}
上面的 get_buffer 返回的是栈上的临时变量空间,在函数返回后,其所在的空间也就被“回收/释放”了,也就是说函数返回的地址位于栈的增长方向上,是不稳定和不被保证的。
那么返回结构体的函数则不同,你可以发现返回结构体的函数是工作正常有效的。在 add 函数中有一个临时性结构体 t,毫无疑问,t 将在 add 函数返回时被释放,但由于 t 被当做“值”进行返回,因此编译器将保证 add 的返回值对于 add 的调用者(caller)来说是有效的。
另外需要明确的一点是,我个人觉得,现实里这种返回结构体的方式比较少见,后面将会看到这样做会产生临时对象和多余拷贝过程,效率不高。常见方法是传递结构体指针。但作为语言上允许的方式,有必要弄清楚编译器如何实现这种方式,而要弄清楚这个问题,需要查看汇编代码。使用 VC6 输入上述代码,下面分别给出其汇编代码。
(1)debug 版本,汇编代码如下。
.text:00401020 add proc near ; CODE XREF: j_addj .text:00401020 .text:00401020 var_48 = dword ptr -48h .text:00401020 var_8 = dword ptr -8 .text:00401020 var_4 = dword ptr -4 .text:00401020 arg_0 = dword ptr 8 .text:00401020 arg_4 = dword ptr 0Ch .text:00401020 .text:00401020 push ebp .text:00401021 mov ebp, esp .text:00401023 sub esp, 48h .text:00401026 push ebx .text:00401027 push esi .text:00401028 push edi .text:00401029 lea edi, [ebp+var_48] .text:0040102C mov ecx, 12h .text:00401031 mov eax, 0CCCCCCCCh .text:00401036 rep stosd .text:00401038 mov eax, [ebp+arg_0] .text:0040103B imul eax, [ebp+arg_4] .text:0040103F mov [ebp+var_8], eax .text:00401042 mov eax, [ebp+var_8] .text:00401045 mov edx, [ebp+var_4] .text:00401048 pop edi .text:00401049 pop esi .text:0040104A pop ebx .text:0040104B mov esp, ebp .text:0040104D pop ebp .text:0040104E retn .text:0040104E add endp .text:0040104E .text:0040104E ; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪? .text:0040104F dd 4 dup(0CCCCCCCCh) .text:0040105F align 10h .text:00401060 .text:00401060 ; 圹圹圹圹圹圹圹?S U B R O U T I N E 圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹? .text:00401060 .text:00401060 ; Attributes: bp-based frame .text:00401060 .text:00401060 main proc near ; CODE XREF: j_mainj .text:00401060 .text:00401060 var_50 = dword ptr -50h .text:00401060 var_10 = dword ptr -10h .text:00401060 var_C = dword ptr -0Ch .text:00401060 var_8 = dword ptr -8 .text:00401060 var_4 = dword ptr -4 .text:00401060 .text:00401060 push ebp .text:00401061 mov ebp, esp .text:00401063 sub esp, 50h .text:00401066 push ebx .text:00401067 push esi .text:00401068 push edi .text:00401069 lea edi, [ebp+var_50] .text:0040106C mov ecx, 14h .text:00401071 mov eax, 0CCCCCCCCh .text:00401076 rep stosd .text:00401078 push 4 .text:0040107A push 3 .text:0040107C call j_add .text:00401081 add esp, 8 .text:00401084 mov [ebp+var_10], eax .text:00401087 mov [ebp+var_C], edx .text:0040108A mov eax, [ebp+var_10] .text:0040108D mov [ebp+var_8], eax .text:00401090 mov ecx, [ebp+var_C] .text:00401093 mov [ebp+var_4], ecx .text:00401096 mov edx, [ebp+var_8] .text:00401099 push edx .text:0040109A push offset ??_C@_0L@CMGB@t?4a?5?$DN?5?$CFld?6?$AA@ ; "t.a = %ld\n" .text:0040109F call printf .text:004010A4 add esp, 8 .text:004010A7 xor eax, eax .text:004010A9 pop edi .text:004010AA pop esi .text:004010AB pop ebx .text:004010AC add esp, 50h .text:004010AF cmp ebp, esp .text:004010B1 call __chkesp .text:004010B6 mov esp, ebp .text:004010B8 pop ebp .text:004010B9 retn .text:004010B9 main endp
下面是实现方式的栈示意图:
总结:
(1.1)用 edx:eax 传递返回值。调用方不需要在栈上向 add 函数传递接受返回值的地址。
(2.2)debug 版本在调用方生成临时对象返回值,然后再把临时对象拷贝到 main 临时变量所在地址。效率低。
(2)release 版本,汇编代码如下:
.text:00401000 sub_401000 proc near ; CODE XREF: sub_401020+7p .text:00401000 .text:00401000 var_4 = dword ptr -4 .text:00401000 arg_0 = dword ptr 4 .text:00401000 arg_4 = dword ptr 8 .text:00401000 .text:00401000 mov eax, [esp+arg_0] ; add 函数 .text:00401004 mov edx, [esp+var_4] .text:00401008 sub esp, 8 .text:0040100B imul eax, [esp+8+arg_4] .text:00401010 add esp, 8 .text:00401013 retn .text:00401013 sub_401000 endp .text:00401013 .text:00401013 ; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪? .text:00401014 align 10h .text:00401020 .text:00401020 ; 圹圹圹圹圹圹圹?S U B R O U T I N E 圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹? .text:00401020 .text:00401020 .text:00401020 sub_401020 proc near ; CODE XREF: start+AFp .text:00401020 .text:00401020 var_4 = dword ptr -4 .text:00401020 .text:00401020 sub esp, 8 ; 相当于 main 函数 .text:00401023 push 4 .text:00401025 push 3 .text:00401027 call sub_401000 .text:0040102C add esp, 8 .text:0040102F mov [esp+8+var_4], edx .text:00401033 push eax .text:00401034 push offset aT_aLd ; "t.a = %ld\n" .text:00401039 call sub_401050 .text:0040103E xor eax, eax .text:00401040 add esp, 10h .text:00401043 retn .text:00401043 sub_401020 endp
总结:
(2.1)同(1.1),用 edx:eax 传递返回值,不需要传递接收返回值的地址。
(2.2)release 版本调用方没有临时对象,效率基本等同于传结构体指针。
(2.3)release 版本优化的太厉害,甚至都没有把返回值完整的拷贝到临时变量 t (只拷贝了结构体中的成员t.b,t.a 的拷贝被认为没有存在价值而被优化掉了,因为 t.a 的值存于 eax),和高级语言有较大差别。
(二)超过 8 bytes 的结构体,调用方需要提供用于接收返回值的地址。
如果是超过 8 bytes 的结构体,EDX:EAX 将容纳不下,这时就需要调用方提供接受返回值的地址,即调用方在栈上分配临时对象,并把其地址通过栈传递给函数(先 push 参数,最后 push 用于设置返回值的结构体地址)。
把上述代码中的结构体定义增加一个 int 成员即可令结构体超过 8 bytes,即调整上述代码的 struct 定义:
struct A
{
int a;
int b;
int c;
};
使用 VC6 编译后产生的汇编代码如下:
debug 版本:
.text:00401020 add proc near ; CODE XREF: j_addj .text:00401020 .text:00401020 var_4C = dword ptr -4Ch .text:00401020 var_C = dword ptr -0Ch .text:00401020 var_8 = dword ptr -8 .text:00401020 var_4 = dword ptr -4 .text:00401020 arg_0 = dword ptr 8 .text:00401020 arg_4 = dword ptr 0Ch .text:00401020 arg_8 = dword ptr 10h .text:00401020 .text:00401020 push ebp .text:00401021 mov ebp, esp .text:00401023 sub esp, 4Ch .text:00401026 push ebx .text:00401027 push esi .text:00401028 push edi .text:00401029 lea edi, [ebp+var_4C] .text:0040102C mov ecx, 13h .text:00401031 mov eax, 0CCCCCCCCh .text:00401036 rep stosd .text:00401038 mov eax, [ebp+arg_4] .text:0040103B imul eax, [ebp+arg_8] .text:0040103F mov [ebp+var_C], eax .text:00401042 mov ecx, [ebp+arg_0] .text:00401045 mov edx, [ebp+var_C] .text:00401048 mov [ecx], edx .text:0040104A mov eax, [ebp+var_8] .text:0040104D mov [ecx+4], eax .text:00401050 mov edx, [ebp+var_4] .text:00401053 mov [ecx+8], edx .text:00401056 mov eax, [ebp+arg_0] .text:00401059 pop edi .text:0040105A pop esi .text:0040105B pop ebx .text:0040105C mov esp, ebp .text:0040105E pop ebp .text:0040105F retn .text:0040105F add endp .text:0040105F .text:00401060 .text:00401060 ; 圹圹圹圹圹圹圹?S U B R O U T I N E 圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹? .text:00401060 .text:00401060 ; Attributes: bp-based frame .text:00401060 .text:00401060 main proc near ; CODE XREF: j_mainj .text:00401060 .text:00401060 var_64 = dword ptr -64h .text:00401060 var_24 = dword ptr -24h .text:00401060 var_18 = dword ptr -18h .text:00401060 var_14 = dword ptr -14h .text:00401060 var_10 = dword ptr -10h .text:00401060 var_C = dword ptr -0Ch .text:00401060 var_8 = dword ptr -8 .text:00401060 var_4 = dword ptr -4 .text:00401060 .text:00401060 push ebp .text:00401061 mov ebp, esp .text:00401063 sub esp, 64h .text:00401066 push ebx .text:00401067 push esi .text:00401068 push edi .text:00401069 lea edi, [ebp+var_64] .text:0040106C mov ecx, 19h .text:00401071 mov eax, 0CCCCCCCCh .text:00401076 rep stosd .text:00401078 push 4 .text:0040107A push 3 .text:0040107C lea eax, [ebp+var_24] .text:0040107F push eax .text:00401080 call j_add .text:00401085 add esp, 0Ch .text:00401088 mov ecx, [eax] .text:0040108A mov [ebp+var_18], ecx .text:0040108D mov edx, [eax+4] .text:00401090 mov [ebp+var_14], edx .text:00401093 mov eax, [eax+8] .text:00401096 mov [ebp+var_10], eax .text:00401099 mov ecx, [ebp+var_18] .text:0040109C mov [ebp+var_C], ecx .text:0040109F mov edx, [ebp+var_14] .text:004010A2 mov [ebp+var_8], edx .text:004010A5 mov eax, [ebp+var_10] .text:004010A8 mov [ebp+var_4], eax .text:004010AB mov ecx, [ebp+var_C] .text:004010AE push ecx .text:004010AF push offset ??_C@_0L@CMGB@t?4a?5?$DN?5?$CFld?6?$AA@ ; "t.a = %ld\n" .text:004010B4 call printf .text:004010B9 add esp, 8 .text:004010BC xor eax, eax .text:004010BE pop edi .text:004010BF pop esi .text:004010C0 pop ebx .text:004010C1 add esp, 64h .text:004010C4 cmp ebp, esp .text:004010C6 call __chkesp .text:004010CB mov esp, ebp .text:004010CD pop ebp .text:004010CE retn .text:004010CE main endp
release 版本:
.text:00401000 sub_401000 proc near ; CODE XREF: sub_401030+Cp .text:00401000 .text:00401000 var_8 = dword ptr -8 .text:00401000 var_4 = dword ptr -4 .text:00401000 arg_0 = dword ptr 4 .text:00401000 arg_4 = dword ptr 8 .text:00401000 arg_8 = dword ptr 0Ch .text:00401000 .text:00401000 mov ecx, [esp+arg_4] .text:00401004 mov eax, [esp+arg_0] .text:00401008 sub esp, 0Ch .text:0040100B imul ecx, [esp+0Ch+arg_8] .text:00401010 mov edx, eax .text:00401012 mov [edx], ecx .text:00401014 mov ecx, [esp+0Ch+var_8] .text:00401018 mov [edx+4], ecx .text:0040101B mov ecx, [esp+0Ch+var_4] .text:0040101F mov [edx+8], ecx .text:00401022 add esp, 0Ch .text:00401025 retn .text:00401025 sub_401000 endp .text:00401025 .text:00401025 ; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪? .text:00401026 align 10h .text:00401030 .text:00401030 ; 圹圹圹圹圹圹圹?S U B R O U T I N E 圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹? .text:00401030 .text:00401030 .text:00401030 sub_401030 proc near ; CODE XREF: start+AFp .text:00401030 .text:00401030 var_14 = dword ptr -14h .text:00401030 var_10 = dword ptr -10h .text:00401030 var_C = dword ptr -0Ch .text:00401030 .text:00401030 sub esp, 18h .text:00401033 push 4 .text:00401035 lea eax, [esp+1Ch+var_C] .text:00401039 push 3 .text:0040103B push eax .text:0040103C call sub_401000 .text:00401041 mov ecx, eax .text:00401043 add esp, 0Ch .text:00401046 mov eax, [ecx] .text:00401048 push eax .text:00401049 push offset aT_aLd ; "t.a = %ld\n" .text:0040104E mov edx, [ecx+4] .text:00401051 mov [esp+20h+var_14], edx .text:00401055 mov ecx, [ecx+8] .text:00401058 mov [esp+20h+var_10], ecx .text:0040105C call sub_401070 .text:00401061 xor eax, eax .text:00401063 add esp, 20h .text:00401066 retn .text:00401066 sub_401030 endp
上述两种编译结果,实现的模型基本相同。因此在这里以debug版本代码为主,一并分析,其栈示意图如下,下图左侧为 debug 版本,右侧是 release 版本:
总结:
(1)当结构体超过 8 bytes,不能用 EDX:EAX 传递,这时调用方在栈上保留有一个用于填充返回值的结构体,其地址在入栈参数后 push 到栈上。函数将会根据这个地址,把返回值设置到这个地址。
(2)在 main 函数中,debug 版本比 release 版本还多了一个临时对象,效率低。而 release 版本中只有返回值和临时变量 t,效率略高于 debug。但两者模型基本一致,总体效率低于传结构体指针。
(3)release 版本同样优化比较厉害,main 函数中对 t 的赋值是不完整的,因为编译器认为没有必要,只要满足代码等效即可。
最后我们总结针对较大结构体(超过 8 bytes)时,返回结构体的函数的实现方式的基本模型:
(1)调用方在栈上分配用于接收返回值的临时结构体,并把地址通过栈传递给函数。
(2)函数根据返回值的地址,设置返回值。
(3)调用方根据需要,把返回值再赋值给需要的临时变量。
(4)返回时,eax 存储的是返回值的那个地址。
因此,从上面的过程可以看到,由于存在临时对象和拷贝操作,其效率比传递结构体指针的函数低。
由于不管 debug 还是 release,对于“大结构体”都会在栈上传递返回值的地址,所以我们可以通过下面的代码,来测试出这样的结论:函数 add 的返回值(临时结构体)的地址和 main 中的变量 t 的地址是不同的。原理是,第一个形参的栈顶方向的相邻元素就是返回值的地址,因此用一个指针指向第一个形参,然后向栈顶移动一格,取出其值,就是返回值的地址。
#include <stdio.h>struct A {int a;int b;int c; };struct A add(int x, int y) {struct A t;int* p = &x;p--;printf("address of return struct: %08X\n", *p);t.a = x * y;return t; }int main(int argc, char* argv[]) {struct A t = add(3, 4);struct A *p1 = &t;printf("address of t in main: %p\n", &t);return 0; }
上面的代码中,有一点需要注意,返回值的地址和 t 的地址的关系是依赖编译器的,也就是说,没有任何保证,两者之间是否相邻以及它们之间的大小关系。但你可以通过尝试移动上面的指针 p1,试图将 p1 指向返回值,但这并不是一个简单容易的事情(因为编译器的行为效果是尽量避免让这个返回值被其他指针指到)。