6.5 函数的返回值

函数调用结束后,ret指令执行后为什么可以返回到函数调用处的下一条指令呢?call指令被执行后,该指令同时还会做另一件事,那就是将下一条指令所在的地址压入栈中。图6-4为函数调用前esp与栈中内存数据的信息。

图 6-4 调用函数前esp与栈中的信息

call指令的下一句指令所在地址为0x0040DB39,当前esp保存的地址为0x0012FF2C。当执行call指令时,再次进入函数实现中观察esp与栈数据的变化,发现esp被减4,并且其对应地址中的数据被修改,如图6-5所示。

图 6-5 执行call指令后esp与栈中内存数据的信息

在图6-5中,执行call指令后,由于有压栈操作,esp被减4,修改为0x0012FF28,并且该地址中保存的信息为0x0040DB39。对比图6-4,该地址即为函数的返回地址。当函数执行到ret指令时,当前esp已经被平衡,此时将再次指向0x0012FF28。函数退出前,会执行ret指令,这个指令取得esp所指向的4字节内容作为函数的返回地址值更新eip,程序的流程回到返回地址处,同时执行esp加4的操作,以释放返回的地址空间,平衡栈顶。

前面分析了call和ret指令的细节,介绍了栈结构中函数的运行机制。那么函数的返回值是如何得到的呢?VC中使用寄存器eax来保存返回值,由于32位的eax寄存器只能保存4字节数据,因此大于4字节的数据将使用其他方法保存。通常,eax作为返回值,只有基本数据类型与sizeof(type)小于等于4的自定义类型(浮点类型除外(详见2.2.1节))。在Debug版下,如果函数有返回值,那么最后的操作通常为对eax赋值后执行ret指令,如代码清单6-8所示。

代码清单6-8 函数返回值—Debug版


//C++源码说明

//函数功能:获取当前函数的返回地址

int GetAddr(int nNumber){

//获取参数地址,减1后得到返回地址在栈中的地址

int nAddr=*(int*)(&nNumber-1);

return nAddr;//将返回地址作为返回值返回

}

//C++源码与对应汇编代码讲解

int GetAddr(int nNumber){

;Debug保护环境初始化部分略

int nAddr=*(int*)(&nNumber-1);

;ebp加法与esp加法原理相同,都是取参数,但是这里为什么是加8呢?

;在Debug版下进入函数后,首先保存ebp会执行push ebp的操作,这样esp将执行压栈减4操作,

;随后执行mov ebp, esp的操作,由于栈顶esp之前被修改,所以ebp需要加4调整到最初的栈底位置

;因此ebp+4可以得到返回地址,ebp+8将会寻址第一个参数

;以下代码将第一个参数的地址传入eax中

0040DB78 lea eax,[ebp+8]

;执行eax自减4操作,执行后eax等价于ebp+4,得到函数返回地址所在栈中的地址

0040DB7B sub eax,4

;取出函数返回地址传入ecx中

0040DB7E mov ecx, dword ptr[eax]

;使用ecx赋值局部变量

0040DB80 mov dword ptr[ebp-4],ecx

return nAddr;

;取出局部变量数据传入eax中,用做函数返回值

0040DB83 mov eax, dword ptr[ebp-4]

}

;Debug恢复环境,平衡栈、栈平衡检测部分略

0040DB8C ret

//函数调用处

int nAddr=GetAddr(1);

0040DAF8 push 1;压栈传参,传入参数1

0040DAFA call@ILT+30(ss)(00401023);函数调用

0040DAFF add esp,4;__cdecl调用方式,平衡栈

0040DB02 mov dword ptr[ebp-4],eax;取得返回值


代码清单6-8利用函数的特性,通过对参数地址的间接访问得到函数返回地址,最后通过eax寄存器将其返回。接下来再分析一个不寻常的示例,返回值类型为自定义类型—结构体,其大小超过4字节,编译器会如何处理呢?欲知真相,请阅读代码清单6-9。

代码清单6-9 结构体类型作为返回值


//C++源码说明:结构体类型作为返回值

struct tagTEST{//结构体定义

int m_nOne;//两个整型成员变量

int m_nTwo;

};

//返回值为结构体类型的函数

tagTEST RetStruct(){

tagTEST testRet;

testRet.m_nOne=1;

testRet.m_nTwo=2;

return testRet;

}

//调用函数,并将返回值赋值到结构体实例test中

void main(){

tagTEST test;

test=RetStruct();

//C++源码与对应汇编代码讲解

tagTEST RetStruct(){

;Debug保存环境、初始化部分略

tagTEST testRet;

testRet.m_nOne=1;

004012A8 mov dword ptr[ebp-8],1;对结构体成员变量赋值

testRet.m_nTwo=2;

004012AF mov dword ptr[ebp-4],2;对结构体成员变量赋值

return testRet;

004012B6 mov eax, dword ptr[ebp-8];取结构体成员变量数据传入eax中

004012B9 mov edx, dword ptr[ebp-4];取结构体成员变量数据传入edx中

}

;Debug恢复环境略

004012C2 ret;执行ret指令结束函数调用

//函数调用处

tagTEST test;

test=RetStruct();

0040DC38 call@ILT+35(RetStruct)(00401028);调用函数RetStruct

;eax中保存函数RetStruct中结构体testRet成员m_nOne的数据

0040DC3D mov dword ptr[ebp-10h],eax;ebp-10h为临时变量

;edx中保存函数RetStruct中结构体testRet成员m_nTwo的数据

0040DC40 mov dword ptr[ebp-0Ch],edx;ebp-0Ch为临时变量

;经过几次数据传递,最终将返回结果存入结构体实例test的两个成员所在地址处

0040DC43 mov eax, dword ptr[ebp-10h]

0040DC46 mov dword ptr[ebp-8],eax

0040DC49 mov ecx, dword ptr[ebp-0Ch]

0040DC4C mov dword ptr[ebp-4],ecx


代码清单6-9演示了一个返回类型为结构体,并且其大小大于4字节的返回流程。由于只有两个成员,因此编译器使用了eax和edx来传递返回值。本节的重点是讲解函数的识别,此处的讲解只是为了让读者了解函数对返回值的处理。更多关于结构体知识的讲解见第9章。