函數指針的含義就是一個指向函數的指針,它本質上就是一個地址。在IA32上,它就是一個int型指針。
下面是最簡單的兩個對比的例子:
int* fun_a(); int* (*fun_b)();
第一個fun_a就是一個函數名,其函數返回值是 int*;第二個fun_b則是一個函數指針,它指向一個函數,這個函數的參數為空,返回值為一個整型指針。
函數指針的一個好處就是可以將實現一系列功能的模塊統一起來標識,這可以使得結構更加清晰,便於後期維護。下面是一個具體的例子。
#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; }
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: 警告: 從不兼容的指針類型賦值 [默認啟用]
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
由於linux下使用AT&T格式匯編,相對於INTEL格式匯編有些晦澀,所以在反匯編之前先將匯編格式設置為INTEL格式,方便分析。
(gdb) set disassembly-flavor intel
對主函數進行反匯編,可以熟悉整個函數執行的流程,從而更具體的針對某個子函數進行分析。下面每一行我都加上了具體解釋。
(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.
只有找到funa,funb,func的地址,才能具體分析其實現過程。
(gdb) print fp[0] $1 = (void (*)()) 0x8048481 <fun_a> (gdb) print fp[1] $2 = (void (*)()) 0x804849a <fun_b>