一、段錯誤是什么
一句話來說,段錯誤是指訪問的內(nèi)存超出了系統(tǒng)給這個程序所設(shè)定的內(nèi)存空間,例如訪問了不存在的內(nèi)存地址先巴、訪問了系統(tǒng)保護(hù)的內(nèi)存地址亿絮、訪問了只讀的內(nèi)存地址等等情況仍律。
1拘悦、訪問不存在的內(nèi)存地址
#include<stdio.h>
#include<stdlib.h>
void main()
{
??int *ptr = NULL;
??*ptr = 0;
}
2诈豌、訪問系統(tǒng)保護(hù)的內(nèi)存地址
#include<stdio.h>
#include<stdlib.h>
void main()
{
??int *ptr = (int *)0;
??*ptr = 100;
}
3壶谒、訪問只讀的內(nèi)存地址
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void main()
{
??char *ptr = "test";
??strcpy(ptr, "TEST");
}
4云矫、棧溢出
#include<stdio.h>
#include<stdlib.h>
void main()
{
??main();
}
5、delete使用錯誤
delete只能刪除new得來的內(nèi)存汗菜,上面的p指向了新的內(nèi)存让禀,原先new來的內(nèi)存已找不到了,內(nèi)存泄漏陨界。
上面釋放了兩次new來的內(nèi)存巡揍。
下面是程序中的一個段錯誤實(shí)例:
上面的段錯誤是因?yàn)樵浇缌恕?shù)組的邊界沒有確定好菌瘪,此處是循環(huán)的數(shù)量錯了腮敌。(還有一次是指針數(shù)組忘記分配內(nèi)存了)
三、內(nèi)存問題
內(nèi)存問題始終是c++程序員需要去面對的問題俏扩,這也是c++語言的門檻較高的原因之一糜工。通常我們會犯的內(nèi)存問題大概有以下幾 種: 1.內(nèi)存重復(fù)釋放,出現(xiàn)double free時录淡,通常是由于這種情況所致捌木。 2.內(nèi)存泄露,分配的內(nèi)存忘了釋放赁咙。 3.內(nèi)存越界使用钮莲,使用了不該使用的內(nèi)存。 4.使用了無效指針彼水。 5.空指針崔拥,對一個空指針進(jìn)行操作。第四種情況凤覆,通常是指操作已釋放的對象链瓦,如: 1.已釋放對象,卻再次操作該指針?biāo)笇ο蟆?span> 2.多線程中某一動態(tài)分配的對象同時被兩個線程使用盯桦,一個線程釋放了該對象慈俯,而另一線程繼續(xù)對該對象進(jìn)行操作。 重點(diǎn)探討第三種情況拥峦,相對于另幾種情況贴膘,這可以稱得上是疑難雜癥了(第四種情況也可以理解成內(nèi)存越界使用)。
內(nèi)存越界使用略号,這樣的錯誤引起的問題存在極大的不確定性刑峡,有時大,有時小玄柠,有時可能不會對程序的運(yùn)行產(chǎn)生影響突梦,正是這種不易重現(xiàn)的錯誤,才是最致命的羽利,一旦出錯破壞性極大宫患。 什么原因會造成內(nèi)存越界使用呢?有以下幾種情況这弧,可供參考: 例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
當(dāng)這樣的代碼一旦運(yùn)行娃闲,錯誤就在所難免,會帶來的后果也是不確定的当宴,通承蟮酰可能會造成如下后果: 1.破壞了堆中的內(nèi)存分配信息數(shù)據(jù),特別是動態(tài)分配的內(nèi)存塊的內(nèi)存信息數(shù)據(jù)户矢,因?yàn)椴僮飨到y(tǒng)在分配和釋放內(nèi)存塊時需要訪問該數(shù)據(jù)玲献,一旦該數(shù)據(jù)被破壞,以下的幾種情況都可能會出現(xiàn)梯浪。? ??? ??? *** glibcdetected *** free(): invalid pointer:
??? ??? *** glibcdetected *** malloc(): memory corruption:
??? ??? *** glibcdetected *** double free or corruption (out): 0x00000000005c18a0 ***
??? ??? *** glibcdetected *** corrupted double-linked list: 0x00000000005ab150***??? ????
2.破壞了程序自己的其他對象的內(nèi)存空間捌年,這種破壞會影響程序執(zhí)行的不正確性,當(dāng)然也會誘發(fā)coredump挂洛,如破壞了指針數(shù)據(jù)礼预。 3.破壞了空閑內(nèi)存塊,很幸運(yùn)虏劲,這樣不會產(chǎn)生什么問題托酸,但誰知道什么時候不幸會降臨呢褒颈? 通常,代碼錯誤被激發(fā)也是偶然的励堡,也就是說之前你的程序一直正常谷丸,可能由于你為類增加了兩個成員變量,或者改變了某一部分代碼应结,coredump就頻繁發(fā)生刨疼,而你增加的代碼絕不會有任何問題,這時你就應(yīng)該考慮是否是某些內(nèi)存被破壞了鹅龄。
四揩慕、錯誤排查 保持好的編碼習(xí)慣是杜絕錯誤的最好方式!排查的原則扮休,首先是保證能重現(xiàn)錯誤迎卤,根據(jù)錯誤估計(jì)可能的環(huán)節(jié),逐步裁減代碼玷坠,縮小排查空間止吐。 1、檢查所有的內(nèi)存操作函數(shù)侨糟,檢查內(nèi)存越界的可能碍扔。常用的內(nèi)存操作函數(shù): sprintf strcpy strcat? memcpy memmove memset等,如果有用到自己編寫的動態(tài)庫的情況秕重,要確保動態(tài)庫的編譯與程序編譯的環(huán)境一致不同。
2、捕獲段錯誤溶耘,針對段錯誤的信號調(diào)用?sigaction?注冊一個處理函數(shù)就可以了二拐。發(fā)生段錯誤時的函數(shù)調(diào)用關(guān)系體現(xiàn)在棧幀上,可以通過在信號處理函數(shù)中調(diào)用?backstrace?來獲取棧幀信息凳兵。先輸出堆棧信息百新,接下來,分析出錯時的函數(shù)調(diào)用路徑庐扫。
在glibc頭文件"execinfo.h"中聲明了三個函數(shù)用于獲取當(dāng)前線程的函數(shù)調(diào)用堆棧饭望。
int?backtrace(void?**buffer,int?size)?
該函數(shù)用于獲取當(dāng)前線程的調(diào)用堆棧,獲取的信息將會被存放在buffer中,它是一個指針列表形庭。參數(shù) size 用來指定buffer中可以保存多少個void*元素铅辞。函數(shù)返回值是實(shí)際獲取的指針個數(shù),最大不超過size大小
在buffer中的指針實(shí)際是從堆棧中獲取的返回地址,每一個堆棧框架有一個返回地址
注意:某些編譯器的優(yōu)化選項(xiàng)對獲取正確的調(diào)用堆棧有干擾萨醒,另外內(nèi)聯(lián)函數(shù)沒有堆椪迳海框架;刪除框架指針也會導(dǎo)致無法正確解析堆棧內(nèi)容。
char?**?backtrace_symbols?(void?*const?*buffer,?int?size)?
backtrace_symbols將從backtrace函數(shù)獲取的信息轉(zhuǎn)化為一個字符串?dāng)?shù)組富纸,參數(shù)buffer應(yīng)該是從backtrace函數(shù)獲取的指針數(shù)組囤踩,size是該數(shù)組中的元素個數(shù)(backtrace的返回值)旨椒。
函數(shù)返回值是一個指向字符串?dāng)?shù)組的指針,它的大小同buffer相同堵漱。每個字符串包含了一個相對于buffer中對應(yīng)元素的可打印信息钩乍,它包括函數(shù)名,函數(shù)的偏移地址和實(shí)際的返回地址怔锌。
#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是用查看目標(biāo)文件或者可執(zhí)行的目標(biāo)文件的構(gòu)成的GCC工具。
objdump -x obj 以某種分類信息的形式把目標(biāo)文件的數(shù)據(jù)組織(被分為幾大塊)輸出 <可查到該文件的所有動態(tài)庫>??? objdump -t obj 輸出目標(biāo)文件的符號表()
objdump -h obj 輸出目標(biāo)文件的所有段概括()
objdump -j .text/.data -S obj 輸出指定段的信息变过,大概就是反匯編源代碼把
objdump -S obj C語言與匯編語言同時顯示
或者使用下面的命令輸出具體的行數(shù):
3埃元、不在用戶自己編寫的函數(shù)內(nèi)的錯誤查找
動態(tài)鏈接庫無非就是編譯后的代碼,里面有一些基本的段媚狰、符號信息岛杀。如果出錯代碼行在動態(tài)鏈接庫內(nèi),那必然可以從動態(tài)鏈接庫內(nèi)找到出錯時的代碼行號崭孤。
因?yàn)檫M(jìn)程掛掉時輸出的地址类嗤,和動態(tài)鏈接庫文件內(nèi)的靜態(tài)偏移地址根本就不是一回事。所以我們需要知道出錯時辨宠,所輸出的代碼地址與動態(tài)鏈接庫偏移地址之間的關(guān)系遗锣。
事實(shí)上,每一個進(jìn)程都對應(yīng)了一個 /proc/pid 目錄嗤形,下面記載了諸多與該進(jìn)程相關(guān)的信息精偿,其中有一個maps文件,里面記錄了各個動態(tài)鏈接庫的加載地址赋兵。我們只需要根據(jù)所得到的出錯地址笔咽,以及這個maps文件,就可以得出具體是哪一個庫霹期,相應(yīng)的偏移地址是多少叶组。
知道了對應(yīng)的動態(tài)鏈接庫和偏移地址后,我們進(jìn)一步用 addr2line 將這個偏移地址翻譯一下就可以了历造。
(可以在程序中加入輸出語句或斷點(diǎn)甩十,因?yàn)槌霈F(xiàn)段錯誤的時候就不會繼續(xù)執(zhí)行了)
dmesg可以在應(yīng)用程序crash掉時,顯示內(nèi)核中保存的相關(guān)信息吭产≡嫜酰可通過dmesg命令可以查看發(fā)生段錯誤的程序名稱、引起段錯誤發(fā)生的內(nèi)存地址垮刹、指令指針地址达吞、堆棧指針地址、錯誤代碼荒典、錯誤原因等酪劫。
使用ldd命令查看二進(jìn)制程序的共享鏈接庫依賴吞鸭,包括庫的名稱、起始地址覆糟,這樣可以確定段錯誤到底是發(fā)生在了自己的程序中還是依賴的共享庫中刻剥。
4、使用cout輸出信息
這個是看似最簡單但往往很多情況下十分有效的調(diào)試方式滩字,也許可以說是程序員用的最多的調(diào)試方式造虏。簡單來說,就是在程序的重要代碼附近加上像cout這類輸出信息麦箍,這樣可以跟蹤并打印出段錯誤在代碼中可能出現(xiàn)的位置漓藕。
為了方便使用這種方法,可以使用條件編譯指令#ifdefDEBUG和#endif把printf函數(shù)包起來挟裂。這樣在程序編譯時享钞,如果加上-DDEBUG參數(shù)就能查看調(diào)試信息;否則不加該參數(shù)就不會顯示調(diào)試信息诀蓉。
5栗竖、catchsegv命令
catchsegv命令專門用來撲獲段錯誤,它通過動態(tài)加載器(ld-linux.so)的預(yù)加載機(jī)制(PRELOAD)把一個事先寫好的庫(/lib/libSegFault.so)加載上渠啤,用于捕捉斷錯誤的出錯信息狐肢。
五、一些注意事項(xiàng)
1沥曹、出現(xiàn)段錯誤時处坪,首先應(yīng)該想到段錯誤的定義,從它出發(fā)考慮引發(fā)錯誤的原因架专。
2同窘、在使用指針時,定義了指針后記得初始化指針部脚,在使用的時候記得判斷是否為NULL想邦。
3、在使用數(shù)組時委刘,注意數(shù)組是否被初始化丧没,數(shù)組下標(biāo)是否越界,數(shù)組元素是否存在等锡移。
4呕童、在訪問變量時,注意變量所占地址空間是否已經(jīng)被程序釋放掉淆珊。
5夺饲、在處理變量時,注意變量的格式控制是否合理等。
本文使用 文章同步助手 同步