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字節時相同,但沒有看到他測試這種情況,而在我的實驗環境下並不是這樣表現的。