一、段錯(cuò)誤是什么
一句話來(lái)說(shuō),段錯(cuò)誤是指訪問(wèn)的內(nèi)存超出了系統(tǒng)給這個(gè)程序所設(shè)定的內(nèi)存空間,例如訪問(wèn)了不存在的內(nèi)存地址、訪問(wèn)了系統(tǒng)保護(hù)的內(nèi)存地址、訪問(wèn)了只讀的內(nèi)存地址等等情況。
1、訪問(wèn)不存在的內(nèi)存地址
#include<stdio.h>
#include<stdlib.h>
void main()
{
??int *ptr = NULL;
??*ptr = 0;
}
2、訪問(wèn)系統(tǒng)保護(hù)的內(nèi)存地址
#include<stdio.h>
#include<stdlib.h>
void main()
{
??int *ptr = (int *)0;
??*ptr = 100;
}
3、訪問(wèn)只讀的內(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使用錯(cuò)誤
delete只能刪除new得來(lái)的內(nèi)存,上面的p指向了新的內(nèi)存,原先new來(lái)的內(nèi)存已找不到了,內(nèi)存泄漏。
上面釋放了兩次new來(lái)的內(nèi)存。
下面是程序中的一個(gè)段錯(cuò)誤實(shí)例:

上面的段錯(cuò)誤是因?yàn)樵浇缌恕?shù)組的邊界沒(méi)有確定好,此處是循環(huán)的數(shù)量錯(cuò)了。(還有一次是指針數(shù)組忘記分配內(nèi)存了)
三、內(nèi)存問(wèn)題
內(nèi)存問(wèn)題始終是c++程序員需要去面對(duì)的問(wèn)題,這也是c++語(yǔ)言的門(mén)檻較高的原因之一。通常我們會(huì)犯的內(nèi)存問(wèn)題大概有以下幾 種: 1.內(nèi)存重復(fù)釋放,出現(xiàn)double free時(shí),通常是由于這種情況所致。 2.內(nèi)存泄露,分配的內(nèi)存忘了釋放。 3.內(nèi)存越界使用,使用了不該使用的內(nèi)存。 4.使用了無(wú)效指針。 5.空指針,對(duì)一個(gè)空指針進(jìn)行操作。第四種情況,通常是指操作已釋放的對(duì)象,如: 1.已釋放對(duì)象,卻再次操作該指針?biāo)笇?duì)象。 2.多線程中某一動(dòng)態(tài)分配的對(duì)象同時(shí)被兩個(gè)線程使用,一個(gè)線程釋放了該對(duì)象,而另一線程繼續(xù)對(duì)該對(duì)象進(jìn)行操作。 重點(diǎn)探討第三種情況,相對(duì)于另幾種情況,這可以稱(chēng)得上是疑難雜癥了(第四種情況也可以理解成內(nèi)存越界使用)。
內(nèi)存越界使用,這樣的錯(cuò)誤引起的問(wèn)題存在極大的不確定性,有時(shí)大,有時(shí)小,有時(shí)可能不會(huì)對(duì)程序的運(yùn)行產(chǎn)生影響,正是這種不易重現(xiàn)的錯(cuò)誤,才是最致命的,一旦出錯(cuò)破壞性極大。 什么原因會(huì)造成內(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)行,錯(cuò)誤就在所難免,會(huì)帶來(lái)的后果也是不確定的,通??赡軙?huì)造成如下后果: 1.破壞了堆中的內(nèi)存分配信息數(shù)據(jù),特別是動(dòng)態(tài)分配的內(nèi)存塊的內(nèi)存信息數(shù)據(jù),因?yàn)椴僮飨到y(tǒng)在分配和釋放內(nèi)存塊時(shí)需要訪問(wèn)該數(shù)據(jù),一旦該數(shù)據(jù)被破壞,以下的幾種情況都可能會(huì)出現(xiàn)。? ??? ??? *** glibcdetected *** free(): invalid pointer:
??? ??? *** glibcdetected *** malloc(): memory corruption:
??? ??? *** glibcdetected *** double free or corruption (out): 0x00000000005c18a0 ***
??? ??? *** glibcdetected *** corrupted double-linked list: 0x00000000005ab150***??? ????
2.破壞了程序自己的其他對(duì)象的內(nèi)存空間,這種破壞會(huì)影響程序執(zhí)行的不正確性,當(dāng)然也會(huì)誘發(fā)coredump,如破壞了指針數(shù)據(jù)。 3.破壞了空閑內(nèi)存塊,很幸運(yùn),這樣不會(huì)產(chǎn)生什么問(wèn)題,但誰(shuí)知道什么時(shí)候不幸會(huì)降臨呢? 通常,代碼錯(cuò)誤被激發(fā)也是偶然的,也就是說(shuō)之前你的程序一直正常,可能由于你為類(lèi)增加了兩個(gè)成員變量,或者改變了某一部分代碼,coredump就頻繁發(fā)生,而你增加的代碼絕不會(huì)有任何問(wèn)題,這時(shí)你就應(yīng)該考慮是否是某些內(nèi)存被破壞了。
四、錯(cuò)誤排查 保持好的編碼習(xí)慣是杜絕錯(cuò)誤的最好方式!排查的原則,首先是保證能重現(xiàn)錯(cuò)誤,根據(jù)錯(cuò)誤估計(jì)可能的環(huán)節(jié),逐步裁減代碼,縮小排查空間。 1、檢查所有的內(nèi)存操作函數(shù),檢查內(nèi)存越界的可能。常用的內(nèi)存操作函數(shù): sprintf strcpy strcat? memcpy memmove memset等,如果有用到自己編寫(xiě)的動(dòng)態(tài)庫(kù)的情況,要確保動(dòng)態(tài)庫(kù)的編譯與程序編譯的環(huán)境一致。
2、捕獲段錯(cuò)誤,針對(duì)段錯(cuò)誤的信號(hào)調(diào)用?sigaction?注冊(cè)一個(gè)處理函數(shù)就可以了。發(fā)生段錯(cuò)誤時(shí)的函數(shù)調(diào)用關(guān)系體現(xiàn)在棧幀上,可以通過(guò)在信號(hào)處理函數(shù)中調(diào)用?backstrace?來(lái)獲取棧幀信息。先輸出堆棧信息,接下來(lái),分析出錯(cuò)時(shí)的函數(shù)調(diào)用路徑。
在glibc頭文件"execinfo.h"中聲明了三個(gè)函數(shù)用于獲取當(dāng)前線程的函數(shù)調(diào)用堆棧。
int?backtrace(void?**buffer,int?size)?
該函數(shù)用于獲取當(dāng)前線程的調(diào)用堆棧,獲取的信息將會(huì)被存放在buffer中,它是一個(gè)指針列表。參數(shù) size 用來(lái)指定buffer中可以保存多少個(gè)void*元素。函數(shù)返回值是實(shí)際獲取的指針個(gè)數(shù),最大不超過(guò)size大小
在buffer中的指針實(shí)際是從堆棧中獲取的返回地址,每一個(gè)堆??蚣苡幸粋€(gè)返回地址
注意:某些編譯器的優(yōu)化選項(xiàng)對(duì)獲取正確的調(diào)用堆棧有干擾,另外內(nèi)聯(lián)函數(shù)沒(méi)有堆??蚣?/span>;刪除框架指針也會(huì)導(dǎo)致無(wú)法正確解析堆棧內(nèi)容。
char?**?backtrace_symbols?(void?*const?*buffer,?int?size)?
backtrace_symbols將從backtrace函數(shù)獲取的信息轉(zhuǎn)化為一個(gè)字符串?dāng)?shù)組,參數(shù)buffer應(yīng)該是從backtrace函數(shù)獲取的指針數(shù)組,size是該數(shù)組中的元素個(gè)數(shù)(backtrace的返回值)。
函數(shù)返回值是一個(gè)指向字符串?dāng)?shù)組的指針,它的大小同buffer相同。每個(gè)字符串包含了一個(gè)相對(duì)于buffer中對(duì)應(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 以某種分類(lèi)信息的形式把目標(biāo)文件的數(shù)據(jù)組織(被分為幾大塊)輸出 <可查到該文件的所有動(dòng)態(tài)庫(kù)>??? objdump -t obj 輸出目標(biāo)文件的符號(hào)表()
objdump -h obj 輸出目標(biāo)文件的所有段概括()
objdump -j .text/.data -S obj 輸出指定段的信息,大概就是反匯編源代碼把
objdump -S obj C語(yǔ)言與匯編語(yǔ)言同時(shí)顯示
或者使用下面的命令輸出具體的行數(shù):
3、不在用戶(hù)自己編寫(xiě)的函數(shù)內(nèi)的錯(cuò)誤查找
動(dòng)態(tài)鏈接庫(kù)無(wú)非就是編譯后的代碼,里面有一些基本的段、符號(hào)信息。如果出錯(cuò)代碼行在動(dòng)態(tài)鏈接庫(kù)內(nèi),那必然可以從動(dòng)態(tài)鏈接庫(kù)內(nèi)找到出錯(cuò)時(shí)的代碼行號(hào)。
因?yàn)檫M(jìn)程掛掉時(shí)輸出的地址,和動(dòng)態(tài)鏈接庫(kù)文件內(nèi)的靜態(tài)偏移地址根本就不是一回事。所以我們需要知道出錯(cuò)時(shí),所輸出的代碼地址與動(dòng)態(tài)鏈接庫(kù)偏移地址之間的關(guān)系。
事實(shí)上,每一個(gè)進(jìn)程都對(duì)應(yīng)了一個(gè) /proc/pid 目錄,下面記載了諸多與該進(jìn)程相關(guān)的信息,其中有一個(gè)maps文件,里面記錄了各個(gè)動(dòng)態(tài)鏈接庫(kù)的加載地址。我們只需要根據(jù)所得到的出錯(cuò)地址,以及這個(gè)maps文件,就可以得出具體是哪一個(gè)庫(kù),相應(yīng)的偏移地址是多少。
知道了對(duì)應(yīng)的動(dòng)態(tài)鏈接庫(kù)和偏移地址后,我們進(jìn)一步用 addr2line 將這個(gè)偏移地址翻譯一下就可以了。
(可以在程序中加入輸出語(yǔ)句或斷點(diǎn),因?yàn)槌霈F(xiàn)段錯(cuò)誤的時(shí)候就不會(huì)繼續(xù)執(zhí)行了)
dmesg可以在應(yīng)用程序crash掉時(shí),顯示內(nèi)核中保存的相關(guān)信息。可通過(guò)dmesg命令可以查看發(fā)生段錯(cuò)誤的程序名稱(chēng)、引起段錯(cuò)誤發(fā)生的內(nèi)存地址、指令指針地址、堆棧指針地址、錯(cuò)誤代碼、錯(cuò)誤原因等。
使用ldd命令查看二進(jìn)制程序的共享鏈接庫(kù)依賴(lài),包括庫(kù)的名稱(chēng)、起始地址,這樣可以確定段錯(cuò)誤到底是發(fā)生在了自己的程序中還是依賴(lài)的共享庫(kù)中。
4、使用cout輸出信息
這個(gè)是看似最簡(jiǎn)單但往往很多情況下十分有效的調(diào)試方式,也許可以說(shuō)是程序員用的最多的調(diào)試方式。簡(jiǎn)單來(lái)說(shuō),就是在程序的重要代碼附近加上像cout這類(lèi)輸出信息,這樣可以跟蹤并打印出段錯(cuò)誤在代碼中可能出現(xiàn)的位置。
為了方便使用這種方法,可以使用條件編譯指令#ifdefDEBUG和#endif把printf函數(shù)包起來(lái)。這樣在程序編譯時(shí),如果加上-DDEBUG參數(shù)就能查看調(diào)試信息;否則不加該參數(shù)就不會(huì)顯示調(diào)試信息。
5、catchsegv命令
catchsegv命令專(zhuān)門(mén)用來(lái)?yè)浍@段錯(cuò)誤,它通過(guò)動(dòng)態(tài)加載器(ld-linux.so)的預(yù)加載機(jī)制(PRELOAD)把一個(gè)事先寫(xiě)好的庫(kù)(/lib/libSegFault.so)加載上,用于捕捉斷錯(cuò)誤的出錯(cuò)信息。
五、一些注意事項(xiàng)
1、出現(xiàn)段錯(cuò)誤時(shí),首先應(yīng)該想到段錯(cuò)誤的定義,從它出發(fā)考慮引發(fā)錯(cuò)誤的原因。
2、在使用指針時(shí),定義了指針后記得初始化指針,在使用的時(shí)候記得判斷是否為NULL。
3、在使用數(shù)組時(shí),注意數(shù)組是否被初始化,數(shù)組下標(biāo)是否越界,數(shù)組元素是否存在等。
4、在訪問(wèn)變量時(shí),注意變量所占地址空間是否已經(jīng)被程序釋放掉。
5、在處理變量時(shí),注意變量的格式控制是否合理等。
本文使用 文章同步助手 同步







