函数指针解析(C语言)

函数指针解析(C语言)


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 按浮点数格式显示变量。



Date: 2012-07-27

Author: lishuo

Org version 7.9.1 with Emacs version 23

Validate XHTML 1.0
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值