VC中函数返回值的存放与传递

转载 2016年05月31日 09:50:20

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

举报

相关文章推荐

误人子弟篇之C语言函数返回值与参数传递

写篇博客证明还干着程序员这勾当,并乐此不疲,,, 写在开头以免看到结尾你骂人,此篇博客纯属瞎扯,看看就可以了,不要当真哦! 如果搞过汇编,写过子程序,那么你就不用看了,因为看到最后你会发现,在汇编中你...

函数返回值如何传递

一般情况下,函数返回值是通过eax进行传递的,但是eax只能存储4个字节的信息,对于那些返回值大于4个字节的函数,返回值是如何传递的呢?假设返回值大小为M字节1. M 2. 4 3. M > 8,如何...

我是如何成为一名python大咖的?

人生苦短,都说必须python,那么我分享下我是如何从小白成为Python资深开发者的吧。2014年我大学刚毕业..

如何在Shell 中正确的传递函数返回值

一个Shell 函数返回值引发的问题、该问题的Debug 过程以及结论。

【从C到C++学习笔记】引用/const引用/引用传递/引用作为函数返回值/引用和指针的区别

C++ 二维数组作为函数返回值类型传递方式举例

#include 002 #define ROW 3 003 #define COL 4 004...

函数返回值与参数传递

对结构体返回有些疑问,我在函数内部定义了一个结构体并打印出他的地址,又在main函数中定义一个结构体等于函数返回再打印地址,得到的结果竟然是一样的。今天看到一篇很好的博文,记录如下。原博文地址:htt...

汇编学习第五课之函数参数传递,函数返回值

最近在看《C++反汇编和逆向分析》这本书,还是收获不少. 自己也做实验来验证下心里的想法.函数参数是通过栈结构进行传递,在C++代码中,默认函数调用约定下(有三种函数调用约定,区别在于函数参数入栈顺...

函数返回值

前言:         对于函数,供别人调用,或自己内部调用,返回值可以传递很多信息,但返回值亦应该尽量简单明了的表达函数执行的结果或状态。 1. 定义通用的返回值     每个编写的函数,如果...
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)