信號量崩潰原因初探

信號量崩潰原因初探

1.SIGSEGV

1.什么是段錯誤(segmentation fault)

wiki上的是這么說的

A segmentation fault (often shortened to SIGSEGV) is a particular error condition that can occur during the operation of computer software. A segmentation fault occurs when a program attempts to access a memory location that it is not allowed to access, or attempts to access a memory location in a way that is not allowed (for example, attempting to write to a read-only location, or to overwrite part of the operating system).

Segmentation is one approach to memory management and protection in the operating system. It has been superseded by paging for most purposes, but much of the terminology of segmentation is still used, "segmentation fault" being an example. Some operating systems still have segmentation at some logical level although paging is used as the main memory management policy.

On Unix-like operating systems, a process that accesses an invalid memory address receives the SIGSEGV signal. On Microsoft Windows, a process that accesses invalid memory receives the STATUS_ACCESS_VIOLATION exception.

上述文字沒有給出SIGSEGV的定義,僅僅說它是“計算機軟件操作過程中的一種錯誤情況”鸯绿。文字描述了SIGSEGV在何時發(fā)生坝冕,即“當(dāng)程序試圖訪問不被允許訪問的內(nèi)存區(qū)域(比如斜做,嘗試寫一塊屬于操作系統(tǒng)的內(nèi)存)柄错,或以錯誤的類型訪問內(nèi)存區(qū)域(比如埃碱,嘗試寫一塊只讀內(nèi)存)嚎尤。這個描述是準(zhǔn)確的荔仁。為了加深理解,我們再更加詳細(xì)的概括一下SIGSEGV芽死。

  1. SIGSEGV是在訪問內(nèi)存時發(fā)生的錯誤乏梁,它屬于內(nèi)存管理的范疇
  2. SIGSEGV是一個用戶態(tài)的概念,是操作系統(tǒng)在用戶態(tài)程序錯誤訪問內(nèi)存時所做出的處理关贵。
  3. 當(dāng)用戶態(tài)程序訪問(訪問表示讀遇骑、寫或執(zhí)行)不允許訪問的內(nèi)存時,產(chǎn)生SIGSEGV揖曾。
  4. 當(dāng)用戶態(tài)程序以錯誤的方式訪問允許訪問的內(nèi)存時落萎,產(chǎn)生SIGSEGV。

從用戶態(tài)程序開發(fā)的角度炭剪,我們并不需要理解操作系統(tǒng)復(fù)雜的內(nèi)存管理機制模暗,這是和硬件平臺相關(guān)的。但是念祭,了解內(nèi)核發(fā)送SIGSEGV信號的流程兑宇,對我們理解SIGSEGV是很有幫助的。在《Understanding Linux Kernel Edition 3》和《Understanding the Linux Virtual Memory Manager》相關(guān)章節(jié)都有一幅總圖對此描述粱坤,對比之下隶糕,筆者認(rèn)為ULK的圖更為直觀。


圖片

圖1紅色部分展示了內(nèi)核發(fā)送SIGSEGV信號給用戶態(tài)程序的總體流程站玄。當(dāng)用戶態(tài)程序訪問一個會引發(fā)SIGSEGV的地址時枚驻,硬件首先產(chǎn)生一個page fault,即“缺頁異持昕酰”再登。在內(nèi)核的page fault處理函數(shù)中,首先判斷該地址是否屬于用戶態(tài)程序的地址空間[*]晾剖。以Intel的32bit IA32架構(gòu)的CPU為例锉矢,用戶態(tài)程序的地址空間為[0,3G]齿尽,內(nèi)核地址空間為[3G沽损,4G]。如果該地址屬于用戶態(tài)地址空間循头,檢查訪問的類型是否和該內(nèi)存區(qū)域的類型是否匹配绵估,不匹配炎疆,則發(fā)送SIGSEGV信號;如果該地址不屬于用戶態(tài)地址空間国裳,檢查訪問該地址的操作是否發(fā)生在用戶態(tài)形入,如果是,發(fā)送SIGSEGV信號缝左。

[*]這里的用戶態(tài)程序地址空間唯笙,特指程序可以訪問的地址空間范圍。如果廣義的說盒使,一個進程的地址空間應(yīng)該包括內(nèi)核空間部分崩掘,只是它不能訪問而已。

圖2更為詳細(xì)的描繪了內(nèi)核發(fā)送SIGSEGV信號的流程少办。在這里我們不再累述圖中流程苞慢,在后面章節(jié)的例子中,筆者會結(jié)合實際英妓,描述具體的流程挽放。


圖片

2.指針越界和SIGSEGV

經(jīng)常看到有帖子把兩者混淆蔓纠,而這兩者的關(guān)系也確實微妙辑畦。在此,我們把指針運算(加減)引起的越界腿倚、野指針纯出、空指針都?xì)w為指針越界。SIGSEGV在很多時候是由于指針越界引起的敷燎,但并不是所有的指針越界都會引發(fā)SIGSEGV暂筝。一個越界的指針,如果不解引用它硬贯,是不會引起SIGSEGV的焕襟。而即使解引用了一個越界的指針,也不一定會引起SIGSEGV饭豹。這聽上去讓人發(fā)瘋鸵赖,而實際情況確實如此。SIGSEGV涉及到操作系統(tǒng)拄衰、C庫它褪、編譯器、鏈接器各方面的內(nèi)容肾砂,我們以一些具體的例子來說明列赎。

2.1錯誤的訪問類型引起的SIGSEGV

#include <stdio.h>
#include <stdlib.h>
  
int main() {
    char* s = "hello world";
    s[1] = 'H';
}

這是最常見的一個例子。此例中镐确,”hello world”作為一個常量字符串包吝,在編譯后會被放在.rodata節(jié)(GCC),最后鏈接生成目標(biāo)程序時.rodata節(jié)會被合并到text segment與代碼段放在一起源葫,故其所處內(nèi)存區(qū)域是只讀的诗越。這就是錯誤的訪問類型引起的SIGSEGV。

其在圖2中的順序為:

1 -> 3 -> 4 -> 6 -> 8 -> 11 ->10

2.2訪問了不屬于進程地址空間的內(nèi)存

#include <stdio.h>
#include <stdlib.h>
int main() {
    int* p = (int*)0xC0000fff;
    *p = 10;
}

在這個例子中息堂,我們訪問了一個屬于內(nèi)核的地址(IA32嚷狞,32bit)。當(dāng)然荣堰,很少會有人這樣寫程序床未,但你的程序可能在不經(jīng)意的情況下做出這樣的行為(這個不經(jīng)意的行為在后面討論)。此例在圖2的流程:

1 -> 2 -> 11 -> 10

2.3訪問了不存在的內(nèi)存

最常見的情況不外乎解引用空指針了振坚,如:

#include <stdio.h>
#include <stdlib.h>
   
int main () {
    int *a = NULL;
    *a = 1;
}

在實際情況中薇搁,此例中的空指針可能指向用戶態(tài)地址空間,但其所指向的頁面實際不存在渡八。其產(chǎn)生SIGSEGV在圖2中的流程為:

1 -> 3 -> 4 -> 5 -> 11 ->10

2.4棧溢出了啃洋,有時SIGSEGV,有時卻啥都沒發(fā)生

這也是CU常見的一個月經(jīng)貼屎鳍。大部分C語言教材都會告訴你宏娄,當(dāng)從一個函數(shù)返回后,該函數(shù)棧上的內(nèi)容會被自動“釋放”逮壁》跫幔“釋放”給大多數(shù)初學(xué)者的印象是free(),似乎這塊內(nèi)存不存在了窥淆,于是當(dāng)他訪問這塊應(yīng)該不存在的內(nèi)存時十饥,發(fā)現(xiàn)一切都好,便陷入了深深的疑惑祖乳。

#include <stdio.h>
#include <stdlib.h>
int* foo() {
    int a = 10;
    return &a;
}
int main() {
    int* b;
    b = foo();
    printf ("%d\n", *b);
}

當(dāng)你編譯這個程序時逗堵,會看到“warning: function returns address of local variable”,GCC已經(jīng)在警告你棧溢的可能了眷昆。實際運行結(jié)果一切正常蜒秤。原因是操作系統(tǒng)通常以“頁”的粒度來管理內(nèi)存,Linux中典型的頁大小為4K亚斋,內(nèi)核為進程棧分配內(nèi)存也是以4K為粒度的作媚。故當(dāng)棧溢的幅度小于頁的大小時,不會產(chǎn)生SIGSEGV帅刊。那是否說棧溢出超過4K纸泡,就會產(chǎn)生SIGSEGV呢?看下面這個例子:

#include <stdio.h>
#include <stdlib.h>

char* foo() {
    char buf[8192];
    memset (buf, 0x55, sizeof(buf));
    return buf;
}
int main() {
    char* c;
   c = foo();
    printf ("%#x\n", c[5000]);
}

雖然我們的棧溢已經(jīng)超出了4K大小赖瞒,可運行仍然正常女揭。這是因為C教程中提到的“棧自動釋放”實際上是改變棧指針蚤假,而其指向的內(nèi)存,并不是在函數(shù)返回時就被回收了吧兔。在我們的例子中磷仰,所訪問的棧溢處內(nèi)存仍然存在。無效的棧內(nèi)存(即棧指針范圍外未被回收的棧內(nèi)存)是由操作系統(tǒng)在需要時回收的境蔼,這是無法預(yù)測的灶平,也就無法預(yù)測何時訪問非法的棧內(nèi)容會引發(fā)SIGSEGV。

好了箍土,在上面的例子中逢享,我們的棧溢例子,無論是大于一個頁尺寸還是小于一個頁尺寸吴藻,訪問的都是已分配而未回收的棧內(nèi)存瞒爬。那么訪問未分配的棧內(nèi)存,是否就一定會引發(fā)SIGSEGV呢调缨?答案是否定的疮鲫。

#include <stdio.h>
#include <stdlib.h>
int main() {
    char* c;
    c = (char*)&c – 8192 *2;
    *c = 'a';
    printf ("%c\n", *c);
}

在IA32平臺上,棧默認(rèn)是向下增長的弦叶,我們棧溢16K俊犯,訪問一塊未分配的棧區(qū)域(至少從我們的程序來看,此處是未分配的)伤哺。選用16K這個值燕侠,是要讓我們的溢出范圍足夠大,大過內(nèi)核為進程分配的初始棧大辛⒗颉(初始大小為4K或8K)绢彤。按理說,我們應(yīng)該看到期望的SIGSEGV蜓耻,但結(jié)果卻非如此茫舶,一切正常。
答案藏在內(nèi)核的page fault處理函數(shù)中:

if (error_code & PF_USER) {

              /*

               * Accessing the stack below %sp is always a bug.

               * The large cushion allows instructions like enter

               * and pusha to work.  ("enter $65535,$31" pushes

               * 32 pointers and then decrements %sp by 65535.)

               */

              if (address + 65536 + 32 * sizeof(unsigned long) < regs->sp)

                     goto bad_area;

       }

       if (expand_stack(vma, address))

              goto bad_area;

內(nèi)核為enter[*]這樣的指令留下了空間刹淌,從代碼來看饶氏,理論上棧溢小于64K左右都是沒問題的,棧會自動擴展有勾。令人迷惑的是疹启,筆者用下面這個例子來測試棧溢的閾值,得到的確是70K ~ 80K這個區(qū)間蔼卡,而不是預(yù)料中的65K ~ 66K喊崖。

[*]關(guān)于enter指令的詳細(xì)介紹,請參考《Intel(R) 64 and IA-32 Architectures Software Developer Manual Volume 1》6.5節(jié)“PROCEDURE CALLS FOR BLOCK-STRUCTURED LANGUAGES”

#include <stdio.h>
#include <stdlib.h>

#define GET_ESP(esp) do {   \
    asm volatile ("movl %%esp, %0\n\t" : "=m" (esp));  \
}  while (0)
  
#define K 1024
int main() {
    char* c;
    int i = 0;
    unsigned long esp;
    GET_ESP (esp);
    printf ("Current stack pointer is %#x\n", esp);
    while (1) {
        c = (char*)esp -  i * K;
        *c = 'a';
        GET_ESP (esp);
        printf ("esp = %#x, overflow %dK\n", esp, i);
        i ++;
    }
}

筆者目前也不能解釋其中的魔術(shù),這神奇的程序盎缍茁裙!上例中發(fā)生SIGSEGV時,在圖2中的流程是:

1 -> 3 -> 4 -> 5 -> 11 -> 10 (注意势誊,發(fā)生SIGSEGV時呜达,該地址已經(jīng)不屬于用戶態(tài)棧了谣蠢,所以是5 à 11 而不是 5 -à 6)

到這里粟耻,我們至少能夠知道SIGSEGV和操作系統(tǒng)(棧的分配和回收),編譯器(誰知道它會不會使用enter這樣的指令呢)有著密切的聯(lián)系眉踱,而不像教科書中“函數(shù)返回后其使用的棧自動回收”那樣簡單挤忙。

2.5 堆

#include <stdio.h>
#include <stdlib.h>
#define K 1024
int main () {
    char* c;
    int i = 0;
    c = malloc (1);
    while (1) {
        c += i*K;
        *c = 'a';
        printf ("overflow %dK\n", i);
        i ++;
    }
}

看了棧的例子,舉一反三就能知道谈喳,SIGSEGV和堆的關(guān)系取決于你的內(nèi)存分配器册烈,通常這意味著取決于C庫的實現(xiàn)。

上面這個例子在筆者機器上于15K時產(chǎn)生SIGSEGV婿禽。讓我們改變初次malloc的內(nèi)存大小赏僧,當(dāng)初次分配16M時,SIGSEGV推遲到了溢出180K扭倾;當(dāng)初次分配160M時淀零,SIGSEGV推遲到了溢出571K。我們知道內(nèi)存分配器在分配不同大小的內(nèi)存時通常有不同的機制膛壹,這個例子從某種角度證明了這點驾中。此例SIGSEGV在圖2中的流程為:

1 -> 3 -> 4 -> 5 -> 11 -> 10

用一個野指針在堆里胡亂訪問很少見,更多被問起的是“為什么我訪問一塊free()后的內(nèi)存卻沒發(fā)生SIGSEGV”模聋,比如下面這個例子:

#include <stdio.h>
#include <stdlib.h>
   
#define K 1024
int main () {
    int* a;
   
    a = malloc (sizeof(int));
    *a = 100;
    printf ("%d\n", *a);
    free (a);
    printf ("%d\n", *a);
}

SIGSEGV沒有發(fā)生肩民,但free()后a指向的內(nèi)存被清零了,一個合理的解釋是為了安全链方。相信不會再有人問SIGSEGV沒發(fā)生的原因持痰。是的,free()后的內(nèi)存不一定就立即歸還給了操作系統(tǒng)祟蚀,在真正的歸還發(fā)生前工窍,它一直在那兒。

2.6如果是指向全局區(qū)的野指針呢暂题?

看了上面兩個例子移剪,我覺得這實在沒什么好講的。

2.7 函數(shù)跳轉(zhuǎn)到了一個非法的地址上執(zhí)行

這也是產(chǎn)生SIGSEGV的常見原因薪者,來看下面的例子:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void foo () {
    char c;
    memset (&c, 0x55, 128);
}
int main () {
    foo();
}

通過棧溢出纵苛,我們將函數(shù)foo的返回地址覆蓋成了0x55555555,函數(shù)跳轉(zhuǎn)到了一個非法地址執(zhí)行,最終引發(fā)SIGSEGV攻人。非法地址執(zhí)行取试,在圖2中的流程中的可能性就太多了,從1->3 ->4 -> … ->10怀吻,從4到10之間瞬浓,幾乎每條路徑都可能出現(xiàn)。當(dāng)然對于此例蓬坡,0x55555555所指向的頁面并不在內(nèi)存之中猿棉,其在圖2的流程為:

1->3 ->4 ->5-->11->10

如果非法地址對應(yīng)的頁面(頁面屬于用戶態(tài)地址空間)存在于內(nèi)存中,它又是可執(zhí)行的[*]屑咳,則程序會執(zhí)行一大堆隨機的指令萨赁。在這些指令執(zhí)行過程中一旦訪問內(nèi)存,其產(chǎn)生SIGSEGV的流程幾乎就無法追蹤了(除非你用調(diào)試工具跟進)兆龙≌人看到這里,一個很合理的問題是:為什么程序在非法地址中執(zhí)行的是隨機指令紫皇,而不是非法指令呢慰安?在一塊未知的內(nèi)存上執(zhí)行,遇到非法指令可能性比較大吧聪铺,這樣應(yīng)該收到SIGILL信號盎馈?

[*]如果不用段寄存器的type checking计寇,只用頁表保護锣杂,傳統(tǒng)32bit IA32可讀即可執(zhí)行。在NX技術(shù)出現(xiàn)后頁級也可以控制是否可以執(zhí)行番宁。

事實并非如此元莫,我們的IA32架構(gòu)使用了如此復(fù)雜的指令集,以至于找到一條非法指令的編碼還真不容易蝶押。在下例子中:

#include <stdio.h>
#include <stdlib.h>
int main() {
    char buf[128] = "asdfaowerqoweurqwuroahfoasdbaoseur20   234123akfhasbfqower53453";
    sleep(1);
}

筆者在buf中隨機的敲入了一些字符踱蠢,反匯編其內(nèi)容得到的結(jié)果是:

0xbffa9e00:     popa  
0xbffa9e01:     jae    0xbffa9e67
0xbffa9e03:     popaw 
0xbffa9e05:     outsl  %ds:(%esi),(%dx)
0xbffa9e06:     ja     0xbffa9e6d
0xbffa9e08:     jb     0xbffa9e7b
0xbffa9e0a:     outsl  %ds:(%esi),(%dx)
0xbffa9e0b:     ja     0xbffa9e72
0xbffa9e0d:     jne    0xbffa9e81
0xbffa9e0f:     jno    0xbffa9e88
0xbffa9e11:     jne    0xbffa9e85
0xbffa9e13:     outsl  %ds:(%esi),(%dx)
0xbffa9e14:     popa  
0xbffa9e15:     push   $0x73616f66
0xbffa9e1a:     bound  %esp,%fs:0x6f(%ecx)
0xbffa9e1e:     jae    0xbffa9e85
0xbffa9e20:     jne    0xbffa9e94
0xbffa9e22:     xor    (%eax),%dh
0xbffa9e24:     and    %ah,(%eax)
0xbffa9e26:     and    %dh,(%edx)
0xbffa9e28:     xor    (%ecx,%esi,1),%esi
0xbffa9e2b:     xor    (%ebx),%dh
0xbffa9e2d:     popa  
0xbffa9e2e:     imul   $0x61,0x68(%esi),%esp
0xbffa9e32:     jae    0xbffa9e96
0xbffa9e34:     data16
0xbffa9e35:     jno    0xbffa9ea6
0xbffa9e37:     ja     0xbffa9e9e
0xbffa9e39:     jb     0xbffa9e70
0xbffa9e3b:     xor    0x33(,%esi,1),%esi
0xbffa9e42:     add    %al,(%eax)
0xbffa9e44:     add    %al,(%eax)
0xbffa9e46:     add    %al,(%eax)
0xbffa9e48:     add    %al,(%eax)
0xbffa9e4a:     add    %al,(%eax)
0xbffa9e4c:     add    %al,(%eax)
0xbffa9e4e:     add    %al,(%eax)
0xbffa9e50:     add    %al,(%eax)
0xbffa9e52:     add    %al,(%eax)
0xbffa9e54:     add    %al,(%eax)
0xbffa9e56:     add    %al,(%eax)
0xbffa9e58:     add    %al,(%eax)
0xbffa9e5a:     add    %al,(%eax)
0xbffa9e5c:     add    %al,(%eax)
0xbffa9e5e:     add    %al,(%eax)

…………………………………………………………………………

一條非法指令都沒有!大家也可以自己構(gòu)造一些隨機內(nèi)容試試棋电,看能得到多少非法指令茎截。故在實際情況中,函數(shù)跳轉(zhuǎn)到非法地址執(zhí)行時赶盔,遇到SIGSEGV的概率是遠(yuǎn)遠(yuǎn)大于SIGILL的企锌。

我們來構(gòu)造一個遭遇SIGILL的情況,如下例:

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

 

#define GET_EBP(ebp)    \

    do {    \

        asm volatile ("movl %%ebp, %0\n\t" : "=m" (ebp));  \

    } while (0)
char buf[128];

void foo () {
    printf ("Hello world\n");
}
void build_ill_func() {
   int i = 0;
    memcpy (buf, foo, sizeof(buf));
    while (1) {
        /*
         * Find *call* instruction and replace it with
         * *ud2a* to generate a #UD exception
         */
        if ( buf[i] == 0xffffffe8 ) {
            buf[i] = 0x0f;
            buf[i+1] = 0x0b;
            break;
        }
        i ++;
    }
}
void overflow_ret_address () {
    unsigned long ebp;
    unsigned long addr = (unsigned long)buf;
    int i;
    GET_EBP (ebp);
    for ( i=0; i<16; i++ )
        memcpy ((void*)(ebp + i*sizeof(addr)), &addr, sizeof(addr));
    printf ("ebp = %#x\n", ebp);
}
int main() {
    printf ("%p\n", buf);
    build_ill_func ();
    overflow_ret_address ();
}

2.8非法的系統(tǒng)調(diào)用參數(shù)

我們在一塊全局的buf里填充了一些指令于未,其中有一條是ud2a撕攒,它是IA32指令集中用來構(gòu)造一個非法指令陷阱陡鹃。在overflow_ret_address()中,我們通過棧溢出覆蓋函數(shù)的返回地址抖坪,使得函數(shù)返回時跳轉(zhuǎn)到buf執(zhí)行萍鲸,最終執(zhí)行到ud2a指令產(chǎn)生一個SIGILL信號。注意此例使用了ebp框架指針寄存器擦俐,在編譯時不能使用-fomit-frame-pointer參數(shù)脊阴,否則得不到期望的結(jié)果。
這是一種較為特殊的情況蚯瞧。特殊是指前面的例子訪問非法內(nèi)存都發(fā)生在用戶態(tài)嘿期。而此例中,對非法內(nèi)存的訪問卻發(fā)生在內(nèi)核態(tài)状知。通常是執(zhí)行copy_from_user()或copy_to_user()時秽五。其流程在圖2中為:
1 -> …. -> 11 -> 12 -> 13
內(nèi)核使用fixup[*]的技巧來處理在處理此類錯誤孽查。ULK說通常的處理是發(fā)送一個SIGSEGV信號饥悴,但實際大多數(shù)系統(tǒng)調(diào)用都可以返回EFAULT(bad address)碼,從而避免用戶態(tài)程序被終結(jié)盲再。這種情況就不舉例了西设,筆者一時間想不出哪個系統(tǒng)調(diào)用可以模擬此種情況而不返回EFAULT錯誤。

3.如何避免SIGSEGV

良好的編程習(xí)慣永遠(yuǎn)是最好的預(yù)防方法答朋。良好的習(xí)慣包括:

盡量按照C標(biāo)準(zhǔn)寫程序贷揽。之所以說是盡量,是因為C標(biāo)準(zhǔn)有太多平臺相關(guān)和無定義的行為梦碗,而其中一些實際上已經(jīng)有既成事實的標(biāo)準(zhǔn)了禽绪。例如C標(biāo)準(zhǔn)中,一個越界的指針導(dǎo)致的是無定義的行為洪规,而在實際情況中印屁,一個越界而未解引用的指針是不會帶來災(zāi)難后果的。借用CU的一個例子斩例,如下:

#include <stdio.h>
#include <stdlib.h>
int main () {
    char a[] = "hello";
    char* p;
    for ( p = a+5; p>=a; p-- )
        printf ("%c\n", *p);
}

雖然循環(huán)結(jié)束后雄人,p指向了數(shù)組a前一個元素,在C標(biāo)準(zhǔn)中這是一個無定義的行為念赶,但實際上程序卻是安全的础钠,沒有必要為了不讓p成為一個野指針而把程序改寫為:

#include <stdio.h>
#include <stdlib.h>
int main () {
    char a[] = "hello";
    char* p;      
    for ( p = a+5; p!=a; p-- ) {
        printf ("%c\n", *p);
    }
    printf ("%c\n", *p);
}

當(dāng)然,或許世界上真有編譯器會對“越界但未解引用”的野指針進行處理叉谜,例如引發(fā)一個SIGSEGV旗吁。筆者無法100%保證,所以大家在實踐中還是各自斟酌吧停局。

徹底的懂得你的程序很钓。和其它程序員不同的是驻民,C程序員需要對自己的程序完全了解,做到精確控制履怯。尤其在內(nèi)存的分配和釋放方面回还。在操作每一個指針前,你都應(yīng)該清楚它所指向內(nèi)存的出處(棧叹洲、堆柠硕、全局區(qū)),并清楚此內(nèi)存的生存周期运提。只有明白的使用內(nèi)存蝗柔,才能最大限度的避免SIGSEGV的產(chǎn)生。

大量使用assert民泵。筆者偏好在程序中使用大量的assert癣丧,凡是有認(rèn)為不該出現(xiàn)的情況,筆者就會加入一個assert做檢查栈妆。雖然assert無法直接避免SIGSEGV胁编,但它卻能盡早的拋出錯誤。離錯誤越近鳞尔,就越容易root cause嬉橙。很多時候出現(xiàn)SIGSEGV時,程序已經(jīng)跑飛很遠(yuǎn)了寥假。

打開-Wall –Werror編譯選項市框。如果程序是自己寫的,0 warning應(yīng)該始終是一項指標(biāo)(0 warning不包括因為編譯器版本不同而引起的warning)糕韧。一種常見的SIGSEGV來源于向函數(shù)傳入了錯誤的參數(shù)類型垃它。例如:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main () {
    char buf[12];
    int buff;
    strcpy (buff, "hello");
}

這個例子中绒障,本意是要向buf拷貝一個字符串,但由于有一個和buf名稱很相近的buff變量,由于一個筆誤(這個筆誤很可能就來自你編輯器的自動補全忆嗜,例如vim的ctrl – p, ctrl – n)刷晋,strcpy如愿的引發(fā)了SIGSEGV蓖议。實際在編譯期間史汗,編譯器就提示我們warning: passing argument 1 of strcpy makes pointer from integer without a cast,但我們忽略了怕吴。

這就進一步要求我們盡量使用編譯器的類型檢查功能窍侧,包括多用函數(shù)少用宏(特別是完成復(fù)雜功能的宏),函數(shù)參數(shù)多用帶類型的指針转绷,少用void*指針等伟件。此例就是我們在2.2節(jié)提到的不經(jīng)意的行為。

少用奇技淫巧议经,多用標(biāo)準(zhǔn)方法斧账。好的程序應(yīng)該邏輯清楚谴返,干凈整潔,像一篇朗朗上口的文章咧织,讓人一讀就懂嗓袱。那種充滿晦澀語法、怪異招數(shù)的試驗作品习绢,是不受歡迎的渠抹。很多人喜歡把性能問題做為使用不標(biāo)準(zhǔn)方法的借口,實際上他們根本不知道對性能的影響如何闪萄,拿不出具體指標(biāo)梧却,全是想當(dāng)然爾。筆者曾經(jīng)在項目中败去,將一個執(zhí)行頻繁的異常處理函數(shù)用匯編重寫放航,使該函數(shù)的執(zhí)行周期從2000多個機器周期下降到40多個。滿心歡喜的提交了一個patch給該項目的maintainer圆裕,得到的答復(fù)是:“張广鳍,你具體測試過你的patch能帶來多大的性能提升嗎?如果沒有明顯的數(shù)據(jù)葫辐,我是不愿意將優(yōu)雅的C代碼替換成這晦澀的匯編的搜锰。”于是我做了一個內(nèi)核編譯來測試patch耿战,耗時15分鐘,我的patch帶來的整體性能提升大約為0.1%焊傅。所以剂陡,盡量寫清楚明白的代碼,不僅有利于避免SIGSEGV狐胎,也利于在出現(xiàn)SIGSEGV后進行調(diào)試鸭栖。

當(dāng)你的一個需求,標(biāo)準(zhǔn)的方法不能滿足時握巢,只有兩種可能:1.從一開始的設(shè)計就錯了晕鹊,才會導(dǎo)致錯誤的需求;2.你讀過的代碼太少暴浦,不知道業(yè)界解決該問題的標(biāo)準(zhǔn)方法是什么溅话。計算機已經(jīng)發(fā)展了幾十年,如果你不是在做前沿研究歌焦,遇到一定得用非標(biāo)準(zhǔn)方法解決的問題的機會實在太小了飞几。正如我們經(jīng)常用gdb跟蹤發(fā)現(xiàn)SIGSEGV發(fā)生在C庫里,不要嚷嚷說C庫有bug独撇,大部情況是一開始你傳入的參數(shù)就錯了屑墨。

主要內(nèi)容來源

SEGMENTATION FAULT IN LINUX 原因與避免

2.SIGILL

1.illegal instruction躁锁,即SIGILL

是POSIX標(biāo)準(zhǔn)中提供的一類錯誤。 從名字上看卵史,SIGILL是啟動的某個進程中的某一句不能被CPU識別成正確的指令战转。 此類錯誤是由操作系統(tǒng)發(fā)送給進程的,在進程試圖執(zhí)行一些形式錯誤以躯、未知或者特權(quán)指令時操作系統(tǒng)會使用SIGILL信號終止程序匣吊。 SIGILL對應(yīng)的常數(shù)是4.

2.造成SIGILL的原因

錯誤代碼示例

typedef void(*FUNC)(void);
int main(void)
{
    const static unsigned char insn[4] = { 0xff, 0xff, 0xff, 0xff };
    FUNC function = (FUNC) insn;
    function();
}

2.1 將不正確的數(shù)據(jù)段寫入代碼段

進程在代碼段中的數(shù)據(jù)是要被作為一個指令執(zhí)行的。 若不小心覆蓋了已有的代碼段寸潦,可能會得到錯誤格式的指令色鸳。 這種錯誤尤其在Just-In-Time即時編譯器中最可能出現(xiàn)。
同樣见转,如果不小心覆蓋了棧上活躍記錄中的返回地址命雀,程序就可能根據(jù)這個錯誤地址,執(zhí)行沒有意義的內(nèi)存中的數(shù)據(jù)斩箫,進而操作吏砂。
進一步可以認(rèn)為,任何導(dǎo)致數(shù)據(jù)錯誤的問題都可能帶來illegal instruction問題乘客。比如硬盤發(fā)生故障狐血。

2.2 指令集的演進

比如SIMD指令,自從奔騰4開始有MMX易核,X86的芯片就開始不停的增加和拓寬SIMD支持匈织,SSE、SSE2牡直、SSE3缀匕、SSE42、AVX碰逸、AVX2乡小。 默認(rèn)情況下,很多編譯器都在O2或者O3中開了自動向量化饵史,這就導(dǎo)致很多在新體系結(jié)構(gòu)中編譯的可執(zhí)行程序满钟,在老機器上運行時會有illegal instruction問題。

2.3 工具鏈BUG

對于普通C語言通過編譯器生成的可執(zhí)行程序胳喷。一般都已經(jīng)通過嚴(yán)格的測試,不會隨便發(fā)生這種問題湃番。 所以如果你遇到這種錯,并且試過了靜態(tài)鏈厌蔽,而且程序中沒有嵌入式匯編牵辣,基本可以斷定是工具鏈出了問題。 編譯器奴饮?匯編器或者鏈接器纬向。

2.4 訪存對齊或浮點數(shù)格式問題

根據(jù)經(jīng)驗择浊,出現(xiàn)錯誤的指令可能和訪存地址指令有關(guān)。 另外逾条,浮點數(shù)的格式是否符合IEEE的標(biāo)準(zhǔn)也可能會有影響琢岩。

3.錯誤排查方法

1.程序中有沒有特權(quán)指令、或者訪問特權(quán)寄存器
2.有沒有將在較新CPU上編譯得到的可執(zhí)行文件拿到老CPU上運行
3.程序中有沒有嵌入式匯編师脂,先檢查担孔。
>1.一般編譯器很少會生成有這種問題的代碼
>2.X86平臺上要尤其注意64位匯編指令和32位匯編指令的混用問題

4.程序有在進程代碼段空間寫數(shù)據(jù)的機會嗎?
5.棧操作夠安全嗎吃警?
6.注意程序的ABI是否正確糕篇,尤其是動態(tài)鏈和靜態(tài)鏈?zhǔn)欠裉幚淼恼_,盡量避免動態(tài)鏈的可執(zhí)行文件調(diào)用錯誤庫的問題(ARM的EABI酌心,MIPS的N32/O32/N64都很可能出這種問題)
7.用的工具鏈靠譜嗎拌消?

4.參考文檔

SIGILL定義
Illegal Instruction 錯誤初窺

3.SIGABRT

1.什么是SIGABRT

wiki上是這么定義的
The SIGABRT and SIGIOT signal is sent to a process to tell it to abort, i.e. to terminate. The signal is usually initiated by the process itself when it calls abort() function of the C Standard Library, but it can be sent to the process from outside like any other signal.
通俗的說就是由調(diào)用abort函數(shù)產(chǎn)生,進程非正常退出安券。

2.導(dǎo)致SIGABRT的原因

2.1.多次free導(dǎo)致SIGABRT

#include "stdlib.h"
#include "string.h"
#include "stdio.h
int main()
{
    void *pc = malloc(1024);
    free(pc);
    //free(pc);  //打開注釋會導(dǎo)致錯誤
    printf("free ok!\n");
    return 0;
}

2.2執(zhí)行abort()函數(shù)

#include "string.h"
#include "stdio.h"
#include "stdlib.h"
 
int main()
{
    printf("before run abort!\n");
    abort();
    printf("after run abort!\n");
     return 0;
}

2.3執(zhí)行到assert函數(shù)

#include "string.h"
#include "stdio.h"
#include "assert.h"
#include "stdlib.h"
int main(){
    printf("before run assert!\n");
#if 0  //該值為0墩崩,則報錯;為1侯勉,則正常
    void *pc = malloc(1024);
#else
    void *pc = NULL;
#endif
    assert( pc != NULL );
    printf("after run assert!\n");
    return 0;
}

3.參考文獻(xiàn)

程序運行產(chǎn)生SIGABRT信號的原因
SIGABRT

4.SIGBUS

1.什么是SIGBUG

wiki是這么說的
The SIGBUS signal is sent to a process when it causes a bus error. The conditions that lead to the signal being sent are, for example, incorrect memory access alignment or non-existent physical address.

通常來說SIGBUS鹦筹,是由于進程引起了一個總線錯誤(Bus error)。如下是wiki對總線錯誤的定義
In computing, a bus error is a fault raised by hardware, notifying an operating system (OS) that a process is trying to access memory that the CPU cannot physically address: an invalid address for the address bus, hence the name. In modern use on most architectures these are much rarer than segmentation faults, which occur primarily due to memory access violations: problems in the logical address or permissions. On POSIX-compliant platforms, bus errors usually result in the SIGBUS signal being sent to the process that caused the error. SIGBUS can also be caused by any general device fault that the computer detects, though a bus error rarely means that the computer hardware is physically broken—it is normally caused by a bug in software.[citation needed] Bus errors may also be raised for certain other paging errors;

2.導(dǎo)致SIGBUS的原因

2.1 未對齊的讀或?qū)?/h3>

事實上址貌,總線錯誤幾乎都是由于未對齊的讀或?qū)懸鸬挠嗖啊K苑Q為總線錯誤,是因為出現(xiàn)未對齊的內(nèi)存訪問請求時政恍,被阻塞(block)的組件就是地址總線宪赶。對齊(alignment)的意思就是數(shù)據(jù)項只能存儲在地址是數(shù)據(jù)項大小的整數(shù)倍的內(nèi)存上辕棚。在現(xiàn)代的計算機架構(gòu)中补君,尤其是RISC架構(gòu)屿储,都需要數(shù)據(jù)對齊赊堪,因為與任意的對齊有關(guān)的有關(guān)的額外邏輯會使整個內(nèi)存系統(tǒng)更大且更慢相叁。通過迫使每個內(nèi)存訪問局限在一個cache行或一個單獨的頁面內(nèi)遵绰,可以極大地簡化并加速如cache控制器和內(nèi)存管理單元(MMU)這樣的硬件。
我們用地址對齊這個術(shù)語來陳述這個問題增淹,而不是直截了當(dāng)?shù)卣f是禁止內(nèi)存跨頁訪問椿访,但它們說但是同一回事。例如虑润,訪問一個8字節(jié)的double數(shù)據(jù)時成玫,地址只允許是8的整數(shù)倍。所以一個double數(shù)據(jù)可以存儲于地址24,地址8008或32768哭当,但不能存儲于地址1006(因為它無法被8整除)猪腕。
頁和cache的大小都是經(jīng)過精心設(shè)計的,這樣只要遵守對齊規(guī)則就可以保證一個原子數(shù)據(jù)項不會跨過一個頁或cache塊的邊界荣病。

下面是代碼示例

#include<stdio.h>
union {
    char a[10];
    int i;
} u;
int main(void){
    int *p = (int *) (&(u.a[1]));
    /**
     * p中未對齊的地址將會引起總線錯誤码撰,
     * 因為數(shù)組和int的聯(lián)合確保了a是按照int的4字節(jié)來對齊的,
     * 所以“a+1”肯定不是int對齊的
     */
    *p = 17; 
    printf("%d %p %p %p\n", *p, &(u.a[0]), &(u.a[1]), &(u.i));
    printf("%lu %lu\n", sizeof(char), sizeof(int));
    return 0;
}

5.SIGTRAP

1.什么是SIGTRAP个盆?

The SIGTRAP signal is sent to a process when an exception (or trap) occurs: a condition that a debugger has requested to be informed of – for example, when a particular function is executed, or when a particular variable changes value.
通常來說SIGTRAP是由斷點指令或其它trap指令產(chǎn)生. 由debugger使用脖岛。如果沒有附加調(diào)試器,則該過程將終止并生成崩潰報告颊亮。 較低級的庫(例如柴梆,libdispatch)會在遇到致命錯誤時捕獲進程。

2.導(dǎo)致SIGTRAP被發(fā)送給進程的原因

2.1 斷點指令觸發(fā)

Debugger模式下终惑,設(shè)置斷點绍在,當(dāng)程序運行到斷點時候,就會引發(fā)SIGTRAP雹有。

2.2 其他trap指令觸發(fā)

相關(guān)資料不多偿渡,待后續(xù)更新

6.SIGFPE

1.什么是SIGFPE?

SIG是信號名的通用前綴霸奕。FPE是floating-point exception(浮點異常)的首字母縮略字溜宽。在POSIX兼容的平臺上,SIGFPE是當(dāng)一個進程執(zhí)行了一個錯誤的算術(shù)操作時發(fā)送給它的信號质帅。SIGFPE的符號常量在頭文件signal.h中定義适揉。因為在不同平臺上,信號數(shù)字可能變化煤惩,因此常使用信號名稱嫉嘀。

2.導(dǎo)致SIGFPE被發(fā)送給進程的原因

1.FPE_INTDIV 整數(shù)除以零

2.FPE_INTOVF 整數(shù)上溢

3.FPE_FLTDIV 浮點除以零

4.FPE_FLTOVF 浮點上溢

5.FPE_FLTUND 浮點下溢

6.FPE_FLTRES 浮點結(jié)果不準(zhǔn)

7.FPE_FLTINV 無效浮點操作

8.FPE_FLTSUB 浮點下標(biāo)越界

這是一個嘗試執(zhí)行一個稱為整數(shù)除以零,或FPE_INTDIV的錯誤算術(shù)運算的ANSI C程序的例子魄揉。

int main(){      
 int x = 42/0;      
}

3.參考文獻(xiàn)

Documentation ArchiveDeveloperSearch
Exception Programming Topics

參考文獻(xiàn)

POSIX signals

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末剪侮,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子洛退,更是在濱河造成了極大的恐慌票彪,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件不狮,死亡現(xiàn)場離奇詭異,居然都是意外死亡在旱,警方通過查閱死者的電腦和手機摇零,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來桶蝎,“玉大人驻仅,你說我怎么就攤上這事谅畅。” “怎么了噪服?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵毡泻,是天一觀的道長。 經(jīng)常有香客問我粘优,道長仇味,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任雹顺,我火速辦了婚禮丹墨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嬉愧。我一直安慰自己贩挣,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布没酣。 她就那樣靜靜地躺著王财,像睡著了一般。 火紅的嫁衣襯著肌膚如雪裕便。 梳的紋絲不亂的頭發(fā)上绒净,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天,我揣著相機與錄音闪金,去河邊找鬼疯溺。 笑死,一個胖子當(dāng)著我的面吹牛哎垦,可吹牛的內(nèi)容都是我干的囱嫩。 我是一名探鬼主播,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼漏设,長吁一口氣:“原來是場噩夢啊……” “哼墨闲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起郑口,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤鸳碧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后犬性,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瞻离,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年乒裆,在試婚紗的時候發(fā)現(xiàn)自己被綠了套利。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖肉迫,靈堂內(nèi)的尸體忽然破棺而出验辞,到底是詐尸還是另有隱情,我是刑警寧澤喊衫,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布跌造,位于F島的核電站,受9級特大地震影響族购,放射性物質(zhì)發(fā)生泄漏壳贪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一联四、第九天 我趴在偏房一處隱蔽的房頂上張望撑碴。 院中可真熱鬧,春花似錦朝墩、人聲如沸醉拓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽亿卤。三九已至,卻和暖如春鹿霸,著一層夾襖步出監(jiān)牢的瞬間排吴,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工懦鼠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留钻哩,地道東北人。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓肛冶,卻偏偏與公主長得像街氢,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子睦袖,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,724評論 2 351

推薦閱讀更多精彩內(nèi)容

  • 在iOS開發(fā)中珊肃,Crash無疑是App的致命殺手。作為一個嚴(yán)謹(jǐn)?shù)膇OS 開發(fā)人員來說馅笙,寫出優(yōu)秀的健碩的無Crash...
    烈焰德瑪閱讀 2,651評論 0 11
  • 崩潰日志 如何得到crash report 當(dāng)一個iOS應(yīng)用程序崩潰時, 系統(tǒng)會創(chuàng)建一份crash日志保存在設(shè)備上...
    々莫等閑々閱讀 2,897評論 0 2
  • define SIGHUP 1 // 終端連接結(jié)束時發(fā)出(不管正陈浊牵或非正常)define SIGINT 2 /...
    唯吾知足_c35c閱讀 361評論 0 0
  • 一、信號機制 函數(shù)運行在用戶態(tài),當(dāng)遇到系統(tǒng)調(diào)用董习、中斷或是異常的情況時,程序會進入內(nèi)核態(tài)烈和。信號涉及到了這兩種狀態(tài)之間...
    feifei_fly閱讀 8,061評論 1 14
  • 當(dāng)iOS設(shè)備上的App應(yīng)用閃退時斥杜,操作系統(tǒng)會生成一個crash日志虱颗,保存在設(shè)備上。crash日志上有很多有用的信息...
    SuperBoy_Timmy閱讀 4,899評論 0 7