什么是函数指针?
函数是计算机程序中一段可执行代码的封装,当程序运行时函数会被加载到内存布局中的代码段位置,这段代码会有一段内存空间,有内存空间就会有地址,这段内存空间的首地址,就是函数的地址
每当我们调用这些函数时,都会触发call指令,call指令的地址就是这个函数的首地址
函数指针与指针的区别
函数指针与指针一样,只不过一个是指向变量,一个是指向函数
指针指向的是变量的首地址,而函数指针指向的是函数的首地址
函数指针的定义
定义方法跟指针一样,在变量名周围加上()就可以了,然后要在前面加上()参数
函数指针名不需要与函数名一致
//函数
int func(int a,int b){
}
//函数指针
int(*p)(int, int);
使用方法:
在c语言里,传递函数名编译器在编译阶段自动替换成已经生成的文件偏移地址,也就是所谓的内存地址,文件偏移地址是没有被加载到内存中的一种称法
//函数
int func(int a,int b){
}
//函数指针
int(*p)(int, int) = NULL;
//赋予函数地址
p = func;
或者也可以初始化时直接赋予值:
//函数
int func(int a,int b){
}
//函数指针
int(*p)(int, int) = func;
获取函数首地址的方法:
通过函数指针的方式或者直接打印函数地址都可以
#include <stdio.h>
int func() {
return 0;
}
int main()
{
int(*ptr_func)() = func;
func();
printf("%p\n%p", ptr_func, func);
}
函数指针的转换方式运行代码:
我们可以通过一些类型转换,转换成函数指针的方式然后让c语言最后帮我们编译成call指令,执行某段地址的代码
但是这样的方法一般用在hook下和单片机上比较多
因为hook注入目标程序后,知道自己某段代码的地址后(如shellcode),使用这个方法可以直接执行
单片机里一般有许多地址空间里包含了一些我们预先写好的代码,如0x14地址下有一段启动开机画面的代码,我们可以使用这个方法来执行它
以下代码来自c语言的陷阱与缺陷,代码写法:
( *(void(*)())0 )();
函数指针的声明方式是:
void (*func)(void)
函数指针也是一个类型,所以我们可以利用c语言编译器自带的类型转换方式把一个地址或者变量转换成函数指针的类型,这样c语言会依据函数指针的方式去对这块地址进行解引用
指针的解引用是取值,而函数指针的解引用是call函数首地址
类型转换的方法:
void(*)(参数)地址
这样就可以把一个类型转换成函数指针了,但是这样还不符合c语言的调用规则,因为在ANSI C里 c语言允许用户在编写时无需在前面加上*,c语言会帮我们做好这些工作
如:
void (*func)(void);
func();
其实真正的写法是:
void (*func)(void);
*func();
这一点是我在c的缺陷与陷阱里看到的,但是我自己在vs2019的编译器还有gnc c的编译器上发现加上*会出错误提示,而c的缺陷与陷阱这本书出自90年代也比较早了,所以推测是较新的编译器已经无需对函数指针进行解引用操作了,早期的编译器里,可以加上*或者不加,但是较新的编译器加上解引用运算符就会出错,可能是历史已经遗弃这种写法了,但是编译器内部还是会给你加上解引用运算符的,只是对于我们来说不需要在使用解引用运算符了。
所以仅仅转换成函数指针类型是无法让c语言编译器以函数指针的方式去给我们call,因为编译器认为这是一次类型转换,并没有发生调用
所以我们需要在前面加上*
*void(*)()0
这样还是无法调用的,因为编译器认为我们只是转换成函数指针并对其进行了解引用,对这块地址以函数指针的方式访问,但是并没有发生函数调用,我们需要在最右侧加上(),括号,代表函数调用
*void(*)()0()
最右侧的括号里可以加上你要传入的参数
你会发现这样还是无法运行,因为这里就设计到一个最基础的知识点
运算符的优先级
我们需要注意,()优先级比较高
以上的代码里c语言会从左到右依次解析运算符
*void(*) 首先会将void(*)优先看成一个类型,这属于c语言编译器解析运算符时的一个贪心法
贪心法就是从左到右依次读取一个运算符,看看运算符是否符合一个表达式,如果优先级较高的情况下,会优先读取优先级较高的运算符,把他们看成一个表达式
void(*),不属于函数指针类型,这是一个c语言没有的类型,所以我们要用括号括起来
*(void(*)())0()
就变成了这样,把void(*)()用括号括起来,这样c语言解析括号里时,会把括号里的运算符看成一个整体,这样就成了函数指针类型
接下来c语言会将0这个地址转换成函数指针后去把()看成一个优先级的表达式,因为我们没有在c语言处理最右侧的()表达式之前对这块地址进行解引用,c不符合函数指针的调用规则,所以我们要在加一个括号:
(*(void(*)())0)();
这样就把一个函数指针的类型转换看成了一个整体。
这种写法的好处在于,你可以不需要知道对应函数的一个参数是什么,或者原型是什么,可以直接调用
#include <stdio.h>
int func(int a) {
printf("1");
return 0;
}
int main()
{
(*(int(*)())func)();
}
如果用函数指针的方式的话,需要将函数指针声明与函数一样的原型才可以,这是规范
我们可以通过一些稀奇古怪的写法来越过规范,当然这考验你对编译器的理解
TYPE函数指针
我们可以通过type函数指针来定义一个函数指针的类型
typedef int(*FunType)(int);
这样下去有相同函数原型可以直接使用这个类型来定义
typedef int(*FunType)(int); /* ②. 定义一个函数指针类型FunType,与①函数类型一致 */
int func(int a) {
printf("1");
return 0;
}
int main()
{
FunType fun;
fun = func;
}
函数指针作为返回值
这个函数写法来自Linux下的一个消息注册函数:
void (*signal(int sig, void (*func)(int)))(int)
这里是声明了一个signal函数,有两个参数,其返回原型是函数指针。这个函数指针只有一个参数
网络上有一个对其实例的例子:
#include <stdio.h>
enum { RED, GREEN, BLUE };
void OutputSignal(int sig)
{
printf("The signal you /'ve input is: ");
switch(sig)
{
case RED:
puts("RED!");
break;
case GREEN:
puts("GREEN!");
break;
case BLUE:
puts("BLUE!");
break;
}
}
void ( *signal( int sig, void (*func)(int) ) ) (int)
{
puts("Hello, world!");
func(sig);
return func;
}
int main(void)
{
(*signal(GREEN, &OutputSignal))(RED);
return 0;
}
输出:
Output:
Hello, world!
The signal you 've input is: GREEN!
The signal you 've input is: RED!
其实可以看到很容易理解,在调用signal函数时,先对其函数进行解引用,这是c语言函数指针的调用规则,这种情况下是一定要加括号的,上面说过优先级的原因,不然不会优先解引用
(*signal(GREEN, &OutputSignal))(RED);
这里加括号是为了函数执行完成后,返回的函数指针类型,然后对这个类型进行解引用然后在调用一次
我们会发现我们明明调用了一次函数,但是缺产生了两次调用
我们仔细看一下这段代码:
void ( *signal( int sig, void (*func)(int) ) ) (int)
{
puts("Hello, world!");
func(sig);
return func;
}
函数是执行了,但是最后返回的是函数指针:func,而func指向OutputSignal函数,所以函数执行完成后,返回了func函数指针,然后在外部:
(*signal(GREEN, &OutputSignal))(RED);
执行完成后,又去调用了一次函数指针
如果我们不想调用函数指针可以这样写:
signal(GREEN, &OutputSignal)
按正常方式调用就可以了。
如果我们想在返回的同时去执行函数指针,就可以用上面的写法:
(*signal(GREEN, &OutputSignal))(RED);
当然也可以这样:
void (*pfunc)(int) = signal(GREEN, &OutputSignal);
pfunc(GREEN);
我们可以把这段代码拆开:
第一次调用
(*signal(GREEN, &OutputSignal))(RED);
返回后signal就变成了:
*(void (*)(int))func(RED);
然后又执行了一遍。