函數指針解析(C語言)

* 函數指針概念

函數指針就是一個指向函數的指針,它本質上就是一個地址。在IA32上,它就是一個int型指針。

下面是最簡單的兩個對比的例子:

點擊(此處)摺疊或打開

  1. int* fun_a();
  2. int* (*fun_b)();

第一個fun\_a就是一個函數名,其函數返回值是 int*;第二個fun\_b則是一個函數指針,它指向一個函數,這個函數的參數爲空,返回值爲一個整型指針。


* 函數指針應用


函數指針的一個好處就是可以將實現一系列功能的模塊統一起來標識,這可以使得結構更加清晰,便於後期維護。下面是一個具體的例子。


點擊(此處)摺疊或打開

  1. #include <stdio.h>

  2. int fun_a(void);
  3. int fun_b(void);
  4. int fun_c(void);
  5. int fun_d(void);

  6. int main(void)
  7. {
  8.   int i ;
  9.   void (*fp[4])(); /* 定義一個函數數組指針 */

  10.   fp[0] = (int*)fun_a; /* 將函數的地址賦給定義好的指針,同時強制轉化爲int* */
  11.   fp[1] = (int*)fun_b;
  12.   fp[2] = (int*)fun_c;
  13.   fp[3] = (int*)fun_d;

  14.   for(i=0;i<4;i++)
  15.     {
  16.       fp[i]();
  17.       printf("%p\n",fp[i]);
  18.     }

  19.   return 0;
  20. }

  21. int fun_a(void)
  22. {
  23.   printf("This is fun_a !\n");
  24.   return 0;
  25. }

  26. int fun_b(void)
  27. {
  28.   printf("This is fun_b !\n");
  29.   return 0;
  30. }

  31. int fun_c(void)
  32. {
  33.   printf("This is fun_c !\n");
  34.   return 0;
  35. }

  36. int fun_d(void)
  37. {
  38.   printf("This is fun_d !\n");
  39.   return 0;
  40. }

** 編譯該源碼並加入調試信息

點擊(此處)摺疊或打開

  1. ~/audio$ gcc --o test test.c

** 執行編譯生成的文件,查看效果

點擊(此處)摺疊或打開

  1. lishuo@lishuo-Rev-1-0:~/audio$ ./test
  2. This is fun_a !
  3. 0x8048481
  4. This is fun_b !
  5. 0x804849a
  6. This is fun_c !
  7. 0x80484b3
  8. This is fun_d !
  9. 0x80484cc

** 利用GDB開始調試


*** 設定反彙編的格式

由於linux下使用AT&T格式彙編,相對於INTEL格式彙編有些晦澀,所以在反彙編之前先將彙編格式設置爲INTEL格式,方便分析。

點擊(此處)摺疊或打開

  1. (gdb) set disassembly-flavor intel

*** 反彙編主函數main

對主函數進行反彙編,可以熟悉整個函數執行的流程,從而更具體的針對某個子函數進行分析。下面每一行我都加上了具體解釋。

