C編程之堆棧回溯


前言

     在嵌入式系統C語言開發調試過程中,常會遇到各類異常情況。一般可按需添加打印信息,以便觀察程序執行流或變量值是否異常。然而,打印操作會占用CPU時間,而且代碼中添加過多打印信息時會顯得很凌亂。此外,即使出錯打印已非常詳盡,但仍難以完全預防和處理段違例(Segment Violation)等錯誤。在沒有外部調試器(如gdb server)可用或無法現場調試的情況下,若程序能在突發崩潰時自動輸出函數的調用堆棧信息(即堆棧回溯),那么對於排錯將會非常有用。

     本文主要介紹嵌入式系統C語言編程中,發生異常時的堆棧回溯方法。文中涉及的代碼運行環境如下:

 

     本文假定讀者已具備函數調用棧、信號處理等方面的知識。相關性文章也可參見:

     《C語言函數調用棧(一)》

     《C語言函數調用棧(二)》

     《C語言函數調用棧(三)》

     《嵌入式系統C編程之錯誤處理》

 

 

一  原理

     通常,在多級函數調用過程中,處理器會將調用函數指令的下一條地址壓入堆棧。通過分析當前棧幀,找到上層函數在堆棧中的棧幀地址,再分析上層函數的棧幀,進而找到再上層函數的棧幀地址……如此回溯直至最頂層函數。這就組成一條函數執行的路徑軌跡(調用順序)。

     以Intel x86架構為例,由於幀基指針(BP)所指向的內存中存儲上一層函數調用時的BP值,而在每層函數調用中都能通過當前BP值向棧底方向偏移得到返回地址。如此遞歸,可逐層向上找到最頂層函數。

     在GDB里,使用bt命令可獲取函數調用棧。若要通過代碼獲取當前函數調用棧,可借助glibc庫提供的backtrace系列函數。由於不同處理器堆棧布局不同,堆棧回溯由編譯器內建函數__buildin_frame_address和__buildin_return_address實現,涉及工具glibc和gcc。若編譯器不支持該功能,也可自行實現,其步驟如下(以Intel x86架構為例):

     1) 獲得當前函數的BP;

     2) 通過BP偏移獲得主調函數的IP(返回地址);

     3) 通過當前BP指向的內容,獲得主調函數BP地址;

     4) 循環執行以上步驟直至到達棧底。

     glibc2.1及以上版本提供backtrace等GNU擴展函數以獲取當前線程的函數調用堆棧,其原型聲明在頭文件<execinfo.h>內。

int backtrace(void **buffer, int size);

     該函數獲取當前線程的調用堆棧,並以指針(實為返回地址)列表形式存入參數buffer緩沖區中。參數size指定buffer中可容納的void*元素數目。該函數返回是實際獲取的元素數,且不超過size大小。若返回值小於size,則buffer中保存完整的堆棧信息;若返回值等於size,則堆棧信息可能已被刪減(最早的那些棧幀返回地址被丟棄)。

char ** backtrace_symbols(void *const *buffer, int size);

     該函數將backtrace函數獲取的信息轉換為一個字符串數組。參數buffer應指向backtrace函數獲取的地址數組,參數size為該數組中的元素個數(backtrace函數返回值)。

     該函數返回一個指向字符串數組的指針,數組元素個數與buffer數組相同(即為size)。每個字符串包含一個對應buffer數組元素的可打印描述信息,如函數名、偏移地址和實際的返回地址(16進制)。

     該函數的返回值指向函數內部通過malloc所申請的動態內存,因此調用者必須使用free函數來釋放該內存。若不能為字符串申請足夠的內存,則該函數返回NULL。

     目前,只有在使用ELF二進制格式的程序和庫的系統中才能獲取函數名和偏移地址。在其他系統中,僅能獲取16進制的返回地址。此外,可能需要向鏈接器傳遞額外的標志,以支持函數名功能(如在使用GNU ld的系統中,需要傳遞-rdynamic選項來通知鏈接器將所有符號添加到動態符號表中)。

void backtrace_symbols_fd(void *const *buffer, int size, int fd);

     該函數與backtrace_symbols函數功能相同,但不向調用者返回字符串數組,而是將結果寫入文件描述符為fd的文件中,每條信息字符串對應一行。該函數不會為字符串存儲申請動態內存,因此適用於堆內存可能被破壞的情況(此時buffer也應為靜態或自動存儲空間)。

     舉例如下:

復制代碼
復制代碼
 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 #include <execinfo.h>
 5 
 6 static void StackTrace(void){
 7     void *pvTraceBuf[10];
 8     int dwTraceSize = backtrace(pvTraceBuf, 10);
 9     backtrace_symbols_fd(pvTraceBuf, dwTraceSize, STDOUT_FILENO);
10 }
11 
12 void FuncC(void){ StackTrace(); }
13 static void FuncB(void){ FuncC(); }
14 void FuncA(void){ FuncB(); }
15 int main(void){
16     FuncA();
17     return 0;
18 }
復制代碼
復制代碼

     編譯運行結果如下:

