C语言中的函数与函数指针——汇编角度剖析


https://zhuanlan.zhihu.com/p/22437704


C语言中的函数与函数指针

C语言中的函数与函数指针

王小军 王小军
1 年前

今天借着函数与函数指针和大家体会一下指针的灵活。

1. 函数是什么?

先看示例:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

void print_something(void)
{
	printf("hello\n\r");
	return;
}

int hello(int a)
{
	printf("hello %d \n\r");
}

int main(void)
{
	print_something();
	hello(1);
	return 0;
}

这一段代码编译出汇编,核心部分如下:

                  print_something:
0000000000400526:   push %rbp
0000000000400527:   mov %rsp,%rbp
000000000040052a:   mov $0x400604,%edi
000000000040052f:   mov $0x0,%eax
0000000000400534:   callq 0x400400 <printf@plt>
0000000000400539:   nop 
000000000040053a:   pop %rbp
000000000040053b:   retq 
                  hello:
000000000040053c:   push %rbp
000000000040053d:   mov %rsp,%rbp
0000000000400540:   sub $0x10,%rsp
0000000000400544:   mov %edi,-0x4(%rbp)
0000000000400547:   mov -0x4(%rbp),%eax
000000000040054a:   mov %eax,%esi
000000000040054c:   mov $0x40060c,%edi
0000000000400551:   mov $0x0,%eax
0000000000400556:   callq 0x400400 <printf@plt>
000000000040055b:   mov -0x4(%rbp),%eax
000000000040055e:   leaveq 
000000000040055f:   retq 
                  main:
0000000000400560:   push %rbp
0000000000400561:   mov %rsp,%rbp
0000000000400564:   callq 0x400526 <print_something>
0000000000400569:   mov $0x1,%edi
000000000040056e:   callq 0x40053c <hello>
0000000000400573:   mov $0x0,%eax

从以上可以看到,在main函数中

callq 0x400526 <print_something>
callq 0x40053c <hello>

用于调用函数print_something和hello ,其使用callq指令+函数首地址来定位函数。

我们看到hello和print_something函数在首地址都压入堆栈,在最后都调用retq返回。

因此,我们可以得到这个结论:

只要知道函数的首地址就可以定位这个函数了

本例中,我们只要知道首地址0x0000000000400526就定位了函数void print_something(void) ;知道首地址0x000000000040053c就定位了函数int hello(int a);

函数首地址的位数由编译器及运行平台决定,但是其大小一定等于同样编译器和运行平台的int位数。

!!! 更新:

有知友对函数指针的大小提出质疑,查阅资料之后发现我的认识确实是错误的。

C99 spec
section 6.2.5
A pointer to void shall have the same representation and alignment requirements as a pointer to a character type. Similarly, pointers to qualified or unqualified versions of compatible types shall have the same representation and alignment requirements. All pointers to structure types shall have the same representation and alignment requirements as each other. All pointers to union types shall have the same representation and alignment requirements as each other. Pointers to other types need not have the same representation or alignment requirements.
section 6.3.2.3
A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer.
第一段就是说,void*和所有指向各种数据的指针都和char *一致,包括大小,对齐方式。第二段就是说,所有指向函数的指针都一致。

但是标准并没有说函数指针的大小!!!


所以函数指针的大小和编译器的实现以及对应的平台有关,现代的编译器中有定义intptr_t函数指针类型,看具体实现是long int型,也就是说函数指针不比int型所占用的字节数小。

2. 获取函数地址

因为函数地址就是bit数等于int的数据而已,所以有多种方法可以取得函数地址

1. 指向int 的指针(int *)

int * p_function = (int *)print_something;

其语句完整的写法应该为:

int * p_function  = (int *) &print_something;

编译器处理时遇到函数的名字会自动取其地址,所以&取地址符可以省略。

2. int

int function_address = (int)print_something;

注意:这个编译器会有警告,可以运行,但是不推荐使用

3. void *(万能指针)

void * p = print_something;

print_something省略了取地址符,这个是获取一个函数的首地址,并把它赋值给万能指针。

4. 常量

0x0000000000400526就可以表示print_something函数

0x000000000040053c就可以表示hello函数

3. 如何根据函数的地址访问函数

相应的,我王小军有一百种方式访问函数(大雾

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

/** @note 定义类型函数指针,方便指针转换 */
typedef void ( * void_function_type)(void); //print_something类型的函数

void print_something(void)
{
	printf("hello\n\r");
	return;
}

int main(void)
{
    // int *
    int * p_function = (int *)print_something;
    ((void_function_type)p_function)(); //int *强制转换成函数指针,进而访问函数
    
    //int
    int function_address = (int)print_something;
    ((void_function_type)function_address)(); //int变量存储了函数的首地址,强制转换成函数指针进行访问,注意这个方法有警告

    //void *
    void * p = print_something;
    ((void_function_type)p)();  //万能指针转化为函数指针,进行函数访问

    //constant
    ((void_function_type)0x0000000000400526)(); //直接利用函数的首地址常量进行函数访问
}

从以上三部分内容我们可以知道,其实在C语言中,函数名就是表示函数的首地址,函数指针就是指向函数首地址的指针。

更新:其中利用常量进行函数访问时,在《C缺陷和指针》中有这样一个例子:

为了计算机启动时,硬件首先调用首地址位0位置的子例程,采用如下语句
(* (void (*) ()) 0 )();

4. 函数指针作为回调函数

函数指针用处很多,但是本文只介绍使用最多的用法,回调函数,详细见示例。

test.h

#ifndef TEST_H_
#define TEST_H_

#include <stdint.h>

typedef enum
{
	TYPE_X,
	TYPE_Y,
	TYPE_Z
} type_t;

typedef uint32_t data_t;

//data_struct
typedef struct
{
	type_t type;
	data_t data;
}data_struct_t;

//callback_function type
typedef void (* callback_function_t)(data_struct_t *data_struct);

//init function
void test_init(callback_function_t callback_function);

//update_function
void test_update(void);

#endif /* TEST_H_ */

其中

typedef void (* callback_function_t)(data_struct_t *data_struct);

定义了我们需要的回调函数类型,外部传递的回调函数,要按照这个函数指针的格式定义

test.c

#include "test.h"

static callback_function_t _callback_function;
static uint32_t count_num = 0;

void test_init(callback_function_t callback_function)
{
	_callback_function = callback_function;
}

void test_update(void)
{
	count_num++;
	if(count_num % 2 == 0)
	{
		data_struct_t my_data;
		my_data.type = TYPE_X;
		my_data.data = count_num;

		_callback_function(&my_data);
	}

	if(count_num % 3 == 0)
	{
		data_struct_t my_data;
		my_data.type = TYPE_Y;
		my_data.data = count_num;

		_callback_function(&my_data);
	}

	if(count_num % 5 == 0)
	{
		data_struct_t my_data;
		my_data.type = TYPE_Z;
		my_data.data = count_num;

		_callback_function(&my_data);
	}
}

在内部实现时,当检测到合适的情况通知外部回调函数

main.c

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#include "test.h"

void my_callback(data_struct_t *data_struct)
{
	printf("type: %d \t data: %d \n\r", data_struct->type, data_struct->data);
}

int main(void)
{
	test_init(my_callback);

	for(int i = 0; i < 100; i++)
	{
		test_update();
	}
}

在main.c中可以看到,我们按照格式定义了回调函数,并传给test相关文件,当test运行时,相应情况出现就会通知main中的回调函数。

5. 经知友指教,更新关于函数指针和void *转换的内容

知友@冒泡指教,感觉自己的认识还是有很多误区的。

查阅ISO/ANSI标准

ANSI 6.3.2.2 Pointers

1. A pointer to void may be converted to or from a pointer to any incomplete or object type. A pointer to any incomplete or object type may be converted to a pointer to void and back again; the result shall compare equal to the original pointer.
8. A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the pointed-to type, the behavior is undefined.

第1点指明了void *指针可以和不完全类型以及对象类型(非面向对象的对象)相互转换,第8点说明了函数指针之间可以相互转换,但是标准并没有定义void * 和函数指针之间如何转换。

为了避免这种未定义行为,建议采用如下方式操作:

void *  p = &print_something; //获取函数地址
void_function_type p_function;
*(void**)&p_function = p;
p_function();

6. 关于函数指针和void *指针转换的再次更新

最近在看《C专家编程》有函数指针和void *直接转换的用法(P189),例子稍微变化一下如下:

extern int print_function(const char * ch);
void * f = (void *)print_function;
(*(int (*)(const char *))f)("hello\n\r");

关于函数指针和void *的转换总结如下:

  • C99标准没有直接定义函数指针和void *的强制类型转换
  • 第4点和第6点中关于函数指针和void *强制转换的使用中gcc没有警告

这个如何使用我也比较迷茫了,可能按照第5点知友的使用比较合适了,如果有哪位大牛知道感谢告知了。

---

以上~ ^ ^

「大爷,小妞给你笑一个~」
还没有人赞赏,快来当第一个赞赏的人吧!
聿舟
chen
啦斯菲
pumpkin
李征
9 条评论
liting
写下你的评论...

冒泡
冒泡回复王小军(作者)
这个倒不是说不能转,而是说有两点:
1 function到void*或其他指针转换,允许编译器在转换过程中对function做修改,或者调整指针位置之类的,比如说,你的编译器在debug模式下对function编译,可能加入很多额外信息,这样在转void*的时候,可能返回的并不一定是function的代码入口,如果function地址是0x1234,有可能转换后的void*指向的是0x1200,当然再反过去转成这个function,就又变回去了,这是规定了你的转换一定要原路逆着回去,最好不要(function)(int *)(void *)func这种三角转换,更不要做把一个void (*)(int)转void*后,再转int (*)(double)这种trick
2 void*或其他指针转function指针,行为未定义,所以linux的dlsym明确建议用下面的方式,免得从动态库弄出来的符号地址被编译器做转换导致非法:
PFunc pf;
*(void **)&pf = dlsym(...);
而不是
pf = (PFunc)dlsym(...);
1 年前
以上为精选评论
妓院饺子
可以啊
1 年前
冒泡
其实c99对函数指针和void*的关系真有个不同于其它指针的规定
1 年前
王小军
王小军(作者)回复冒泡
c99没有直接声明函数指针能否直转换成void *,或者void *直接转换成函数指针,但是常见编译器都实现了此特性,函数指针本质上只是指向函数首地址的指针,如果有编译器不支持我们可以利用int *作为中介,例如typedef void (*void_function_type)(void);
void hello(void)
{
}
int main(void)
{
void * p = (int *) hello;
((void_function)((int *)p)();
}
这样就可以啦
1 年前
王小军
王小军(作者)回复冒泡
谢谢指教了,你在第二点提供的方法确实能避免void *直接转换为function。
1 年前
Pluto Hades
函数首地址的位数由编译器及运行平台决定,但是其大小*不*一定等于同样编译器和运行平台的int位数。
1 年前
王小军
王小军(作者)回复Pluto Hades
我不太懂为什么,能指教一下吗?
1 年前
Xi Yang

int不能保证装得下指针,至少你也应当用intptr_t。

1 年前
王小军
王小军(作者)回复Xi Yang
我查了一下,你说的对,intptr_t就是long int,代表不比int占用字节数小,和pointer占用字节数相同,我回头修改一下文章内容,谢谢了~
1 年前





智能推荐

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
© 2014-2019 ITdaan.com 粤ICP备14056181号  

赞助商广告