VC中函數返回值的存放與傳遞

fromr:http://blog.claudxiao.net/2010/02/return_value_of_vc/

教科書中一般說,在C/C++中,函數通過eax寄存器返回結果。如果結果不大於4字節,則eax就是它的值;如果大於4字節,則返回存放它的內存地址。

請思考如下的問題:

如果函數返回的結果大於4字節,那麼它被存放到哪裏了?

一般情況下,局部變量通過add esp -4*n或者push ecx從堆棧獲得存儲空間。如果結果也像局部變量這般,那麼返回以後,它有可能被後續操作覆蓋掉。所以應該把在調用函數前就爲它分配空間。

這段空間分配在哪裏?函數如何使用它?這是進一步引申出來的問題。

下面我們就做幾個實驗來觀察一下。(如果想直接看結果,可以跳到本文最後的總結。)

先介紹實驗環境。C源程序經過Microsoft Visual C++ 6.0自帶的命令行工具cl.exe(版本12.0.8168.0)編譯,不加任何參數。

先看返回值爲4字節的情況。源代碼如下:

typedef struct stSize4{
    char a;
    char b;
    short c;
} stSize4;

stSize4 stFunc(short num)
{
    stSize4 temp = {'t','h',num};
    return temp;
}

void main()
{
    stSize4 target;
    target = stFunc(1745);
}

編譯後,反彙編結果如下:

00401000  push      ebp                       ;stFunc()
00401001  mov       ebp, esp
00401003  push      ecx                       ;4字節局部變量temp
00401004  mov       byte ptr ss:[ebp-4], 74
00401008  mov       byte ptr ss:[ebp-3], 68
0040100C  mov       ax, word ptr ss:[ebp+8]   ;取出參數
00401010  mov       word ptr ss:[ebp-2], ax
00401014  mov       eax, dword ptr ss:[ebp-4] ;把值直接賦給eax
00401017  mov       esp, ebp
00401019  pop       ebp
0040101A  ret
0040101B  push      ebp                       ;main()
0040101C  mov       ebp, esp
0040101E  push      ecx                       ;4字節局部變量target
0040101F  push      6D1                       ;參數入棧
00401024  call      00401000
00401029  add       esp, 4                     ;堆棧平衡
0040102C  mov       dword ptr ss:[ebp-4], eax ;把eax的值保存
0040102F  mov       esp, ebp
00401031  pop       ebp
00401032  ret

正如教科書所言,結果被直接存儲在eax中返回了。

接下來我們寫一個返回值爲8字節的程序。

typedef struct stSize8{
    short a;
    short b;
    short c;
    short d;
} stSize8;

stSize8 stFunc(short num)
{
    stSize8 temp = {100,200,300,num};
    return temp;
}

void main()
{
    stSize8 target;
    target = stFunc(1745);
}

反彙編後,出現了一些不同的情況:

00401000  push      ebp                       ;stFunc()
00401001  mov       ebp, esp
00401003  sub       esp, 8                     ;8字節局部變量temp
00401006  mov       word ptr ss:[ebp-8], 64
0040100C  mov       word ptr ss:[ebp-6], 0C8
00401012  mov       word ptr ss:[ebp-4], 12C
00401018  mov       ax, word ptr ss:[ebp+8]   ;取出參數
0040101C  mov       word ptr ss:[ebp-2], ax
00401020  mov       eax, dword ptr ss:[ebp-8] ;低位的值存到eax
00401023  mov       edx, dword ptr ss:[ebp-4] ;高位的值存到edx
00401026  mov       esp, ebp
00401028  pop       ebp
00401029  ret
0040102A  push      ebp                       ;main()
0040102B  mov       ebp, esp
0040102D  sub       esp, 8                     ;8字節局部變量target
00401030  push      6D1                       ;參數入棧
00401035  call      00401000
0040103A  add       esp, 4                     ;堆棧平衡
0040103D  mov       dword ptr ss:[ebp-8], eax ;把eax的值存到局部變量
00401040  mov       dword ptr ss:[ebp-4], edx ;把edx的值存到局部變量
00401043  mov       esp, ebp
00401045  pop       ebp
00401046  ret

此時並不是通常說的eax返回一個地址,而是使用eax存儲8位結果的低4位值,同時使用ebx存儲8位結果的高四位值,一併返回給調用者。

再來寫一個3字節的:

typedef struct stSize3{
    char a;
    char b;
    char c;
} stSize3;

stSize3 stFunc(char ch)
{
    stSize3 temp = {'n','k',ch};
    return temp;
}

void main()
{
    stSize3 target;
    target = stFunc('c');
}

反匯編出來的結果又和上述兩種情況有很大不同:

00401000  push      ebp                       ;stFunc()
00401001  mov       ebp, esp
00401003  push      ecx                       ;4字節局部變量temp
00401004  mov       byte ptr ss:[ebp-4], 6E
00401008  mov       byte ptr ss:[ebp-3], 6B
0040100C  mov       al, byte ptr ss:[ebp+C]   ;取出參數,注意是+C而不是+8
0040100F  mov       byte ptr ss:[ebp-2], al
00401012  mov       ecx, dword ptr ss:[ebp+8] ;此時ebp+8是調用者臨時空間地址
00401015  mov       dx, word ptr ss:[ebp-4]
00401019  mov       word ptr ds:[ecx], dx     ;把temp的值拷到臨時空間
0040101C  mov       al, byte ptr ss:[ebp-2]
0040101F  mov       byte ptr ds:[ecx+2], al   ;分兩次拷,因爲是3字節內容
00401022  mov       eax, dword ptr ss:[ebp+8] ;最後把地址給eax返回
00401025  mov       esp, ebp
00401027  pop       ebp
00401028  ret
00401029  push      ebp                       ;main()
0040102A  mov       ebp, esp
0040102C  sub       esp, 8                     ;4字節局部變量target,4字節臨時空間
0040102F  push      63                        ;參數入棧
00401031  lea       eax, dword ptr ss:[ebp-8] ;/臨時空間地址入棧
00401034  push      eax                       ;\注意和參數的先後順序!
00401035  call      00401000
0040103A  add       esp, 8                     ;堆棧平衡,注意是8字節
0040103D  mov       cx, word ptr ds:[eax]     ;取出返回的地址處的值
00401040  mov       word ptr ss:[ebp-4], cx   ;存到局部變量target中
00401044  mov       dl, byte ptr ds:[eax+2]
00401047  mov       byte ptr ss:[ebp-2], dl
0040104A  mov       esp, ebp
0040104C  pop       ebp
0040104D  ret

我們來逐一分析改變之處。

首先,在main()中分配了8字節的堆棧空間,而不是temp所佔的3字節。這8字節的由來是:

考慮到內存對齊,先爲temp分配了4字節的空間。

然後,分配了4字節的臨時空間。在後面會看到,這4個字節被用於存放返回的結果。

另一個不同之處在於,參數入棧後,臨時空間的地址也入棧了。這一點任何一本書中都沒有提到。

因此帶來了兩個改變:一是在函數中訪問參數不再是[ebp+8],而是[ebp+C],因爲前者指向了臨時空間地址;二是在函數返回後(或者返回時)進行堆棧平衡,需要平衡的空間比參數大小多了4個字節。

最後,我們能看到,被調函數把返回結果的值放到了臨時空間,而把臨時空間的地址賦給了eax返回。

最後,我們再來看看6字節的情況:

typedef struct stSize6{
    short a;
    short b;
    short c;
}   stSize6;

stSize6 stFunc(short num)
{
    stSize6 temp = {100,200,num};
    return temp;
}

void main()
{
    stSize6 target;
    target = stFunc(1745);
}

反彙編後的結果是:

00401000  push      ebp                       ;stFunc()
00401001  mov       ebp, esp
00401003  sub       esp, 8                     ;8字節局部變量temp
00401006  mov       word ptr ss:[ebp-8], 64
0040100C  mov       word ptr ss:[ebp-6], 0C8
00401012  mov       ax, word ptr ss:[ebp+C]   ;取出參數,注意是+C而不是+8
00401016  mov       word ptr ss:[ebp-4], ax
0040101A  mov       ecx, dword ptr   ss:[ebp+8] ;此時ebp+8是調用者臨時空間地址
0040101D  mov       edx, dword ptr ss:[ebp-8]
00401020  mov       dword ptr ds:[ecx], edx   ;把temp的值拷到臨時空間
00401022  mov       ax, word ptr ss:[ebp-4]
00401026  mov       word ptr ds:[ecx+4], ax   ;分兩次拷,因爲是6字節內容
0040102A  mov       eax, dword ptr   ss:[ebp+8] ;最後把地址給eax返回
0040102D  mov       esp, ebp
0040102F  pop       ebp
00401030  ret
00401031  push      ebp                       ;main()
00401032  mov       ebp, esp
00401034  sub       esp, 10                    ;8字節局部變量target,8字節臨時空間
00401037  push      6D1                       ;參數入棧
0040103C  lea       eax, dword ptr   ss:[ebp-10];/臨時地址空間入棧
0040103F  push      eax                       ;\注意和參數的先後順序!
00401040  call      00401000
00401045  add       esp, 8                     ;堆棧平衡,注意是8字節
00401048  mov       ecx, dword ptr ds:[eax]   ;取出返回的地址處的值
0040104A  mov       dword ptr ss:[ebp-8],   ecx ;存到局部變量target中
0040104D  mov       dx, word ptr ds:[eax+4]
00401051  mov       word ptr ss:[ebp-4], dx
00401055  mov       esp, ebp
00401057  pop       ebp
00401058  ret

這和3字節時的情況沒什麼不同,只不過爲了內存對齊,爲6字節結構申請的空間爲8字節。

至此,我們可以給出如下結論:

1、 當返回結果爲4字節時,函數將它的值賦給eax返回。

2、 當返回結果爲8字節時,函數將它的值的低四位賦給eax,高四位賦給edx,然後返回。

3、 當返回結果爲其他大小時,調用者在自己的堆棧中申請一些臨時空間(位於局部變量之後,大小由內存對齊方式決定);調用函數時,在所有參數入棧以後將臨時空間的地址入棧;被調函數用訪問參數的方法訪問這個地址,將返回結果的值存到臨時空間中,並將其地址通過eax返回;最後,調用者平衡堆棧時,多平衡4個字節。

最後,本文參考了鄧際鋒的文章《vc如何返回函數結果及壓棧參數》,地址是:

http://blog.csdn.net/soloist/archive/2006/09/22/1267147.aspx

但該文認爲返回結果小於4字節時處理方法與4字節時相同,但沒有看到他測試這種情況,而在我的實驗環境下並不是這樣表現的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章