復制代碼
復制代碼
1 [wangxiaoyuan_@localhost test1]$ gcc -Wall -rdynamic -o StackTrace StackTrace.c
2 [wangxiaoyuan_@localhost test1]$ ./StackTrace
3 ./StackTrace[0x80485f9]
4 ./StackTrace(FuncC+0xb)[0x8048623]
5 ./StackTrace[0x8048630]
6 ./StackTrace(FuncA+0xb)[0x804863d]
7 ./StackTrace(main+0x16)[0x8048655]
8 /lib/libc.so.6(__libc_start_main+0xdc)[0x552e9c]
9 ./StackTrace[0x8048521]
復制代碼
復制代碼

     當若干主調函數中的某個以錯誤的參數調用給定函數時,通過在該函數內檢查參數並調用StackTrace()函數,即可方便地定位出錯的主調函數。

     使用backtrace系列函數獲取堆棧回溯信息時,需要注意以下幾點:

     1) 某些編譯器優化可能對獲取有效的調用堆棧造成干擾。

     若忽略幀基指針(-fomit-frame-pointer),回溯時將無法正確解析堆棧內容。優化級別非0時(如-O2)可能改變函數調用關系;尾調用(Tail-call)優化會替換棧幀內容,這些也會影響回溯結果。

     2) 內聯函數和宏定義沒有棧幀結構。

     3) 靜態函數名無法被內部解析,因其無法被動態鏈接訪問。此時可使用外部工具addr2line解析。

     4) 若內存垃圾導致堆棧自身被破壞,則無法進行回溯。

     若自行實現堆棧回溯功能,可調用dladdr()函數來解析返回地址所對應的文件名和函數名等信息。

#include <dlfcn.h>

int dladdr(void *addr, Dl_info *info);

     該函數出錯時(共享庫libdl.so目標文件段中不存在該地址)返回0,成功時返回非0值。

     Dl_info結構定義如下:

1 typedef struct{
2     const char *dli_fname;  /* Filename of defining object */
3     void *dli_fbase;        /* Load address of that object */
4     const char *dli_sname;  /* Name of nearest lower symbol */
5     void *dli_saddr;        /* Exact value of nearest symbol */
6 }Dl_info;

     使用dladdr()函數時,需加上-rdynamic編譯選項和-ldl鏈接選項。

     更進一步,可將堆棧回溯置於信號處理程序中。這樣,當程序突然崩潰時,當前進程接收到內核發送的信號后,在信號處理程序中自動輸出進程的執行信息、當前寄存器內容及函數調用關系等。

     通常使用sigaction()函數檢查或修改與指定信號相關聯的處理動作(或同時執行這兩種操作):

#include <signal.h>

int sigaction( int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);

     該函數成功時返回0,否則返回-1並設置errno值。參數signo為待檢測或修改其具體動作的信號編號。若act指針非空,則修改其動作;若oact指針非空,則系統經由oact指針返回該信號的上個動作。sigaction結構的sa_flags字段指定對信號進行處理的各個選項。當設置為SA_SIGINFO標志時,表示信號附帶的信息可傳遞到信號處理函數中。此時,應按下列方式調用信號處理程序:

void handler(int signo, siginfo_t *info, void *context);

     siginfo_t結構包含信號產生原因的有關信息,需針對不同信號選取有意義的屬性。其中,si_signo(信號編號)、si_errno(errno值)和si_code(信號產生原因)定義針對所有信號。其余屬性只有部分信息對特定信號有用。例如,si_addr指示觸發故障的內存地址(盡管該地址可能並不准確),僅對SIGILL、SIGFPE、SIGSEGV和SIGBUS 信號有意義。si_errno字段包含錯誤編號,對應於引發信號產生的條件,並由實現定義(Linux中通常不使用該屬性)。

     信號處理程序的context參數是無類型指針,可被強制轉換為ucontext_t結構,用於標識信號產生時的進程上下文(如CPU寄存器)。該結構定義在頭文件<ucontext.h>內,且包含mcontext_t類型的uc_mcontext字段(該字段保存特定於機器的寄存器上下文)。

     注意,即使指定信號處理函數,若不設置SA_SIGINFO標志,信號處理函數同樣不能得到信號傳遞過來的附加信息(info和context),在信號處理函數中訪問這些信息都將導致段錯誤。

 

 

二  實現

     本節將實現基於信號處理的用戶態進程堆棧回溯功能。該實現假定未忽略幀基指針。

     注意,若只需向上回溯一層函數,如查看某函數被哪些函數直接調用,則可對其進行簡單封裝。假定被調函數名為FuncTraced,可將其聲明和定義中的名稱改為FuncTraced1,然后封裝名為FuncTraced的宏。該宏內部輸出定位信息並調用FuncTraced1()函數,如:

