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

Leave a Reply

Your email address will not be published. Required fields are marked *