關於返回結構體的函數


    【前言】寫作本文,源於最近回復的 《匯編中函數返回結構體的方法》 一文。在網絡上也已經有一些相關文章和相關問題,有的文章已經給出了一部分結果,但總體而言還缺少比較重要的結論。本文以分析 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 指向返回值,但這並不是一個簡單容易的事情(因為編譯器的行為效果是盡量避免讓這個返回值被其他指針指到)。

    

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM