函数指针解析(C语言)
Table of Contents
1 函数指针概念
函数指针的含义就是一个指向函数的指针,它本质上就是一个地址。在IA32上,它就是一个int型指针。
下面是最简单的两个对比的例子:
int* fun_a(); int* (*fun_b)();
第一个fun_a就是一个函数名,其函数返回值是 int*;第二个fun_b则是一个函数指针,它指向一个函数,这个函数的参数为空,返回值为一个整型指针。
2 函数指针应用
函数指针的一个好处就是可以将实现一系列功能的模块统一起来标识,这可以使得结构更加清晰,便于后期维护。下面是一个具体的例子。
#include <stdio.h> int fun_a(void); int fun_b(void); int fun_c(void); int fun_d(void); int main(void) { int i ; void (*fp[4])(); /* 定义一个函数数组指针 */ fp[0] = (int*)fun_a; /* 将函数的地址赋给定义好的指针,同时强制转化为int* */ fp[1] = (int*)fun_b; fp[2] = (int*)fun_c; fp[3] = (int*)fun_d; for(i=0;i<4;i++) { fp[i](); printf("%p\n",fp[i]); } return 0; } int fun_a(void) { printf("This is fun_a !\n"); return 0; } int fun_b(void) { printf("This is fun_b !\n"); return 0; } int fun_c(void) { printf("This is fun_c !\n"); return 0; } int fun_d(void) { printf("This is fun_d !\n"); return 0; }
2.1 编译该源码并加入调试信息
lishuo@lishuo-Rev-1-0:~/audio$ gcc -g -o test test.c test.c: 在函数‘main’中: test.c:12:9: 警告: 从不兼容的指针类型赋值 [默认启用] test.c:13:9: 警告: 从不兼容的指针类型赋值 [默认启用] test.c:14:9: 警告: 从不兼容的指针类型赋值 [默认启用] test.c:15:9: 警告: 从不兼容的指针类型赋值 [默认启用]
2.2 执行编译生成的文件,查看效果
lishuo@lishuo-Rev-1-0:~/audio$ ./test This is fun_a ! 0x8048481 This is fun_b ! 0x804849a This is fun_c ! 0x80484b3 This is fun_d ! 0x80484cc
2.3 利用GDB开始调试
2.3.1 设定反汇编的格式
由于linux下使用AT&T格式汇编,相对于INTEL格式汇编有些晦涩,所以在反汇编之前先将汇编格式设置为INTEL格式,方便分析。
(gdb) set disassembly-flavor intel
2.3.2 反汇编主函数main
对主函数进行反汇编,可以熟悉整个函数执行的流程,从而更具体的针对某个子函数进行分析。下面每一行我都加上了具体解释。
(gdb) disassemble main Dump of assembler code for function main: 0x08048414 <+0>: push ebp 0x08048415 <+1>: mov ebp,esp ;将esp保存到ebp中,防止在函数执行过程中破坏esp。 0x08048417 <+3>: and esp,0xfffffff0 0x0804841a <+6>: sub esp,0x30 ;上面两句的目的是,开辟一块48字节大小的栈区,用于保存函数运行过程中的数据和地址 0x0804841d <+9>: mov eax,0x8048481 ;此为fun_a的地址,后面会详细讲到 0x08048422 <+14>: mov DWORD PTR [esp+0x1c],eax ;将fun_a的地址压栈到esp+0x1c处,方便调用该函数的时候取出。 0x08048426 <+18>: mov eax,0x804849a 0x0804842b <+23>: mov DWORD PTR [esp+0x20],eax 0x0804842f <+27>: mov eax,0x80484b3 0x08048434 <+32>: mov DWORD PTR [esp+0x24],eax 0x08048438 <+36>: mov eax,0x80484cc 0x0804843d <+41>: mov DWORD PTR [esp+0x28],eax ;这两个也是一个道理,将fun_b,fun_c的地址压栈,方便调用。 => 0x08048441 <+45>: mov DWORD PTR [esp+0x2c],0x0 ;将0压入esp+0x2c处,实际此处保存的是i变量的值。 0x08048449 <+53>: jmp 0x8048473 <main+95> ;开始for循环,它跳转到cmp指令处,实际就是比较i和4的大小关系,从而决定函数流程 0x0804844b <+55>: mov eax,DWORD PTR [esp+0x2c] ;将i的值赋给eax寄存器 0x0804844f <+59>: mov eax,DWORD PTR [esp+eax*4+0x1c];此处实际是将fp[i]的值赋给eax 0x08048453 <+63>: call eax ;调用fp[i],也就是依次调用fun_a,fun_b,fun_c 0x08048455 <+65>: mov eax,DWORD PTR [esp+0x2c] 0x08048459 <+69>: mov edx,DWORD PTR [esp+eax*4+0x1c];将fp[i]的值赋给edx 0x0804845d <+73>: mov eax,0x80485c0 0x08048462 <+78>: mov DWORD PTR [esp+0x4],edx ;将edx值压栈到esp+0x4处 0x08048466 <+82>: mov DWORD PTR [esp],eax ;将地址0x80485c0压入esp处,此地址为存储字符串的位置,例如“This is fun_a !”。 0x08048469 <+85>: call 0x8048320 <printf@plt> ;它是printf函数调用必须的参数 0x0804846e <+90>: add DWORD PTR [esp+0x2c],0x1 ;i++ 0x08048473 <+95>: cmp DWORD PTR [esp+0x2c],0x3 ;比较i和3的大小 0x08048478 <+100>: jle 0x804844b <main+55> ;如果i <= 3,那么跳转到main+55处;否则结束for循环。 0x0804847a <+102>: mov eax,0x0 ;通常情况下eax保存返回值。这里其实就是return 0 ;。 0x0804847f <+107>: leave ;将ebp弹栈,同时恢复esp原有值。 0x08048480 <+108>: ret End of assembler dump.
2.3.3 打印fun_a,fun_b的地址
只有找到funa,funb,func的地址,才能具体分析其实现过程。
(gdb) print fp[0] $1 = (void (*)()) 0x8048481 <fun_a> (gdb) print fp[1] $2 = (void (*)()) 0x804849a <fun_b>
2.3.4 查看每个函数所占内存及内容
(gdb) x /25xh 0x8048481 0x8048481 <fun_a>: 0x8955 0x83e5 0x18ec 0x04c7 0xc424 0x0485 0xe808 0xfe9d 0x8048491 <fun_a+16>: 0xffff 0x00b8 0x0000 0xc900 0x55c3 0xe589 0xec83 0xc718 0x80484a1 <fun_b+7>: 0x2404 0x85d4 0x0804 0x84e8 0xfffe 0xb8ff 0x0000 0x0000 0x80484b1 <fun_b+23>: 0xc3c9 (gdb) x /25xh 0x804849a 0x804849a <fun_b>: 0x8955 0x83e5 0x18ec 0x04c7 0xd424 0x0485 0xe808 0xfe84 0x80484aa <fun_b+16>: 0xffff 0x00b8 0x0000 0xc900 0x55c3 0xe589 0xec83 0xc718 0x80484ba <fun_c+7>: 0x2404 0x85e4 0x0804 0x6be8 0xfffe 0xb8ff 0x0000 0x0000 0x80484ca <fun_c+23>: 0xc3c9
从上面可以看到,每个函数占用25个字节的内存空间,它们很多内容都是一样的(因为每个函数的实现功能基本一致,而且函数一般都是开始压栈保护结尾弹栈返回)。
2.3.5 反汇编fun_a,fun_b
虽然汇编晦涩难懂的缺点,但是它可以帮助你深入的理解函数的执行过程。所以即便你并不是非常熟悉汇编,基本的反汇编代码是要读懂的,这非常重要。
(gdb) disassemble 0x8048481 Dump of assembler code for function fun_a: 0x08048481 <+0>: push ebp 0x08048482 <+1>: mov ebp,esp 0x08048484 <+3>: sub esp,0x18 0x08048487 <+6>: mov DWORD PTR [esp],0x80485c4 ;此处的将第一个字符串地址压栈,方便后面函数的调用。 0x0804848e <+13>: call 0x8048330 <puts@plt> 0x08048493 <+18>: mov eax,0x0 0x08048498 <+23>: leave 0x08048499 <+24>: ret End of assembler dump. (gdb) disassemble 0x804849a Dump of assembler code for function fun_b: 0x0804849a <+0>: push ebp 0x0804849b <+1>: mov ebp,esp 0x0804849d <+3>: sub esp,0x18 0x080484a0 <+6>: mov DWORD PTR [esp],0x80485d4 ;此处的将第二个字符串地址压栈,方便后面函数的调用。 0x080484a7 <+13>: call 0x8048330 <puts@plt> 0x080484ac <+18>: mov eax,0x0 0x080484b1 <+23>: leave 0x080484b2 <+24>: ret End of assembler dump.
2.3.6 查看0x80485c4和0x80485d4处的内容
(gdb) x /16cb 0x80485c4 0x80485c4: 84 'T' 104 'h' 105 'i' 115 's' 32 ' ' 105 'i' 115 's' 32 ' ' 0x80485cc: 102 'f' 117 'u' 110 'n' 95 '_' 97 'a' 32 ' ' 33 '!' 0 '\000' (gdb) x /16cb 0x80485d4 0x80485d4: 84 'T' 104 'h' 105 'i' 115 's' 32 ' ' 105 'i' 115 's' 32 ' ' 0x80485dc: 102 'f' 117 'u' 110 'n' 95 '_' 98 'b' 32 ' ' 33 '!' 0 '\000'
由此可知,此处存储printf函数所需的“This is fun_a !”等三个字符串。
由上面的分析过程可知,函数指针实际上就是一个地址而已。其中函数名funa代表函数开始的地址,也就是函数的入口地址。它从入口地址保存一系列将要执行的汇编指令。
3 附录GDB调试命令简介
3.1 使用examine命令(简写x)来查看内存地址中的值
x命令的语法如下所示:
x/<n/f/u> <addr>
n、f、u是可选的参数
n 是一个正整数,表示显示内存的长度,也就是说从当前地址向后显示几个地址的内容。
f 表示显示的格式,参见上面。如果地址所指的是字符串,那么格式可以是s,如果地十是指令地址,那么格式可以是i。
u 表示从当前地址往后请求的字节数,如果不指定的话,GDB默认是4个bytes。u参数可以用下面的字符来代替,b表示单字节,h表示双字节,w表示四字节,g表示八字节。当我们指定了字节长度后,GDB会从指内存定的内存地址开始,读写指定字节,并把其当作一个值取出来。<addr> 表示一个内存地址。
n/f/u 三个参数可以一起使用。
3.2 GDB输出格式:
一般来说,GDB会根据变量的类型输出变量的值。但你也可以自定义GDB的输出的格式。例如,你想输出一个整数的十六进制,或是二进制来查看这个整型变量的中的位的情况。要做到这样,你可以使用GDB的数据显示格式:
x 按十六进制格式显示变量。
d 按十进制格式显示变量。
u 按十六进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
a 按十六进制格式显示变量。
c 按字符格式显示变量。
f 按浮点数格式显示变量。