1 extern void FuncTraced1(void);
2 #define FuncTraced() do{ \
3     printf("[%s<%d>]Call FuncTraced!\n", __FILE__, __LINE__); \
4     FuncTraced1(); \
5 }while(0)

     示例中原FuncTraced()函數無返回值,若有則封裝方式略有不同。

2.1 數據定義

     定義如下宏:

復制代碼
復制代碼
 1 #ifndef __i386
 2     #warning "Possibly Non-x86 Platform!"
 3 #endif
 4 
 5 #if defined(REG_RIP)
 6     #define REG_IP     REG_RIP   //指令指針(保存返回地址)
 7     #define REG_BP     REG_RBP   //幀基指針
 8     #define REG_FMT    "%016lx"
 9 #elif defined(REG_EIP)
10     #define REG_IP     REG_EIP
11     #define REG_BP     REG_EBP
12     #define REG_FMT    "%08x"
13 #else
14     #warning "Neither REG_RIP nor REG_EIP is defined!"
15     #define REG_FMT    "%08x" 
16 #endif
17 
18 #define BTR_FILE_LEN    512   //保存堆棧回溯結果的文件路徑最大長度
19 #ifndef BTR_FILE        //保存堆棧回溯結果的基本文件名
20     #define BTR_FILE         "btr" 
21 #endif
22 #ifndef BTR_FILE_PATH   //保存堆棧回溯結果的文件路徑(默認為當前路徑)
23     #define BTR_FILE_PATH    "."  //"..//var//tmp"
24 #endif
25 
26 #ifndef MAX_BTR_LEVEL   //函數回溯的最大層數
27     #define MAX_BTR_LEVEL    20
28 #endif
29 
30 //用戶調用SHOW_STACK宏可觸發堆棧回溯
31 #ifndef BTR_SIG   //觸發堆棧回溯的用戶信號
32     #define BTR_SIG          SIGUSR1
33 #endif
34 #define SHOW_STACK()     do{raise(BTR_SIG);}while(0)
復制代碼
復制代碼

     其中,REG_IP、REG_BP分別為x86處理器的指令指針和幀基指針寄存器編號,REG_FMT宏指定寄存器內容的輸出格式。BTR_FILE等文件相關的宏指定保存堆棧回溯結果時文件路徑和名稱。當程序運行於嵌入式單板時,當前路徑可能沒有寫入權限,此時用戶可自定義BTR_FILE_PATH宏。

     定義如下全局變量:

1 static FILE *gpStraceFd = NULL;  //輸出文件描述符(置為stderr時輸出到終端,否則將輸出存入文件)
2 typedef VOID (*SignalHandleFunc)(INT32S dwSignal);
3 static SignalHandleFunc gfpCustSigHandler = NULL; //用戶自定義的信號處理函數指針

2.2 函數接口

     首先定義一組私有函數。這些內部使用的函數已盡可能保證參數安全性,故省去參數校驗處理。

     SpecifyStraceOutput()函數指定堆棧回溯結果的輸出方式:

復制代碼
復制代碼
 1 /******************************************************************************
 2 * 函數名稱:  SpecifyStraceOutput
 3 * 功能說明:  指定回溯結果輸出方式
 4 ******************************************************************************/
 5 static FILE *SpecifyStraceOutput(VOID)
 6 {
 7 #ifdef __BTR_TO_FILE
 8     time_t tTime;
 9     CHAR szFileName[BTR_FILE_LEN];
10     szFileName[0] = '\0';
11     if(time(&tTime) != -1)
12     {
13         struct tm *ptTime = localtime(&tTime);
14         snprintf(szFileName, sizeof(szFileName), "%s/[%d]%d%02d%02d_%02d%02d%02d.%s",
15                  BTR_FILE_PATH, getpid(), (ptTime->tm_year+1900), (ptTime->tm_mon+1),
16                  ptTime->tm_mday, ptTime->tm_hour, ptTime->tm_min, ptTime->tm_sec, BTR_FILE);
17     }
18     else
19     {
20         snprintf(szFileName, sizeof(szFileName), "%s/%s", BTR_FILE_PATH, BTR_FILE);
21     }
22 
23     FILE *pFile = fopen(szFileName, "w+");
24     if(NULL == pFile)
25     {
26         fprintf(stderr, "Cannot open File '%s'(%s)\n!", szFileName, strerror(errno));
27         return -1;
28     }
29     return pFile;
30 #else
31     return stderr;
32 #endif
33 }
復制代碼
復制代碼

     當__BTR_TO_FILE編譯選項打開時,堆棧回溯結果輸出到指定目錄下的文件內。若成功獲取當前時間,則該文件名為"[進程號]年月日_時分秒.btr",否則名為"btr"。當__BTR_TO_FILE編譯選項關閉時,堆棧回溯結果直接輸出到終端設備屏幕上。

     注意,SpecifyStraceOutput()函數返回的文件描述符類型為FILE*。標准流stdin/stdout/stderr均為該類型,用於帶緩沖的高級I/O函數(如fread/fwrite/fclose等);而STDIN_FILENO/ STDOUT_FILENO/STDERR_FILENO的類型為int,用於低級I/O調用(如read/write/close等)。

     信號處理函數SigHandler()依次輸出接收到的信號信息、堆棧寄存器內容及堆棧回溯信息:

復制代碼
復制代碼
 1 /******************************************************************************
 2 * 函數名稱:  SigHandler
 3 * 功能說明:  信號處理函數
 4 * 輸入參數:  INT32S dwSigNo      :信號名
 5             siginfo_t *tSigInfo :信號產生原因等信息
 6             VOID *pvContext     :信號傳遞時的進程上下文
 7 * 輸出參數:  NA
 8 * 返 回 值:  VOID
 9 ******************************************************************************/
10 static VOID SigHandler(INT32S dwSigNo, siginfo_t *tSigInfo, VOID *pvContext)
11 {
12     fprintf(gpStraceFd, "\nStart of Stack Trace>>>>>>>>>>>>>>>>>>>>>>>>>>\n");
13 
14     fprintf(gpStraceFd, "Process (%d) receive signal %d\n", getpid(), dwSigNo);
15 
16     fprintf(gpStraceFd, "<Signal Information>:\n" );
17     fprintf(gpStraceFd, "\tSigNo:     %-2d(%s)\n", tSigInfo->si_signo, OmciStrSigNo(tSigInfo->si_signo)); //strsignal(dwSigNo)
18     fprintf(gpStraceFd, "\tErrNo:     %-2d(%s)\n", tSigInfo->si_errno, strerror(tSigInfo->si_errno));
19     fprintf(gpStraceFd, "\tSigCode:   %-2d\n", tSigInfo->si_code);
20     fprintf(gpStraceFd, "\tRaised at: %p[Unreliable]\n", tSigInfo->si_addr);
21 
22     fprintf(gpStraceFd, "<Register Content>: \n\t" );
23     INT32U dwIdx = 0;
24     ucontext_t *ptContext = (ucontext_t*)pvContext;
25     for(dwIdx = 0; dwIdx < NGREG; dwIdx++)
26     {
27         fprintf(gpStraceFd, REG_FMT" ", ptContext->uc_mcontext.gregs[dwIdx]);
28         if(0 == ((dwIdx+1)%4)) //每行輸出4個寄存器值
29             fprintf(gpStraceFd, "\n\t");
30     }
31     fprintf(gpStraceFd, "\n");
32 
33 #if defined(REG_RIP) || defined(REG_EIP)
34     dwIdx = 0;
35     VOID *pvIp = (VOID*)ptContext->uc_mcontext.gregs[REG_IP];
36     VOID **ppvBp = (VOID**)ptContext->uc_mcontext.gregs[REG_BP];
37     fprintf(gpStraceFd, "<Stack Trace(Customized)>:\n");
38     while(ppvBp != &pvIp)
39     {
40         Dl_info tDlInfo;
41         if(!dladdr(pvIp, &tDlInfo))
42             break;
43         fprintf(gpStraceFd, "\t[%2d] (%s) [0x%08x] (%s)+0x%02x\n", ++dwIdx,
44                 tDlInfo.dli_fname, (INT32U)pvIp,
45                 (tDlInfo.dli_sname != NULL) ? tDlInfo.dli_sname : "<STATIC>",
46                 ((INT32U)pvIp - (INT32U)tDlInfo.dli_saddr));
47 
48         if((NULL == ppvBp) || (tDlInfo.dli_sname && !strcmp(tDlInfo.dli_sname, "main")))
49             break;
50         pvIp = ppvBp[1];          //幀基指針向高地址偏移1個單位(4字節)為返回地址
51         ppvBp = (VOID**)(*ppvBp); //幀基指針所指向的空間存放主調函數棧幀的幀基指針
52     }
53 #else
54     fprintf(gpStraceFd, "<Stack Trace(Standard)>:\n");
55 
56     VOID *pvTraceBuf[MAX_BTR_LEVEL];
57     INT32U dwTraceSize = backtrace(pvTraceBuf, MAX_BTR_LEVEL);
58     CHAR **ppTraceInfos = backtrace_symbols(pvTraceBuf, dwTraceSize);
59     if(!ppTraceInfos || !(*ppTraceInfos))
60         exit(EXIT_FAILURE);
61 
62     for(dwIdx = 0; dwIdx < dwTraceSize; dwIdx++)
63         fprintf(gpStraceFd, "\t%s\n", ppTraceInfos[dwIdx]);
64 
65     free(ppTraceInfos);
66 #endif
67 
68     fprintf(gpStraceFd, "End of Stack Trace<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n");
69 
70     if(gfpCustSigHandler != NULL)
71         gfpCustSigHandler(dwSigNo);
72 
73     exit(EXIT_FAILURE);
74 }
復制代碼
復制代碼

     其中,si_signo可由入參dwSigNo代替,si_errno句也可省略。因為輸出格式需要使用制表符('\t'),故直接使用backtrace_symbols()函數。若更側重安全性,則可換用backtrace_symbols_fd()函數。

     若REG_RIP或REG_EIP宏已定義,則根據x86寄存器編號獲取主調函數的指令指針IP和幀基指針BP,然后回溯棧幀並自定義輸出信息。否則,調用backtrace()相關函數輸出回溯信息。

     注意,REG_XXP等宏的定義位於頭文件<ucontext.h>內。若要使用這些宏,需先定義_GNU_SOURCE宏。該宏位於頭文件<features.h>(被<ucontext.h>所包含)內,用於控制諸如_ISOC99_SOURCE、_POSIX_SOURCE等功能測試宏,指示是否包含對應標准的特性。_GNU_SOURCE宏必須在所有標准頭文件之前包含,即定義在源文件首行,或指定為編譯選項。定義該宏后,可使用很多非標准的GNU/ Linux擴展函數。

     堆棧回溯時靜態函數名不可見,因此自定義輸出中將其顯示為<STATIC>。此外,直接輸出時文件描述符為stderr(不帶緩沖),若改為stdout(行緩沖)則"\t%s\n"格式控制將不能正常顯示。

     進程調用exit()函數退出時,內核將關閉進程中已打開的所有文件描述符。因此,SigHandler()函數中未顯式調用fclose(gpStraceFd)。

     讀者也可根據棧幀的布局(入參向低地址偏移依次為返回地址和幀基指針),自行獲取IP/BP指針。如:

1 VOID **GetEbp(INT32U dwDummy)
2 {
3     VOID **ebp = (VOID **)&dwDummy - 2;
4     return (*ebp);
5 }

     則SigHandler()函數中對ppvBp的賦值可改為:

1 VOID **ppvBp = getEbp(dwIdx); //或
2 VOID **ppvBp = (VOID **)&dwSigNo - 2;

     注意,此時獲得的寄存器指針指向SigHandler()函數棧幀,while循環內應先執行pvIp = ppvBp[1]再解析地址。

     OmciStrSigNo()函數基於NameParser來解析信號名:

復制代碼
復制代碼
 1 #define NAME_MAP_ENTRY(name)    {name, #name}
 2 static T_NAME_PARSER gSigNameMap[] = {
 3     NAME_MAP_ENTRY(SIGHUP),
 4     NAME_MAP_ENTRY(SIGINT),
 5     NAME_MAP_ENTRY(SIGQUIT),
 6     NAME_MAP_ENTRY(SIGILL),
 7     NAME_MAP_ENTRY(SIGTRAP),
 8     NAME_MAP_ENTRY(SIGABRT), //SIGABRT(ANSI) = SIGIOT(4.2 BSD)
 9     NAME_MAP_ENTRY(SIGBUS),
10     NAME_MAP_ENTRY(SIGFPE),
11     NAME_MAP_ENTRY(SIGKILL),
12     NAME_MAP_ENTRY(SIGUSR1),
13     NAME_MAP_ENTRY(SIGSEGV),
14     NAME_MAP_ENTRY(SIGUSR2),
15     NAME_MAP_ENTRY(SIGPIPE),
16     NAME_MAP_ENTRY(SIGALRM),
17     NAME_MAP_ENTRY(SIGTERM),
18     NAME_MAP_ENTRY(SIGSTKFLT),
19     NAME_MAP_ENTRY(SIGCHLD),  //SIGCHLD(POSIX) = SIGCLD(System V)
20     NAME_MAP_ENTRY(SIGCONT),
21     NAME_MAP_ENTRY(SIGSTOP),
22     NAME_MAP_ENTRY(SIGTSTP),
23     NAME_MAP_ENTRY(SIGTTIN),
24     NAME_MAP_ENTRY(SIGTTOU),
25     NAME_MAP_ENTRY(SIGURG),
26     NAME_MAP_ENTRY(SIGXCPU),
27     NAME_MAP_ENTRY(SIGXFSZ),
28     NAME_MAP_ENTRY(SIGVTALRM),
29     NAME_MAP_ENTRY(SIGPROF),
30     NAME_MAP_ENTRY(SIGWINCH),
31     NAME_MAP_ENTRY(SIGIO),     //SIGIO(4.2 BSD) = SIGPOLL(System V)
32     NAME_MAP_ENTRY(SIGPWR),
33     NAME_MAP_ENTRY(SIGSYS)
34 };
35 //信號值字符串化
36 CHAR *OmciStrSigNo(INT32S dwSigNo)
37 {
38     return NameParser(gSigNameMap, ARRAY_SIZE(gSigNameMap), dwSigNo, "UnkownSigNo");
39 }
復制代碼
復制代碼

     NameParser()函數實現參見《C語言表驅動法編程實踐》一文,讀者也可自行實現解析函數。

     若不想依賴函數解析信號名,也可使用如下宏定義:

復制代碼
復制代碼
 1 #define SIG_NAME(eSigNo) \
 2     ((eSigNo) == SIGHUP     ?    "SIGHUP"      : \
 3     ((eSigNo) == SIGINT     ?    "SIGINT"      : \
 4     ((eSigNo) == SIGQUIT    ?    "SIGQUIT"     : \
 5     ((eSigNo) == SIGILL     ?    "SIGILL"      : \
 6     ((eSigNo) == SIGTRAP    ?    "SIGTRAP"     : \
 7     ((eSigNo) == SIGABRT    ?    "SIGABRT(ANSI)/SIGIOT(4.2 BSD)"     : \
 8     ((eSigNo) == SIGBUS     ?    "SIGBUS"      : \
 9     ((eSigNo) == SIGFPE     ?    "SIGFPE"      : \
10     ((eSigNo) == SIGKILL    ?    "SIGKILL"     : \
11     ((eSigNo) == SIGUSR1    ?    "SIGUSR1"     : \
12     ((eSigNo) == SIGSEGV    ?    "SIGSEGV"     : \
13     ((eSigNo) == SIGUSR2    ?    "SIGUSR2"     : \
14     ((eSigNo) == SIGPIPE    ?    "SIGPIPE"     : \
15     ((eSigNo) == SIGALRM    ?    "SIGALRM"     : \
16     ((eSigNo) == SIGTERM    ?    "SIGTERM"     : \
17     ((eSigNo) == SIGSTKFLT  ?    "SIGSTKFLT"   : \
18     ((eSigNo) == SIGCHLD    ?    "SIGCHLD(POSIX)/SIGCLD(System V)"   : \
19     ((eSigNo) == SIGCONT    ?    "SIGCONT"     : \
20     ((eSigNo) == SIGSTOP    ?    "SIGSTOP"     : \
21     ((eSigNo) == SIGTSTP    ?    "SIGTSTP"     : \
22     ((eSigNo) == SIGTTIN    ?    "SIGTTIN"     : \
23     ((eSigNo) == SIGTTOU    ?    "SIGTTOU"     : \
24     ((eSigNo) == SIGURG     ?    "SIGURG"      : \
25     ((eSigNo) == SIGXCPU    ?    "SIGXCPU"     : \
26     ((eSigNo) == SIGXFSZ    ?    "SIGXFSZ"     : \
27     ((eSigNo) == SIGVTALRM  ?    "SIGVTALRM"   : \
28     ((eSigNo) == SIGPROF    ?    "SIGPROF"     : \
29     ((eSigNo) == SIGWINCH   ?    "SIGWINCH"    : \
30     ((eSigNo) == SIGIO      ?    "SIGIO(4.2 BSD)/SIGPOLL(System V)"  : \
31     ((eSigNo) == SIGPWR     ?    "SIGPWR"      : \
32     ((eSigNo) == SIGSYS     ?    "SIGSYS"      : \
33      "Unknown" )))))))))))))))))))))))))))))))
復制代碼
復制代碼

     InstallFaultTrap()為程序異常時安裝的信號捕獲函數:

復制代碼
復制代碼
 1 /******************************************************************************
 2 * 函數名稱:  InstallFaultTrap
 3 * 功能說明:  安裝出錯時的信號捕獲函數
 4 * 輸入參數:  SignalHandleFunc fpCustSigHandler :用戶自定義的信號處理函數
 5 * 輸出參數:  NA
 6 * 返 回 值:  INT32S
 7 ******************************************************************************/
 8 static INT32S InstallFaultTrap(SignalHandleFunc fpCustSigHandler)
 9 {
10     gfpCustSigHandler = fpCustSigHandler;
11 
12     struct sigaction tSigAction;
13     memset(&tSigAction, 0, sizeof(tSigAction));
14     tSigAction.sa_sigaction = SigHandler;
15     sigemptyset(&tSigAction.sa_mask);
16     tSigAction.sa_flags = SA_SIGINFO;
17 
18     //檢查可能導致進程終止的信號
19     INT32S dwRet = 0;
20     if((dwRet = sigaction(SIGSEGV, &tSigAction, NULL)) < 0)
21         fprintf(stderr, "[%s]Sigaction failed for SIGSEGV(%d, %s)!\n", FUNC_NAME, errno, strerror(errno));
22 
23     if((dwRet = sigaction(SIGQUIT, &tSigAction, NULL)) < 0)
24         fprintf(stderr, "[%s]Sigaction failed for SIGQUIT(%d, %s)!\n", FUNC_NAME, errno, strerror(errno));
25 
26     if((dwRet = sigaction(SIGILL, &tSigAction, NULL)) < 0)
27         fprintf(stderr, "[%s]Sigaction failed for SIGILL(%d, %s)!\n", FUNC_NAME, errno, strerror(errno));
28 
29     if((dwRet = sigaction(SIGTRAP, &tSigAction, NULL)) < 0)
30         fprintf(stderr, "[%s]Sigaction failed for SIGTRAP(%d, %s)!\n", FUNC_NAME, errno, strerror(errno));
31 
32     if((dwRet = sigaction(SIGABRT, &tSigAction, NULL)) < 0)
33         fprintf(stderr, "[%s]Sigaction failed for SIGABRT(%d, %s)!\n", FUNC_NAME, errno, strerror(errno));
34 
35     if((dwRet = sigaction(SIGFPE, &tSigAction, NULL)) < 0)
36         fprintf(stderr, "[%s]Sigaction failed for SIGFPE(%d, %s)!\n", FUNC_NAME, errno, strerror(errno));
37 
38     if((dwRet = sigaction(SIGBUS, &tSigAction, NULL)) < 0)
39         fprintf(stderr, "[%s]Sigaction failed for SIGBUS(%d, %s)!\n", FUNC_NAME, errno, strerror(errno));
40 
41     if((dwRet = sigaction(SIGXFSZ, &tSigAction, NULL)) < 0)
42         fprintf(stderr, "[%s]Sigaction failed for SIGXFSZ(%d, %s)!\n", FUNC_NAME, errno, strerror(errno));
43 
44     if((dwRet = sigaction(SIGXCPU, &tSigAction, NULL)) < 0)
45         fprintf(stderr, "[%s]Sigaction failed for SIGXCPU(%d, %s)!\n", FUNC_NAME, errno, strerror(errno));
46 
47     if((dwRet = sigaction(SIGSYS, &tSigAction, NULL)) < 0)
48         fprintf(stderr, "[%s]Sigaction failed for SIGSYS(%d, %s)!\n", FUNC_NAME, errno, strerror(errno));
49 
50     if((dwRet = sigaction(BTR_SIG, &tSigAction, NULL)) < 0)
51         fprintf(stderr, "[%s]Sigaction failed for %s(%d, %s)!\n", FUNC_NAME,
52                 OmciStrSigNo(BTR_SIG), errno, strerror(errno));
53 
54     return dwRet;
55 }
復制代碼
復制代碼

     用戶可通過fpCustSigHandler回調函數額外地輸出特定的自定義信息。

     通常,並不期望用戶顯式地初始化堆棧回溯功能。因此,提供__BTR_AUTO_INIT編譯選項以支持自動初始化(AutoInitBacktrace):

復制代碼
復制代碼
 1 /******************************************************************************
 2 * 函數名稱:  AutoInitBacktrace
 3 * 功能說明:  自動初始化堆棧回溯功能
 4 * 輸入參數:  VOID
 5 * 輸出參數:  NA
 6 * 返 回 值:  INT32S
 7 * 注意事項:  該函數在main()函數之前執行,無需用戶顯式調用
 8 ******************************************************************************/
 9 #ifdef __BTR_AUTO_INIT
10 static VOID __attribute((constructor)) AutoInitBacktrace(VOID)
11 {
12     gpStraceFd = SpecifyStraceOutput();
13     InstallFaultTrap(NULL);
14 }
15 #endif
復制代碼
復制代碼

     其中,聲明為gcc(constructor)屬性的函數將在main()函數之前被執行,而聲明為gcc(destructor)屬性的函數則在_after_ main()退出時執行。

     若用戶想額外輸出自定義信息,則需要顯式調用MannInitBacktrace()函數進行手工初始化。該函數調用時可指定fpCustSigHandler回調函數:

復制代碼
復制代碼
 1 /******************************************************************************
 2 * 函數名稱:  MannInitBacktrace
 3 * 功能說明:  手工初始化堆棧回溯功能
 4 * 輸入參數:  SignalHandleFunc fpCustSigHandler :用戶自定義的信號處理函數
 5 * 輸出參數:  NA
 6 * 返 回 值:  VOID
 7 * 注意事項:  fpCustSigHandler符合signal()函數原型,用戶可借此額外地輸出
 8             特定的自定義信息
 9 ******************************************************************************/
10 VOID MannInitBacktrace(SignalHandleFunc fpCustSigHandler)
11 {
12     gpStraceFd = SpecifyStraceOutput();
13     InstallFaultTrap(fpCustSigHandler);
14 }
復制代碼
復制代碼

 

 

三  測試

     本節將對上文實現的用戶態進程堆棧回溯功能進行測試。測試函數如下:

復制代碼
復制代碼
 1 VOID Func1(VOID){
 2     SHOW_STACK();
 3     return;
 4 }
 5 VOID Func2(VOID){
 6     Func1();
 7     printf("%s\n", 0x123);
 8     return;
 9 }
10 VOID BtrTest(VOID){
11     Func2();
12     printf("%d\n", 5/0);
13     return;
14 }
復制代碼
復制代碼

     指定的編譯選項為:

1 CFLAGS += -D__BTR_AUTO_INIT -rdynamic –ldl #-D__BTR_TO_FILE
2 CFLAGS += -DMAX_BTR_LEVEL=10
3 CFLAGS += -fno-omit-frame-pointer

     執行結果如下:

復制代碼
復制代碼
 1 Start of Stack Trace>>>>>>>>>>>>>>>>>>>>>>>>>>
 2 Process (18390) receive signal 10
 3 <Signal Information>:
 4         SigNo:     10(SIGUSR1)
 5         ErrNo:     0 (Success)
 6         SigCode:   -6
 7         Raised at: 0x47d6[Unreliable]
 8 <Register Content>: 
 9         00000033 00000000 0000007b 0000007b 
10         006c8ff4 00535ca0 bfb62228 bfb6221c 
11         000047d6 0000000a 000047d6 00000000 
12         00000000 00000000 00480402 00000073 
13         00000202 bfb6221c 0000007b 
14 <Stack Trace(Standard)>:
15         ./OmciExec [0x804a770]
16         [0x480440]
17         ./OmciExec(Func1+0x12) [0x804ad4e]
18         ./OmciExec(Func2+0xb) [0x804ad5b]
19         ./OmciExec(BtrTest+0xb) [0x804ad7c]
20         ./OmciExec(main+0x16) [0x804eec0]
21         /lib/libc.so.6(__libc_start_main+0xdc) [0x552e9c]
22         ./OmciExec [0x8049f31]
23 End of Stack Trace<<<<<<<<<<<<<<<<<<<<<<<<<<<<
復制代碼
復制代碼

     若注釋掉Func1()函數中的SHOW_STACK()語句,則執行結果如下:

復制代碼
復制代碼
 1 Start of Stack Trace>>>>>>>>>>>>>>>>>>>>>>>>>>
 2 Process (18429) receive signal 11
 3 <Signal Information>:
 4         SigNo:     11(SIGSEGV)
 5         ErrNo:     0 (Success)
 6         SigCode:   1 
 7         Raised at: 0x123[Unreliable]
 8 <Register Content>: 
 9         00000033 00000000 0000007b 0000007b 
10         00000123 bf9a5114 bf9a50ec bf9a4acc 
11         0067eff4 00579999 00000003 00000123 
12         0000000e 00000004 005ad1ab 00000073 
13         00010206 bf9a4acc 0000007b 
14 <Stack Trace(Standard)>:
15         ./OmciExec [0x804a740]
16         [0xedc440]
17         /lib/libc.so.6(_IO_printf+0x33) [0x582e83]
18         ./OmciExec(Func2+0x1f) [0x804ad30]
19         ./OmciExec(BtrTest+0xb) [0x804ad3d]
20         ./OmciExec(main+0x16) [0x804ee80]
21         /lib/libc.so.6(__libc_start_main+0xdc) [0x552e9c]
22         ./OmciExec [0x8049f01]
23 End of Stack Trace<<<<<<<<<<<<<<<<<<<<<<<<<<<<
復制代碼
復制代碼

     若指定編譯選項為:

1 CFLAGS += -D__BTR_AUTO_INIT -rdynamic -ldl
2 CFLAGS += -D_GNU_SOURCE
3 CFLAGS += -fno-omit-frame-pointer

     則部分執行結果如下:

復制代碼
復制代碼
 1 <Register Content>: 
 2         00000033 00000000 0000007b 0000007b 
 3         00000123 bfbe8694 bfbe866c bfbe804c 
 4         0067eff4 00579999 00000003 00000123 
 5         0000000e 00000004 005ad1ab 00000073 
 6         00010206 bfbe804c 0000007b 
 7 <Stack Trace(Customized)>:
 8         [ 1] (/lib/libc.so.6) [0x005ad1ab] (strlen)+0x0b
 9         [ 2] (/lib/libc.so.6) [0x00582e83] (_IO_printf)+0x33
10         [ 3] (./OmciExec) [0x0804adfb] (Func2)+0x1f
11         [ 4] (./OmciExec) [0x0804ae08] (BtrTest)+0x0b
12         [ 5] (./OmciExec) [0x0804f154] (main)+0x2a
復制代碼
復制代碼

 

 

四  參考

backtrace系列函數用法參考http://www.kernel.org/doc/man-pages/online/pages/man3/backtrace.3.html

sigaction函數用法參考http://man7.org/linux/man-pages/man2/sigaction.2.html 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM