讓段錯誤不再是一個錯誤

今天的主題是:訪問了空指針一定會出現段錯誤(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就行。當然這絕對是下下策,用這個方法來解決段錯誤簡直無語,更重要的還是寫代碼的時候注意內存的使用,不要訪問不能訪問的內存。

更多文章、視頻、嵌入式學習資料,微信關注公衆號 『學益得智能硬件』

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章