歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

函數指針解析(C語言)

Table of Contents

  • 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的地址,才能具體分析其實現過程。

 

(gdb) print fp[0]
$1 = (void (*)()) 0x8048481 <fun_a>
(gdb) print fp[1]
$2 = (void (*)()) 0x804849a <fun_b>
Copyright © Linux教程網 All Rights Reserved