8.8 函数指针
第6章介绍了函数的调用过程,通过call指令跳转到函数首地址处,执行函数内的指令代码。既然是地址,当然就可以使用指针变量进行存储。用于保存函数首地址的指针变量被称为函数指针。
函数指针的定义很简单,和函数的定义非常相似,由四部分组成:
返回值类型([调用约定,可选]*函数指针变量名称)(参数信息)
函数指针的类型由返回值、参数信息、调用约定组成,它们决定了函数指针在函数调用过程中参数的传递、返回值信息,以及如何平衡栈顶。在没有特殊说明的情况下,调用约定与VC编译器中设置相同。如何区分函数调用与函数指针的调用呢?见代码清单8-16。
代码清单8-16 函数指针与函数—Debug版
//C++源码说明:函数指针与函数对比
void__cdecl Show(){//函数定义
printf("Show\r\n");
}
void main(){
void(__cdecl*pShow)(void)=Show;//函数指针赋值
pShow();//使用函数指针调用函数
Show();//直接调用函数
}
//C++源码与对应汇编代码讲解
void main(){
void(__cdecl*pShow)(void)=Show;
;函数名称即为函数首地址,这是一个常量地址值
0040B90E mov dword ptr[ebp-38h],offset@ILT+15(Show)(00401014)
0040B915 mov edx, dword ptr[ebp-38h]
0040B918 mov dword ptr[ebp-38h],edx
pShow();
0040B91B mov esi, esp
0040B91D call dword ptr[ebp-38h];间接调用函数
0040B920 cmp esi, esp;栈平衡检查,Debug下特有
0040B922 call__chkesp(004012d0);栈平衡检查,Debug下特有
Show();
0040B927 call@ILT+15(Show)(00401014);直接调用函数
}
代码清单8-16演示了函数指针的赋值和调用过程。与函数调用的最大区别在于函数是直接调用,而函数指针的调用需要取出指针变量中保存的地址数据,间接调用函数。
函数指针是比较特殊的指针类型,由于其保存的地址数据为代码段内的地址信息,而非数据区,因此不存在地址偏移的情况。指针的操作非常灵活。为了防止函数指针发生错误的地址偏移,VC编译器在编译期间对其进行检查,不允许对函数指针类型变量执行加法和减法等没有意义的运算。
在代码清单8-16中,函数指针类型的参数和返回值都为void类型,只可存储相同类型的函数地址,否则无法传递函数的参数、返回值,无法正确平衡栈顶。通过修改代码清单8-16,分析带参数与返回信息的函数指针类型,见代码清单8-17。
代码清单8-17 带参数与返回值的函数指针—Debug版
//C++源码说明:带参数与返回类型的函数指针
int__stdcall Show(int nShow){//函数定义
printf("Show:%d\r\n",nShow);
return nShow;
}
void main(){
int(__stdcall*pShow)(int)=Show;//函数指针定义并初始化
int nRet=pShow(5);//使用函数指针调用函数,并获取返回值
printf("ret=%d\r\n",nRet);
}
//C++源码与对应汇编代码讲解
void main(){
int(__stdcall*pShow)(int)=Show;
;初始化过程没有变化,仍然为获取函数首地址并保存
0040B868 mov dword ptr[ebp-4],offset@ILT+20(Show)(00401019)
0040B86F mov eax, dword ptr[ebp-4]
0040B872 mov dword ptr[ebp-4],eax
int nRet=pShow(5);
0040B875 mov esi, esp;保存进入函数前的栈顶,用于栈顶检查
0040B877 push 5;压入参数5
0040B879 call dword ptr[ebp-4];获取函数指针中的地址,间接调用函数
0040B87C cmp esi, esp;栈顶检查
0040B87E call__chkesp(004012d0);栈平衡检查
0040B883 mov dword ptr[ebp-8],eax;接收函数返回值数据
printf("ret=%d\r\n",nRet);
}
代码清单8-17中的函数指针调用只是多了参数的传递、返回值的接收,和代码清单8-16中的函数指针并无实质区别。它们有着共同特征—都是间接调用函数,这是识别函数指针的关键点。