教科书中一般说,在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字节时相同,但没有看到他测试这种情况,而在我的实验环境下并不是这样表现的。