• 关于返回结构体的函数


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

    small_struct_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 版本,汇编代码如下:

    small_struct_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 版本:

    large_struct_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 版本:

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

        

  • 相关阅读:
    RPD Volume 168 Issue 4 March 2016 评论1
    初步接触CERNVM
    java中重载与重写的区别
    第三节 java 函数
    第二节 java流程控制(循环结构)
    第二节 java流程控制(判断结构+选择结构)
    JAVA 对象引用,以及对象赋值
    Java学习笔记整理第一章 java基本数据类型、修饰符、运算符
    plantuml的使用
    力扣 第210题
  • 原文地址:https://www.cnblogs.com/hoodlum1980/p/2598185.html
Copyright © 2020-2023  润新知