1 函数指针概念
2 函数指针应用
2.1 编译该源码并加入调试信息
2.2 执行编译生成的文件,查看效果
2.3 利用GDB开始调试
2.3.1 设定反汇编的格式
2.3.2 反汇编主函数main
2.3.3 打印fun_a,fun_b的地址
2.3.4 查看每个函数所占内存及内容
2.3.5 反汇编fun_a,fun_b
2.3.6 查看0x80485c4和0x80485d4处的内容
3 附录GDB调试命令简介
3.1 使用examine命令(简写x)来查看内存地址中的值
3.2 GDB输出格式:
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的地址,才能具体分析其实现过程。