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中的函数指针并无实质区别。它们有着共同特征—都是间接调用函数,这是识别函数指针的关键点。