點擊(此處)摺疊或打開

  1. (gdb) disassemble main
  2. Dump of assembler code for function main:
  3. 0x08048414 <+0>: push ebp
  4. 0x08048415 <+1>: mov ebp,esp ;將esp保存到ebp中,防止在函數執行過程中破壞esp。
  5. 0x08048417 <+3>: and esp,0xfffffff0
  6. 0x0804841a <+6>: sub esp,0x30 ;上面兩句的目的是,開闢一塊48字節大小的棧區,用於保存函數運行過程中的數據和地址
  7. 0x0804841d <+9>: mov eax,0x8048481 ;此爲fun_a的地址,後面會詳細講到
  8. 0x08048422 <+14>: mov DWORD PTR [esp+0x1c],eax ;將fun_a的地址壓棧到esp+0x1c處,方便調用該函數的時候取出。
  9. 0x08048426 <+18>: mov eax,0x804849a
  10. 0x0804842b <+23>: mov DWORD PTR [esp+0x20],eax
  11. 0x0804842f <+27>: mov eax,0x80484b3
  12. 0x08048434 <+32>: mov DWORD PTR [esp+0x24],eax
  13. 0x08048438 <+36>: mov eax,0x80484cc
  14. 0x0804843d <+41>: mov DWORD PTR [esp+0x28],eax ;這兩個也是一個道理,將fun_b,fun_c的地址壓棧,方便調用。
  15. => 0x08048441 <+45>: mov DWORD PTR [esp+0x2c],0x0 ;將0壓入esp+0x2c處,實際此處保存的是i變量的值。
  16. 0x08048449 <+53>: jmp 0x8048473 <main+95> ;開始for循環,它跳轉到cmp指令處,實際就是比較i和4的大小關係,從而決定函數流程
  17. 0x0804844b <+55>: mov eax,DWORD PTR [esp+0x2c] ;將i的值賦給eax寄存器
  18. 0x0804844f <+59>: mov eax,DWORD PTR [esp+eax*4+0x1c];此處實際是將fp[i]的值賦給eax
  19. 0x08048453 <+63>: call eax ;調用fp[i],也就是依次調用fun_a,fun_b,fun_c
  20. 0x08048455 <+65>: mov eax,DWORD PTR [esp+0x2c]
  21. 0x08048459 <+69>: mov edx,DWORD PTR [esp+eax*4+0x1c];將fp[i]的值賦給edx
  22. 0x0804845d <+73>: mov eax,0x80485c0
  23. 0x08048462 <+78>: mov DWORD PTR [esp+0x4],edx ;將edx值壓棧到esp+0x4處
  24. 0x08048466 <+82>: mov DWORD PTR [esp],eax ;將地址0x80485c0壓入esp處,此地址爲存儲字符串的位置,例如“This is fun_a !”。
  25. 0x08048469 <+85>: call 0x8048320 <printf@plt> ;它是printf函數調用必須的參數
  26. 0x0804846e <+90>: add DWORD PTR [esp+0x2c],0x1 ;i++
  27. 0x08048473 <+95>: cmp DWORD PTR [esp+0x2c],0x3 ;比較i和3的大小
  28. 0x08048478 <+100>: jle 0x804844b <main+55> ;如果i <= 3,那麼跳轉到main+55處;否則結束for循環。
  29. 0x0804847a <+102>: mov eax,0x0 ;通常情況下eax保存返回值。這裏其實就是return 0 ;。
  30. 0x0804847f <+107>: leave ;將ebp彈棧,同時恢復esp原有值。
  31. 0x08048480 <+108>: ret
  32. End of assembler dump.

*** 打印fun\_a,fun\_b的地址

只有找到fun_a,fun_b,fun_c的地址,才能具體分析其實現過程。

點擊(此處)摺疊或打開

  1. (gdb) print fp[0]
  2. $1 = (void (*)()) 0x8048481 <fun_a>
  3. (gdb) print fp[1]
  4. $2 = (void (*)()) 0x804849a <fun_b>


*** 查看每個函數所佔內存及內容

點擊(此處)摺疊或打開

  1. (gdb) x /25xh 0x8048481
  2. 0x8048481 <fun_a>: 0x8955 0x83e5 0x18ec 0x04c7 0xc424 0x0485 0xe808 0xfe9d
  3. 0x8048491 <fun_a+16>: 0xffff 0x00b8 0x0000 0xc900 0x55c3 0xe589 0xec83 0xc718
  4. 0x80484a1 <fun_b+7>: 0x2404 0x85d4 0x0804 0x84e8 0xfffe 0xb8ff 0x0000 0x0000
  5. 0x80484b1 <fun_b+23>: 0xc3c9
  6. (gdb) x /25xh 0x804849a
  7. 0x804849a <fun_b>: 0x8955 0x83e5 0x18ec 0x04c7 0xd424 0x0485 0xe808 0xfe84
  8. 0x80484aa <fun_b+16>: 0xffff 0x00b8 0x0000 0xc900 0x55c3 0xe589 0xec83 0xc718
  9. 0x80484ba <fun_c+7>: 0x2404 0x85e4 0x0804 0x6be8 0xfffe 0xb8ff 0x0000 0x0000
  10. 0x80484ca <fun_c+23>: 0xc3c9

