Segmentation Fault in Linux 原因與避免
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 #define K 1024 5 int main () { 6 char* c; 7 int i = 0; 8 9 c = malloc (1); 10 while (1) { 11 c += i*K; 12 *c = 'a'; 13 printf ("overflow %dK\n", i); 14 i ++; 15 } 16 }
看了棧的例子,舉一反三就能知道,SIGSEGV和堆的關係取決於你的記憶體分配器,通常這意味著取決於C庫的實現。
上面這個例子在筆者機器上於15K時產生SIGSEGV。讓我們改變初次malloc的記憶體大小,當初次分配16M時,SIGSEGV推遲到了溢位180K;當初次分配160M時,SIGSEGV推遲到了溢位571K。我們知道記憶體分配器在分配不同大小的記憶體時通常有不同的機制,這個例子從某種角度證明了這點。此例SIGSEGV在圖2中的流程為:
1 -> 3 -> 4 -> 5 -> 11 ->
用一個野指標在堆裡胡亂訪問很少見,更多被問起的是“為什麼我訪問一塊free()後的記憶體卻沒發生SIGSEGV”,比如下面這個例子:
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 #define K 1024 5 int main () { 6 int* a; 7 8 a = malloc (sizeof(int)); 9 *a = 100; 10 printf ("%d\n", *a); 11 free (a);12 printf ("%d\n", *a); 13 }
SIGSEGV沒有發生,但free()後a指向的記憶體被清零了,一個合理的解釋是為了安全。相信不會再有人問SIGSEGV沒發生的原因。是的,free()後的記憶體不一定就立即歸還給了作業系統,在真正的歸還發生前,它一直在那兒。
2.6如果是指向全域性區的野指標呢?看了上面兩個例子,我覺得這實在沒什麼好講的。
2.7 函式跳轉到了一個非法的地址上執行這也是產生SIGSEGV的常見原因,來看下面的例子:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 5 void foo () { 6 char c; 7 8 memset (&c, 0x55, 128); 9 } 10 11 int main () { 12 foo(); 13 }
通過棧溢位,我們將函式foo的返回地址覆蓋成了0x55555555,函式跳轉到了一個非法地址執行,最終引發SIGSEGV。非法地址執行,在圖2中的流程中的可能性就太多了,從1->3 ->4 -> … ->10,從4到10之間,幾乎每條路徑都可能出現。當然對於此例,0x55555555所指向的頁面並不在記憶體之中,其在圖2的流程為:
1->3 ->4 ->5-->11->10
如果非法地址對應的頁面(頁面屬於使用者態地址空間)存在於記憶體中,它又是可執行的[*],則程式會執行一大堆隨機的指令。在這些指令執行過程中一旦訪問記憶體,其產生SIGSEGV的流程幾乎就無法追蹤了(除非你用除錯工具跟進)。看到這裡,一個很合理的問題是:為什麼程式在非法地址中執行的是隨機指令,而不是非法指令呢?在一塊未知的記憶體上執行,遇到非法指令可能性比較大吧,這樣應該收到SIGILL訊號啊?
[*]如果不用段暫存器的type checking,只用頁表保護,傳統32bit IA32可讀即可執行。在NX技術出現後頁級也可以控制是否可以執行。
事實並非如此,我們的IA32架構使用瞭如此複雜的指令集,以至於找到一條非法指令的編碼還真不容易。在下例子中:
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 int main() { 5 char buf[128] = "asdfaowerqoweurqwuroahfoasdbaoseur20 234123akfhasbfqower53453"; 6 sleep(1); 7 }
筆者在buf中隨機的敲入了一些字元,反彙編其內容得到的結果是:
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)
…………………………………………………………………………
一條非法指令都沒有!大家也可以自己構造一些隨機內容試試,看能得到多少非法指令。故在實際情況中,函式跳轉到非法地址執行時,遇到SIGSEGV的概率是遠遠大於SIGILL的。
我們來構造一個遭遇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 (); }
我們在一塊全域性的buf裡填充了一些指令,其中有一條是ud2a,它是IA32指令集中用來構造一個非法指令陷阱。在overflow_ret_address()中,我們通過棧溢位覆蓋函式的返回地址,使得函式返回時跳轉到buf執行,最終執行到ud2a指令產生一個SIGILL訊號。注意此例使用了ebp框架指標暫存器,在編譯時不能使用-fomit-frame-pointer引數,否則得不到期望的結果。
2.8非法的系統呼叫引數這是一種較為特殊的情況。特殊是指前面的例子訪問非法記憶體都發生在使用者態。而此例中,對非法記憶體的訪問卻發生在核心態。通常是執行copy_from_user()或copy_to_user()時。其流程在圖2中為:
1 -> …. -> 11 -> 12 -> 13
核心使用fixup[*]的技巧來處理在處理此類錯誤。ULK說通常的處理是傳送一個SIGSEGV訊號,但實際大多數系統呼叫都可以返回EFAULT(bad address)碼,從而避免使用者態程式被終結。這種情況就不舉例了,筆者一時間想不出哪個系統呼叫可以模擬此種情況而不返回EFAULT錯誤。
2.9還有什麼?我們已經總結了產生SIGSEGV的大多數情況,在實際程式設計中,即使現象不一樣,最終發生SIGSEGV的原因都可以歸到上述幾類。掌握了這些基本例子,我們可以避免大多數的SIGSEGV。