巧用backtrace系列函數,在不具備gdb環境的Linux系統上大致定位段錯誤位置

1: 段錯誤產生的原因

簡而言之,產生段錯誤就是訪問了錯誤的內存段,一般是你沒有權限,或者根本就不存在對應的物理內存,
尤其常見的是訪問0地址.一旦一個程序發生了越界訪問,系統就採取內存保護措施,並給那個程序發送
SIGSEGV信號,程序接到那個信號後就知道segmentation fault出現了。

想對”段錯誤”有更詳細的瞭解可以去閱讀“Linux下的段錯誤產生的原因及調試方法” 這篇文章,本文的
內容基本是從那文章裏提取出來的。

2: SIGSEGV信號處理函數

程序接到SIGSEGV信號後的缺省處理是退出程序,這也是爲什麼總是看到程序打印一個“segmentation fault”
信息後就消失了。我們可以使用 signal(SIGSEGV, &your_function);函數來接管SIGSEGV信號的處理,讓
程序在發生段錯誤後,自動調用我們準備好的函數,從而在那個函數裏來獲取當前函數調用棧。

3: libc的Backtraces函數

在GDB裏,可以簡單的使用bt命令就可以獲取函數調用棧,但如何通過代碼獲取當前函數調用棧?
這裏我們可以通過libc庫提供的Backtraces系列函數

01.A backtrace is a list of the function calls that are currently active in a  
02.thread. The usual way to inspect a backtrace of a program is to use an  
03.external debugger such as gdb. However, sometimes it is useful to  
04.obtain a backtrace programmatically from within a program, e.g., for the  
05.purposes of logging or diagnostics.  
06.   
07.The header file execinfo.h declares three functions that obtain and  
08.manipulate backtraces of the current thread.  

4: 實現步驟

4.1 在你的工程中添加如下代碼:

#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
 
void dump(int signo)
{
    void *array[30];
    size_t size;
    char **strings;
    size_t i;
 
    size = backtrace (array, 30);
    strings = backtrace_symbols (array, size);
 
    fprintf (stderr,"Obtained %zd stack frames.nm", size);
 
    for (i = 0; i < size; i++)
        fprintf (stderr,"%sn", strings[i]);
 
    free (strings);
 
    exit(0);
}
 
Debug_Printf_FrameInfos()
{
    signal(SIGSEGV, &dump);
}


 

4.2 在mian函數開始位置處調用 Debug_Printf_FrameInfos() 函數
4.3 在編譯程序時 加上 -g 選項

5 定位出錯函數地址實例

這裏以 test.c 爲例,來查找出錯函數地址

#include <execinfo.h> 
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
 
void dump(int signo)
{
    void *array[30];
    size_t size;
    char **strings;
    size_t i;
 
    size = backtrace (array, 30);
    strings = backtrace_symbols (array, size);
 
    fprintf (stderr,"Obtained %zd stack frames.nm", size);
 
    for (i = 0; i <= size; i++)
        fprintf (stderr,"%s\n", strings[i]);
 
    free (strings);
    exit(0);
}
 
Debug_Printf_FrameInfos()
{
    signal(SIGSEGV, dump);
}
 
void func_c()
{
    * ((volatile char *) 0x0) = 0x999;
}
void func_b()
{
    func_c();
}
void func_a()
{
    func_b();
}
int main()
{
    Debug_Printf_FrameInfos();
    func_a();
    return 0;
}


 

該例程調用序列爲:
main() -> func_a() -> func_b() -> func_c() -> 出錯

5.1編譯程序:

# gcc -g test.c -o test

注:選項 -rdynamic 可用來通知鏈接器將所有符號添加到動態符號表中,如果你的鏈接器支持-rdynamic的話,
建議將其加上,即
# gcc -rdynamic -g test.c -o test

5.2 運行 test程序:

?View Code BASH
 #  ./test
Obtained 7 stack frames.nm./a.out [0x80484e3]
[0xb7f70420]
./a.out [0x804859d]
./a.out [0x80485a7]
./a.out [0x80485c4]
/lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe0) [0xb7e1e450]
./a.out [0x8048461]
Segmentation fault

如果編譯似加了-rdynamic選項 的話,將打印如下信息

?View Code BASH
# ./test 
Obtained 7 stack frames.nm./test(dump+0x1f) [0x80487c3]
[0xb7fbb420]
./test(func_b+0x8) [0x804887d]
./test(func_a+0x8) [0x8048887]
./test(main+0x1b) [0x80488a4]
/lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe0) [0xb7e69450]
./test [0x8048741]

打印信息比沒加-rdynamic的程序多出了一個函數名稱+偏移地址..(func_b+0×8)

5.3 使用objdump獲取程序所有符合

objdump -d ./test > tmp.txt

5.4 分析和查找

在tmp.txt 中查找0×80485ad的地址,你會發現如下信息:
08048595 :
8048595: 55 push %ebp
8048596: 89 e5 mov %esp,%ebp
8048598: e8 eb ff ff ff call 8048588 
804859d: 5d pop %ebp
804859e: c3

其中 804859d 是調用( call 8048588 )c函數後的地址,雖然並沒有直接定位到C函數,
通過彙編代碼,基本可以推出是在C函數出現問題了。(pop指令不會導致段錯誤的)

本文最早發佈在 我的CSDN blog上。

本文地址:
http://www.kgdb.info/linuxdev/backtrace_without_gdb/
版權所有 © 轉載時必須以鏈接形式註明作者和原始出處!

發佈了24 篇原創文章 · 獲贊 7 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章