從上面可以看到,每個函數佔用25個字節的內存空間,它們很多內容都是一樣的(因爲每個函數的實現功能基本一致,而且函數一般都是開始壓棧保護結尾彈棧返回)。

*** 反彙編fun_a,fun_b

雖然彙編晦澀難懂的缺點,但是它可以幫助你深入的理解函數的執行過程。所以即便你並不是非常熟悉彙編,基本的反彙編代碼是要讀懂的,這非常重要。

點擊(此處)摺疊或打開

  1. (gdb) disassemble 0x8048481
  2. Dump of assembler code for function fun_a:
  3. 0x08048481 <+0>: push ebp
  4. 0x08048482 <+1>: mov ebp,esp
  5. 0x08048484 <+3>: sub esp,0x18
  6. 0x08048487 <+6>: mov DWORD PTR [esp],0x80485c4 ;此處的將第一個字符串地址壓棧,方便後面函數的調用。
  7. 0x0804848e <+13>: call 0x8048330 <puts@plt>
  8. 0x08048493 <+18>: mov eax,0x0
  9. 0x08048498 <+23>: leave
  10. 0x08048499 <+24>: ret
  11. End of assembler dump.
  12. (gdb) disassemble 0x804849a
  13. Dump of assembler code for function fun_b:
  14. 0x0804849a <+0>: push ebp
  15. 0x0804849b <+1>: mov ebp,esp
  16. 0x0804849d <+3>: sub esp,0x18
  17. 0x080484a0 <+6>: mov DWORD PTR [esp],0x80485d4 ;此處的將第二個字符串地址壓棧,方便後面函數的調用。
  18. 0x080484a7 <+13>: call 0x8048330 <puts@plt>
  19. 0x080484ac <+18>: mov eax,0x0
  20. 0x080484b1 <+23>: leave
  21. 0x080484b2 <+24>: ret
  22. End of assembler dump.

*** 查看0x80485c4和0x80485d4處的內容

點擊(此處)摺疊或打開

  1. (gdb) x /16cb 0x80485c4
  2. 0x80485c4: 84 'T' 104 'h' 105 'i' 115 's' 32 ' ' 105 'i' 115 's' 32 ' '
  3. 0x80485cc: 102 'f' 117 'u' 110 'n' 95 '_' 97 'a' 32 ' ' 33 '!' 0 '\000'
  4. (gdb) x /16cb 0x80485d4
  5. 0x80485d4: 84 'T' 104 'h' 105 'i' 115 's' 32 ' ' 105 'i' 115 's' 32 ' '
  6. 0x80485dc: 102 'f' 117 'u' 110 'n' 95 '_' 98 'b' 32 ' ' 33 '!' 0 '\000'

有此可知,此處存儲printf函數所需的“This is fun_a !”等三個字符串。

由上面的分析過程可知,函數指針實際上就是一個地址而已。其中函數名fun_a代表函數開始的地址,也就是函數的入口地址。它從入口地址保存一系列將要執行的彙編指令。

* 附錄GDB調試命令簡介

** 使用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* 三個參數可以一起使用。

** GDB輸出格式:

一般來說,GDB會根據變量的類型輸出變量的值。但你也可以自定義GDB的輸出的格式。例如,你想輸出一個整數的十六進制,或是二進制來查看這個整型變量的中的位的情況。要做到這樣,你可以使用GDB的數據顯示格式: 

x 按十六進制格式顯示變量。
d 按十進制格式顯示變量。
u 按十六進制格式顯示無符號整型。
o 按八進制格式顯示變量。
t 按二進制格式顯示變量。 
a 按十六進制格式顯示變量。
c 按字符格式顯示變量。
f 按浮點數格式顯示變量。
發佈了0 篇原創文章 · 獲贊 1 · 訪問量 8554
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章