段錯誤總結

https://blog.csdn.net/e_road_by_u/article/details/61415732linux

1、段錯誤是什麼c++

一句話來講,段錯誤是指訪問的內存超出了系統給這個程序所設定的內存空間,例如訪問了不存在的內存地址、訪問了系統保護的內存地址、訪問了只讀的內存地址等等狀況。程序員

2、段錯誤產生的緣由
一、訪問不存在的內存地址
#include<stdio.h>
#include<stdlib.h>
void main()
{
  int *ptr = NULL;
  *ptr = 0;
}
二、訪問系統保護的內存地址
#include<stdio.h>
#include<stdlib.h>
void main()
{
  int *ptr = (int *)0;
  *ptr = 100;
}
三、訪問只讀的內存地址
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void main()
{
  char *ptr = "test";
  strcpy(ptr, "TEST");
}
四、棧溢出
#include<stdio.h>
#include<stdlib.h>
void main()
{
  main();
}
五、delete使用錯誤數組

delete只能刪除new得來的內存,上面的p指向了新的內存,原先new來的內存已找不到了,內存泄漏。多線程

上面釋放了兩次new來的內存。框架

下面是程序中的一個段錯誤實例:函數

上面的段錯誤是由於越界了。數組的邊界沒有肯定好,此處是循環的數量錯了。(還有一次是指針數組忘記分配內存了)工具

3、內存問題優化

內存問題始終是c++程序員須要去面對的問題,這也是c++語言的門檻較高的緣由之一。一般咱們會犯的內存問題大概有如下幾種:
1.內存重複釋放,出現double free時,一般是因爲這種狀況所致。
2.內存泄露,分配的內存忘了釋放。
3.內存越界使用,使用了不應使用的內存。
4.使用了無效指針。
5.空指針,對一個空指針進行操做。this

第四種狀況,一般是指操做已釋放的對象,如:
1.已釋放對象,卻再次操做該指針所指對象。
2.多線程中某一動態分配的對象同時被兩個線程使用,一個線程釋放了該對象,而另外一線程繼續對該對象進行操做。
重點探討第三種狀況,相對於另幾種狀況,這能夠稱得上是疑難雜症了(第四種狀況也能夠理解成內存越界使用)。

內存越界使用,這樣的錯誤引發的問題存在極大的不肯定性,有時大,有時小,有時可能不會對程序的運行產生影響,正是這種不易重現的錯誤,纔是最致命的,一旦出錯破壞性極大。
什麼緣由會形成內存越界使用呢?有如下幾種狀況,可供參考:
例1:
        char buf[32]= {0};
        for(int i=0;i<n; i++)// n < 32 or n > 32
        {
           buf[i] = 'x';
        }
例2:
        char buf[32]= {0};
        string str ="this is a test sting !!!!";
        sprintf(buf,"this is a test buf!string:%s", str.c_str()); //out of buffer space
例3:
        string str ="this is a test string!!!!";
        char buf[16]= {0};
        strcpy(buf,str.c_str()); //out of buffer space
當這樣的代碼一旦運行,錯誤就在所不免,會帶來的後果也是不肯定的,一般可能會形成以下後果:
1.破壞了堆中的內存分配信息數據,特別是動態分配的內存塊的內存信息數據,由於操做系統在分配和釋放內存塊時須要訪問該數據,一旦該數據被破壞,如下的幾種狀況均可能會出現。 
        *** glibcdetected *** free(): invalid pointer:
        *** glibcdetected *** malloc(): memory corruption:
        *** glibcdetected *** double free or corruption (out): 0x00000000005c18a0 ***
        *** glibcdetected *** corrupted double-linked list: 0x00000000005ab150***        
2.破壞了程序本身的其餘對象的內存空間,這種破壞會影響程序執行的不正確性,固然也會誘發coredump,如破壞了指針數據。
3.破壞了空閒內存塊,很幸運,這樣不會產生什麼問題,但誰知道何時不幸會降臨呢?
一般,代碼錯誤被激發也是偶然的,也就是說以前你的程序一直正常,可能因爲你爲類增長了兩個成員變量,或者改變了某一部分代碼,coredump就頻繁發生,而你增長的代碼毫不會有任何問題,這時你就應該考慮是不是某些內存被破壞了。
4、錯誤排查
保持好的編碼習慣是杜絕錯誤的最好方式!排查的原則,首先是保證能重現錯誤,根據錯誤估計可能的環節,逐步裁減代碼,縮小排查空間。
一、檢查全部的內存操做函數,檢查內存越界的可能。經常使用的內存操做函數:
sprintf strcpy strcat  memcpy memmove memset等,若是有用到本身編寫的動態庫的狀況,要確保動態庫的編譯與程序編譯的環境一致。

二、捕獲段錯誤,針對段錯誤的信號調用 sigaction 註冊一個處理函數就能夠了。發生段錯誤時的函數調用關係體如今棧幀上,能夠經過在信號處理函數中調用 backstrace 來獲取棧幀信息。先輸出堆棧信息,接下來,分析出錯時的函數調用路徑。

在glibc頭文件"execinfo.h"中聲明瞭三個函數用於獲取當前線程的函數調用堆棧。

int backtrace(void **buffer,int size) 

該函數用於獲取當前線程的調用堆棧,獲取的信息將會被存放在buffer中,它是一個指針列表。參數 size 用來指定buffer中能夠保存多少個void*元素。函數返回值是實際獲取的指針個數,最大不超過size大小

在buffer中的指針實際是從堆棧中獲取的返回地址,每個堆棧框架有一個返回地址

注意:某些編譯器的優化選項對獲取正確的調用堆棧有干擾,另外內聯函數沒有堆棧框架;刪除框架指針也會致使沒法正確解析堆棧內容。


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

backtrace_symbols將從backtrace函數獲取的信息轉化爲一個字符串數組,參數buffer應該是從backtrace函數獲取的指針數組,size是該數組中的元素個數(backtrace的返回值)。

函數返回值是一個指向字符串數組的指針,它的大小同buffer相同。每一個字符串包含了一個相對於buffer中對應元素的可打印信息,它包括函數名,函數的偏移地址和實際的返回地址。


#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <execinfo.h>
#include <signal.h>

void dump(int signo)
{
     void *buffer[30] = {0};
     size_t size;
     char **strings = NULL;
     size_t i = 0;

     size = backtrace(buffer, 30);
     fprintf(stdout, "Obtained %zd stack frames.nm\n", size);
     strings = backtrace_symbols(buffer, size);
    if (strings == NULL)
     {
         perror("backtrace_symbols.");
         exit(EXIT_FAILURE);
     }
 
    for (i = 0; i < size; i++)
    {
        fprintf(stdout, "%s\n", strings[i]);
    }
   free(strings);
   strings = NULL;
   exit(0);
}

void func_c()
{
   *((volatile int *)0x0) = 0x9999;
}

void func_b()
{
    func_c();
}

void func_a()
{
    func_b();
}

int main(int argc, const char *argv[])
{
    if (signal(SIGSEGV, dump) == SIG_ERR)
       perror("can't catch SIGSEGV");
    func_a();
    return 0;
}


objdump是用查看目標文件或者可執行的目標文件的構成的GCC工具。

objdump -x obj 以某種分類信息的形式把目標文件的數據組織(被分爲幾大塊)輸出 <可查到該文件的全部動態庫>   
objdump -t obj 輸出目標文件的符號表()
objdump -h obj 輸出目標文件的全部段歸納()
objdump -j .text/.data -S obj 輸出指定段的信息,大概就是反彙編源代碼把
objdump -S obj C語言與彙編語言同時顯示

或者使用下面的命令輸出具體的行數:

三、不在用戶本身編寫的函數內的錯誤查找

動態連接庫無非就是編譯後的代碼,裏面有一些基本的段、符號信息。若是出錯代碼行在動態連接庫內,那必然能夠從動態連接庫內找到出錯時的代碼行號。

由於進程掛掉時輸出的地址,和動態連接庫文件內的靜態偏移地址根本就不是一回事。因此咱們須要知道出錯時,所輸出的代碼地址與動態連接庫偏移地址之間的關係。

事實上,每個進程都對應了一個 /proc/pid 目錄,下面記載了諸多與該進程相關的信息,其中有一個maps文件,裏面記錄了各個動態連接庫的加載地址。咱們只須要根據所獲得的出錯地址,以及這個maps文件,就能夠得出具體是哪個庫,相應的偏移地址是多少。

知道了對應的動態連接庫和偏移地址後,咱們進一步用 addr2line 將這個偏移地址翻譯一下就能夠了。

(能夠在程序中加入輸出語句或斷點,由於出現段錯誤的時候就不會繼續執行了)

dmesg能夠在應用程序crash掉時,顯示內核中保存的相關信息。可經過dmesg命令能夠查看發生段錯誤的程序名稱、引發段錯誤發生的內存地址、指令指針地址、堆棧指針地址、錯誤代碼、錯誤緣由等。

使用ldd命令查看二進制程序的共享連接庫依賴,包括庫的名稱、起始地址,這樣能夠肯定段錯誤究竟是發生在了本身的程序中仍是依賴的共享庫中。

四、使用cout輸出信息
這個是看似最簡單但每每不少狀況下十分有效的調試方式,也許能夠說是程序員用的最多的調試方式。簡單來講,就是在程序的重要代碼附近加上像cout這類輸出信息,這樣能夠跟蹤並打印出段錯誤在代碼中可能出現的位置。

爲了方便使用這種方法,可使用條件編譯指令#ifdefDEBUG和#endif把printf函數包起來。這樣在程序編譯時,若是加上-DDEBUG參數就能查看調試信息;不然不加該參數就不會顯示調試信息。

五、catchsegv命令

catchsegv命令專門用來撲獲段錯誤,它經過動態加載器(ld-linux.so)的預加載機制(PRELOAD)把一個事先寫好的庫(/lib/libSegFault.so)加載上,用於捕捉斷錯誤的出錯信息。

5、一些注意事項
一、出現段錯誤時,首先應該想到段錯誤的定義,從它出發考慮引起錯誤的緣由。

二、在使用指針時,定義了指針後記得初始化指針,在使用的時候記得判斷是否爲NULL。

三、在使用數組時,注意數組是否被初始化,數組下標是否越界,數組元素是否存在等。

四、在訪問變量時,注意變量所佔地址空間是否已經被程序釋放掉。

五、在處理變量時,注意變量的格式控制是否合理等。  

相關文章
相關標籤/搜索