今天的主題是:訪問了空指針一定會出現段錯誤(segmentation fault)嗎?
看下面代碼:
test.c
#include <stdio.h>
int main()
{
int *p = NULL;
*p = 1;
return 0;
}
在Linux裏面寫了這麼多代碼,大家應該很清楚,上面的代碼會出現段錯誤,因爲訪問了空指針。
#gcc test.c -o test
#./test
Segmentation fault
#
現象和我們想的一樣。在Linux中,如果訪問了空指針或者野指針,都會出現段錯誤(segmentation fault),這也是操作系統出於對內存的保護。 發生段錯誤的時候,系統會發出11信號(SIGSEGV),收到這個信號,程序就掛了。我們來驗證一下:
circle.c
#include <stdio.h>
int main()
{
while (1);
return 0;
}
就是一個簡單的死循環程序,運行起來,再打開一個終端,先查詢進程的pid,再用kill發送一個SIGSEGV信號:
再來看下運行程序的結果:
清楚了沒,現象和程序出現段錯誤的現象一樣。
衆所周知,在Linux裏面,進程對信號的處理有三種方法,捕獲、忽略、缺省行爲。一般情況下,你什麼都不做,行爲就是缺省的,比如CTRL+C就會讓進程死。
但是下面的進程,CTRL+C之後不僅死不了,還能打印東西:
signal.c
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
printf("receive signal %d\n", sig);
}
int main()
{
signal(SIGINT, handler);
while (1);
return 0;
}
編譯運行,然後不停的按CTRL+C:
原因很簡單,進程的2號信號SIGINT被我們捕獲了。這個從ps命令裏面也可以看出(留意CAUGHT這列):
當然,我們也可以選擇忽略它:
#include <stdio.h>
#include <signal.h>
int main()
{
signal(SIGINT, SIG_IGN);
while (1);
return 0;
}
不管怎麼按CTRL+C,都沒用:
我們還是可以ps一下(留意IGNORED這列):
在這麼多信號裏面,除了極少數信號類似SIGKILL、SIGSTOP這種不能捕獲和忽略以外,其他的都是可以被我們拿來把玩的,當然也包括看起來牛逼轟轟的SIGSEGV段錯誤(編號11)的信號。
我必須反覆強調一點,當你用CTRL+C等對應的信號去殺死一個進程A的時候,從來都不是你殺死了A,而是你給A發了個信號,而A在響應這個信號的時候,其對應行爲是進程exit。所以,不是你殺死了進程A,而是你發個信號,“通知”目標進程A去死。所以你不能用人類世界的殺死來理解Linux的殺死。Linux的邏輯類似:你對進程A說:“你去死吧”(發個可以讓它死的信號),A看到這個pending的信號後,啥廢話都不說,立即悶聲死翹翹!所以,你只要改變A的響應行爲,就可以選擇不死。
下面問題就簡單了,我們利用setjmp和longjmp來實現段錯誤後繼續執行:
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
jmp_buf env;
void handler(int sig)
{
longjmp(env, 1); //跳轉到上一次保存的現場
}
int main()
{
int ret = setjmp(env); //保存執行現場,返回0
if (0 == ret)
{
signal(SIGSEGV, handler);
printf("製造段錯誤...\n");
int *p = NULL;
*p = 1;
}
else
{
printf("段錯誤後!\n");
}
return 0;
}
setjmp這個函數的原理是:
調用這個函數的時候,它會保存執行現場,並返回0;之後調用longjmp,可恢復到setjmp保存的現場,setjmp再次返回,不過這次返回的是longjmp()的第二個參數。看下面這個圖:
如果我們對代碼稍加修改,刪除一行:
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
jmp_buf env;
void handler(int sig)
{
longjmp(env, 1); //跳轉到上一次保存的現場
}
int main()
{
int ret = setjmp(env); //保存執行現場,返回0
if (0 == ret)
{
//signal(SIGSEGV, handler);
printf("製造段錯誤...\n");
int *p = NULL;
*p = 1;
}
else
{
printf("段錯誤後!\n");
}
return 0;
}
現象又是我們熟悉的:
看明白沒,段錯誤其實只是操作系統爲了保護內存而發出的信號,進程收到信號缺省處理爲掛掉,所以要想避免段錯誤,忽略SIGSEGV就行。當然這絕對是下下策,用這個方法來解決段錯誤簡直無語,更重要的還是寫代碼的時候注意內存的使用,不要訪問不能訪問的內存。
更多文章、視頻、嵌入式學習資料,微信關注公衆號 『學益得智能硬件』