您的位置:首页 > 其它

关于返回结构体的函数

2012-07-18 21:49 225 查看
    【前言】写作本文,源于最近回复的 《汇编中函数返回结构体的方法》 一文。在网络上也已经有一些相关文章和相关问题,有的文章已经给出了一部分结果,但总体而言还缺少比较重要的结论。本文以分析 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 版本,汇编代码如下。

large_struct_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 指向返回值,但这并不是一个简单容易的事情(因为编译器的行为效果是尽量避免让这个返回值被其他指针指到)。

    
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: