from:http://blog.csdn.net/adaptiver/article/details/37656507linux
1、 段錯誤緣由分析
1 使用非法的指針,包括使用未經初始化及已經釋放的指針(指針使用以前和釋放以後置爲NULL)
2 內存讀/寫越界。包括數組訪問越界,或在使用一些寫內存的函數時,長度指定不正確或者這些函數自己不能指定長度,典型的函數有strcpy(strncpy),sprintf
(snprint)等等。
3 對於C++對象,請經過相應類的接口來去內存進行操做,禁止經過其返回的指針對內存進行寫操做,典型的如string類的data()和c_str()兩個接口。
4 函數不要返回其中局部對象的引用或地址,當函數返回時,函數棧彈出,局部對象的地址將失效,改寫或讀這些地址都會形成未知的後果。
5 避免在棧中定義過大的數組,不然可能致使進程的棧空間不足,此時也會出現段錯誤。
6 操做系統的相關限制,如:進程能夠分配的最大內存,進程能夠打開的最大文件描述符個數等,這些須要經過ulimit或setrlimit或sysctl來解除相關的限制。
7 多線程的程序,涉及到多個線程同時操做一塊內存時必須進行互斥,不然內存中的內存將不可預料
8 使用非線程安全的函數調用,例如 strerror 函數等
9 在有信號的環境中,使用不可重入函數調用,而這些函數內部會讀或寫某片內存區,當信號中斷時,內存寫操做將被打斷,而下次進入時將不避免的出錯。
10 跨進程傳遞某個地址
11 某些有特殊要求的系統調用,例如epool_wait,正常狀況下使用close關閉一個套接字後,epool會再也不返回這個socket上的事件,可是若是你使用dup或dup2操做,將
致使epool沒法進行移除操做。
2、 段錯誤緣由查找
1) 查看函數調用棧
在頭文件"execinfo.h"中聲明瞭三個函數用於獲取當前線程的函數調用堆棧
Function: int backtrace(void **buffer,int size)
該函數用與獲取當前線程的調用堆棧,獲取的信息將會被存放在buffer中,它是一個指針列表。參數 size 用來指定buffer中能夠保存多少個void* 元素。函數返回值是實際獲取的指針個數,最大不超過size大小。
在buffer中的指針實際是從堆棧中獲取的返回地址,每個堆棧框架有一個返回地址。
注意某些編譯器的優化選項對獲取正確的調用堆棧有干擾,另外內聯函數沒有堆棧框架;刪除框架指針也會使沒法正確解析堆棧內容。
Function: char ** backtrace_symbols (void *const *buffer, int size)
backtrace_symbols將從backtrace函數獲取的信息轉化爲一個字符串數組. 參數buffer應該是從backtrace函數獲取的數組指針,size是該數組中的元素個數(backtrace的返回值) 。
函數返回值是一個指向字符串數組的指針,它的大小同buffer相同.每一個字符串包含了一個相對於buffer中對應元素的可打印信息.它包括函數名,函數的偏移地址,和實際的返回地址。
如今,只有使用ELF二進制格式的程序和苦衷才能獲取函數名稱和偏移地址.在其餘系統,只有16進制的返回地址能被獲取.另外,你可能須要傳遞相應的標誌給連接器,以能支持函數名功能(好比,在使用GNU ld的系統中,你須要傳遞(-rdynamic))。
該函數的返回值是經過malloc函數申請的空間,所以調用這必須使用free函數來釋放指針。
注意:若是不能爲字符串獲取足夠的空間函數的返回值將會爲NULL
Function:void backtrace_symbols_fd (void *const *buffer, int size, int fd)
backtrace_symbols_fd與backtrace_symbols 函數具備相同的功能,不一樣的是它不會給調用者返回字符串數組,而是將結果寫入文件描述符爲fd的文件中,每一個函數對應一行.它不須要調用malloc函數,所以適用於有可能調用該函數會失敗的狀況。
下面的例子顯示了這三個函數的用法
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
/* Obtain a backtrace and print it to stdout. */
void print_trace (void)
{
void *array[10];
size_t size;
char **strings;
size_t i;
size = backtrace (array, 10);
strings = backtrace_symbols (array, size);
printf ("Obtained %zd stack frames.\n", size);
for (i = 0; i < size; i++)
{
printf ("%s\n", strings);
}
free (strings);
}
/* A dummy function to make the backtrace more interesting. */
void dummy_function (void)
{
print_trace ();
}
int main (void)
{
dummy_function ();
return 0;
}
備註:void *const *buffer -- buffer指向char類型的常量指針的指針(非常拗口)
2) 查看寄存器內容
要查看寄存器內容有兩個解決辦法:數組
A) 在內核裏面把這些寄存器打印出來;安全
圖一:段錯誤時內核執行路徑多線程
根據上圖,咱們只須要在__do_user_fault的時候把打印信息打開就能夠了,以下:架構
把app
#ifdef CONFIG_DEBUG_USER框架
if (user_debug & UDBG_SEGV) {socket
printk(KERN_DEBUG "%s: unhandled page fault (%d) at 0x%08lx, code 0x%03x\n",函數
tsk->comm, sig, addr, fsr);工具
show_pte(tsk->mm, addr);
show_regs(regs);
}
#endif
改爲
printk(KERN_DEBUG "%s: unhandled page fault (%d) at 0x%08lx, code 0x%03x\n",
tsk->comm, sig, addr, fsr);
show_pte(tsk->mm, addr);
show_regs(regs);
就能夠了;
裏面會打印出pc寄存器的值。
B) 在上層程序裏面把寄存器打印出來;
這個作法的主要思路就是先攔截SIGSEGV信號,而後在信號處理函數裏面打印信息:
信號攔截代碼以下:
static void catch_sigsegv()
{
struct sigaction action;
memset(&action, 0, sizeof(action));
action.sa_sigaction = sigsegv_handler;
action.sa_flags = SA_SIGINFO; // 注意這裏,flag 是 SA_SIGINFO,這樣信號處理函數就會多一些信息。
if(sigaction(SIGSEGV, &action, NULL) < 0){
perror("sigaction");
}
}
只須要在main函數裏面加入這個函數就能夠了,
main(…)
{
….
catch_sigsegv();
…
}
下面來看看這個處理函數sigsegv_handler是怎麼寫的,代碼以下:
#include <memory.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <ucontext.h>
#include <dlfcn.h>
static void sigsegv_handler(int signum, siginfo_t* info, void*ptr)
{
static const char *si_codes[3] = {"", "SEGV_MAPERR", "SEGV_ACCERR"};
int i;
ucontext_t *ucontext = (ucontext_t*)ptr;
void *bt[100];
char **strings;
printf("Segmentation Fault Trace:\n");
printf("info.si_signo = %d\n", signum);
printf("info.si_errno = %d\n", info->si_errno);
printf("info.si_code = %d (%s)\n", info->si_code, si_codes[info->si_code]);
printf("info.si_addr = %p\n", info->si_addr);
/*for arm*/
printf("the arm_fp 0x%3x\n",ucontext->uc_mcontext.arm_fp);
printf("the arm_ip 0x%3x\n",ucontext->uc_mcontext.arm_ip);
printf("the arm_sp 0x%3x\n",ucontext->uc_mcontext.arm_sp);
printf("the arm_lr 0x%3x\n",ucontext->uc_mcontext.arm_lr);
printf("the arm_pc 0x%3x\n",ucontext->uc_mcontext.arm_pc);
printf("the arm_cpsr 0x%3x\n",ucontext->uc_mcontext.arm_cpsr);
printf("the falut_address 0x%3x\n",ucontext->uc_mcontext.fault_address);
printf("Stack trace (non-dedicated):");
int sz = backtrace(bt, 20);
printf("the stack trace is %d\n",sz);
strings = backtrace_symbols(bt, sz);
for(i = 0; i < sz; ++i){
printf("%s\n", strings[i]);
}
_exit (-1);
}
測試代碼以下:
void test_segv()
{
char *i=0;
*i=10;
}
void cause_segv()
{
printf("this is the cause_segv\n");
test_segv();
}
int main(int argc,char **argv)
{
catch_sigsegv();
cause_segv();
return 0;
}
編譯方法:
gcc segment_trace.c -g –rdynamic –o segment_trace
執行:
./segment_trace
輸出以下:
this is the catch_sigsegv
Segmentation Fault Trace:
info.si_signo = 11
info.si_errno = 0
info.si_code = 1 (SEGV_MAPERR)
info.si_addr = (nil)
the arm_fp 0xb7f8a3d4
the arm_ip 0xb7f8a3d8
the arm_sp 0xb7f8a3c0
the arm_lr 0x8998
the arm_pc 0x8974
the arm_cpsr 0x60000010
the falut_address 0x 0
Stack trace (non-dedicated):the stack trace is 5
./segment_trace(backtrace_symbols+0x1c8) [0x8844]
/lib/libc.so.6(__default_rt_sa_restorer+0) [0xb5e22230]
./segment_trace(cause_segv+0x18) [0x8998]
./segment_trace(main+0x20) [0x89c0]
/lib/libc.so.6(__libc_start_main+0x108) [0xb5e0c10c]
C) 輸出信息分析
根據上面的輸出能夠看出一些端倪:
根據棧信息,能夠看出是在cause_segv裏面出了問題,可是最後一層棧信息是看不到的,另外須要根據pc寄存器的值來定位:
addr2line -f -e segment_trace 0x8974
test_segv
/home/wf/test/segment_trace.c:55
能夠看到說是在55行,一看:
恰好是
*i=10;
這一行,
並且能夠看出,函數名是test_segv,
因此基本上不須要打印棧信息,也能夠定位了。
也可使用 objdump 工具:
objdump -S -l -z -j .text segment_trace >1.txt
查看 0x8974 地址的代碼。
百度百科上關於段錯誤的資料。
A
segmentation
fault
(often
shortened
to
SIGSEGV
)
is
a
particular
error
condition
that
can
occur
during
the
operation
of
computer
software
.
A
segmentation
fault
occurs
when
a
program
attempts
to
access
a
memory
location
that
it
is
not
allowed
to
access,
or
attempts
to
access
a
memory
location
in
a
way
that
is
not
allowed
(for
example,
attempting
to
write
to
a
read-only
location,
or
to
overwrite
part
of
the
operating
system).
Segmentation
is
one
approach
to
memory
management
and
protection
in
the
operating
system
.
It
has
been
superseded
by
paging
for
most
purposes,
but
much
of
the
terminology
of
segmentation
is
still
used,
"segmentation
fault"
being
an
example.
Some
operating
systems
still
have
segmentation
at
some
logical
level
although
paging
is
used
as
the
main
memory
management
policy.
On
Unix-like
operating
systems,
a
process
that
accesses
an
invalid
memory
address
receives
the
SIGSEGV
signal
.
On
Microsoft
Windows
,
a
process
that
accesses
invalid
memory
receives
the
STATUS_ACCESS_VIOLATION
exception
.
上述文字沒有給出
SIGSEGV
的定義,僅僅說它是「計算機軟件操做過程當中的一種錯誤
狀況」。文字描述了
SIGSEGV
在什麼時候發生,即「當程序試圖訪問不被容許訪問的內存區域
(好比,嘗試寫一塊屬於操做系統的內存)
,或以錯誤的類型訪問內存區域(好比,嘗試寫
一塊只讀內存)
。
這個描述是準確的。
爲了加深理解,
咱們再更加詳細的歸納一下
SIGSEGV
。
�
SIGSEGV
是在訪問內存時發生的錯誤,它屬於內存管理的範疇
�
SIGSEGV
是一個用戶態的概念,是操做系統在用戶態程序錯誤訪問內存時所作出的處
理。
�
當用戶態程序訪問(訪問表示讀、寫或執行)不容許訪問的內存時,產生
SIGSEGV
。
�
當用戶態程序以錯誤的方式訪問容許訪問的內存時,產生
SIGSEGV
。
從用戶態程序開發的角度,
咱們並不須要理解操做系統複雜的內存管理機制,
這是和硬
件平臺相關的。可是,瞭解內核發送
SIGSEGV
信號的流程,對咱們理解
SIGSEGV
是頗有
幫
助
的
。
在
《
Understanding
Linux
Kernel
Edition
3
》
和《
Understanding
the
Linux
Virtual
Memory
Manager
》相關章節都有一幅總圖對此描述,對比之下,筆者認爲
ULK
的圖更爲直觀。
Segmentation
Fault
in
Linux
6
Y
e
s
地
址
是
否
屬
於
進
程
地
址
空
間
N
o
訪
問
類
型
(讀、寫、執
行
)
是否符合該內存區域類型
該
異
常
(指
page fault
)
是
否
發
生
在
用
戶
態
Y
e
s
N
o
訪
問
合
法
,分
配
新
內
存
非
法
訪
問
,發
送
segfault
信
號
給
用
戶
態
程
序
Y
e
s
內
核
錯
誤
N
o
Page fault
異
常
圖
1.
SIGSEGV
Overview
圖
1
紅色部分展現了內核發送
SIGSEGV
信號給用戶態程序的整體流程。當用戶態
程序訪問一個會引起
SIGSEGV
的地址時,硬件首先產生一個
page
fault
,即「缺頁異常」。
在內核的
page
fault
處理函數中,
首先判斷該地址是否屬於用戶態程序的地址空間
[*]
。
以
Intel
的
32bit
IA32
架構的
CPU
爲例,
用戶態程序的地址空間爲
[0
,
3G]
,
內核地址空間爲
[3G
,
4G]
。
若是該地址屬於用戶態地址空間,
檢查訪問的類型是否和該內存區域的類型是否匹配,
不匹
配,則發送
SIGSEGV
信號;若是該地址不屬於用戶態地址空間,檢查訪問該地址的操做是
否發生在用戶態,若是是,發送
SIGSEGV
信號。
[*]
這裏的用戶態程序地址空間,特指程序能夠訪問的地址空間範圍。若是廣義的說,
一個進程的地址空間應該包括內核空間部分,只是它不能訪問而已。
圖
2
更爲詳細的描繪了內核發送
SIGSEGV
信號的流程。在這裏咱們再也不累述圖中
流程,在後面章節的例子中,筆者會結合實際,描述具體的流程