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章。