Linux 段錯誤詳解

背景

筆者早年寫過一篇:《可惡的”Segmentation faults”之初級總結篇》,網絡轉載甚多。多年下來,關於段錯誤的討論依舊很熱烈,該問題也還是很常見。所以打算在這裏再系統地梳理一下該問題的來龍去脈。

什麼是段錯誤

下面是來自 Answers.com 的定義:

A segmentation fault (often shortened to segfault) is a particular error condition that can occur during the operation of computer software. In short, 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 (e.g., attempts to write to a read-only location, or to overwrite part of the operating system). Systems based on processors like the Motorola 68000 tend to refer to these events as Address or Bus errors.

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 invalid memory receives the SIGSEGV signal. On Microsoft Windows, a process that accesses invalid memory receives the STATUS_ACCESS_VIOLATION exception.

另外,網上還有個基本上對照的中文解釋:

所謂的段錯誤就是指訪問的內存超出了系統所給這個程序的內存空間,通常這個值是由 gdtr 來保存的,他是一個 48 位的寄存器,其中的 32 位是保存由它指向的 gdt 表,後 13 位保存相應於 gdt 的下標,最後 3 位包括了程序是否在內存中以及程序的在 cpu 中的運行級別,指向的 gdt 是由以 64 位爲一個單位的表,在這張表中就保存着程序運行的代碼段以及數據段的起始地址以及與此相應的段限和頁面交換還有程序運行級別還有內存粒度等等的信息。一旦一個程序發生了越界訪問,cpu 就會產生相應的異常保護,於是 segmentation fault 就出現了

通過上面的解釋,段錯誤應該就是訪問了不可訪問的內存,這個內存區要麼是不存在的,要麼是受到系統保護的。

段錯誤日誌分析

例子

一個典型的例子是 scanf參數使用錯誤:

#include <stdio.h>

int main(int argc, char *argv[])
{
        int i; 

        scanf("%d\n", i);

        return 0;
}

文件保存爲 segfault-scanf.c。其中 &i寫成了 i

段錯誤信息

$ make segfault-scanf
$ ./segfault-scanf
100
Segmentation fault (core dumped)

段錯誤分析

$ catchsegv ./segfault-scanf
100
Segmentation fault (core dumped)
*** Segmentation fault
Register dump:

 RAX: 0000000000000ca0   RBX: 0000000000000040   RCX: 0000000000000010
 RDX: 0000000000000000   RSI: 0000000000000000   RDI: 1999999999999999
 RBP: 00007fffdbdf1010   R8 : 00007fbb45330060   R9 : 0000000000000000
 R10: 0000000000000ca0   R11: 0000000000000000   R12: 0000000000000004
 R13: 0000000000000000   R14: 00007fbb45330640   R15: 000000000000000a
 RSP: 00007fffdbdf0c20

 RIP: 00007fbb44fc761a   EFLAGS: 00010212

 CS: 0033   FS: 0000   GS: 0000

 Trap: 0000000e   Error: 00000006   OldMask: 00000000   CR2: 00000000

 FPUCW: 0000037f   FPUSW: 00000000   TAG: 00000000
 RIP: 00000000   RDP: 00000000

 ST(0) 0000 0000000000000000   ST(1) 0000 0000000000000000
 ST(2) 0000 0000000000000000   ST(3) 0000 0000000000000000
 ST(4) 0000 0000000000000000   ST(5) 0000 0000000000000000
 ST(6) 0000 0000000000000000   ST(7) 0000 0000000000000000
 mxcsr: 1f80
 XMM0:  00000000000000000000000000000000 XMM1:  00000000000000000000000000000000
 XMM2:  00000000000000000000000000000000 XMM3:  00000000000000000000000000000000
 XMM4:  00000000000000000000000000000000 XMM5:  00000000000000000000000000000000
 XMM6:  00000000000000000000000000000000 XMM7:  00000000000000000000000000000000
 XMM8:  00000000000000000000000000000000 XMM9:  00000000000000000000000000000000
 XMM10: 00000000000000000000000000000000 XMM11: 00000000000000000000000000000000
 XMM12: 00000000000000000000000000000000 XMM13: 00000000000000000000000000000000
 XMM14: 00000000000000000000000000000000 XMM15: 00000000000000000000000000000000

Backtrace:
/lib/x86_64-linux-gnu/libc.so.6(_IO_vfscanf+0x303a)[0x7fbb44fc761a]
/lib/x86_64-linux-gnu/libc.so.6(__isoc99_scanf+0x109)[0x7fbb44fce399]
??:?(main)[0x400587]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5)[0x7fbb44f91ec5]
??:?(_start)[0x400499]

Memory map:

00400000-00401000 r-xp 00000000 08:09 2903814 segfault-scanf
00600000-00601000 r--p 00000000 08:09 2903814 segfault-scanf
00601000-00602000 rw-p 00001000 08:09 2903814 segfault-scanf
01b98000-01bbd000 rw-p 00000000 00:00 0 [heap]
7fbb44d5a000-7fbb44d70000 r-xp 00000000 08:02 1710807 /lib/x86_64-linux-gnu/libgcc_s.so.1
7fbb44d70000-7fbb44f6f000 ---p 00016000 08:02 1710807 /lib/x86_64-linux-gnu/libgcc_s.so.1
7fbb44f6f000-7fbb44f70000 rw-p 00015000 08:02 1710807 /lib/x86_64-linux-gnu/libgcc_s.so.1
7fbb44f70000-7fbb4512b000 r-xp 00000000 08:02 1731685 /lib/x86_64-linux-gnu/libc-2.19.so
7fbb4512b000-7fbb4532b000 ---p 001bb000 08:02 1731685 /lib/x86_64-linux-gnu/libc-2.19.so
7fbb4532b000-7fbb4532f000 r--p 001bb000 08:02 1731685 /lib/x86_64-linux-gnu/libc-2.19.so
7fbb4532f000-7fbb45331000 rw-p 001bf000 08:02 1731685 /lib/x86_64-linux-gnu/libc-2.19.so
7fbb45331000-7fbb45336000 rw-p 00000000 00:00 0
7fbb45336000-7fbb4533a000 r-xp 00000000 08:02 1731696 /lib/x86_64-linux-gnu/libSegFault.so
7fbb4533a000-7fbb45539000 ---p 00004000 08:02 1731696 /lib/x86_64-linux-gnu/libSegFault.so
7fbb45539000-7fbb4553a000 r--p 00003000 08:02 1731696 /lib/x86_64-linux-gnu/libSegFault.so
7fbb4553a000-7fbb4553b000 rw-p 00004000 08:02 1731696 /lib/x86_64-linux-gnu/libSegFault.so
7fbb4553b000-7fbb4555e000 r-xp 00000000 08:02 1731686 /lib/x86_64-linux-gnu/ld-2.19.so
7fbb45729000-7fbb4572c000 rw-p 00000000 00:00 0
7fbb4575a000-7fbb4575d000 rw-p 00000000 00:00 0
7fbb4575d000-7fbb4575e000 r--p 00022000 08:02 1731686 /lib/x86_64-linux-gnu/ld-2.19.so
7fbb4575e000-7fbb4575f000 rw-p 00023000 08:02 1731686 /lib/x86_64-linux-gnu/ld-2.19.so
7fbb4575f000-7fbb45760000 rw-p 00000000 00:00 0
7fffdbdd2000-7fffdbdf3000 rw-p 00000000 00:00 0
7fffdbdfe000-7fffdbe00000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

上述日誌包含了寄存器、回調以及內存映像信息。其中回調部分的 _IO_vfscanf即指出了 scanf的問題。不過咋一看不明顯,可以用gdb單步跟蹤進行確認。

關於寄存器我們最關心的信息:

Trap: 0000000e   Error: 00000006

arch/x86/include/asm/traps.harch/x86/kernel/traps.c找到SIGSEGV的類型有:

/* Interrupts/Exceptions */
enum {
        ...
        X86_TRAP_OF,            /*  4, Overflow */
        X86_TRAP_BR,            /*  5, Bound Range Exceeded */
        X86_TRAP_TS,            /* 10, Invalid TSS */
        X86_TRAP_GP,            /* 13, General Protection Fault */
        X86_TRAP_PF,            /* 14, Page Fault */
        ...
}

Trap 爲 0xe,即 14,也就是 Page Fault。

arch/x86/mm/fault.c則詳細解釋了錯誤碼(Error):

/*
 * Page fault error code bits:
 *
 *   bit 0 ==    0: no page found       1: protection fault
 *   bit 1 ==    0: read access         1: write access
 *   bit 2 ==    0: kernel-mode access  1: user-mode access
 *   bit 3 ==                           1: use of reserved bit detected
 *   bit 4 ==                           1: fault was an instruction fetch
 */
enum x86_pf_error_code {

        PF_PROT         =               1 << 0,
        PF_WRITE        =               1 << 1,
        PF_USER         =               1 << 2,
        PF_RSVD         =               1 << 3,
        PF_INSTR        =               1 << 4,
};

上面的錯誤碼:6,二進制爲 110 即:

  • 1: user-mode access
  • 1: write access
  • 0: no page found

也可以用 在線查看工具,例如,輸入錯誤碼 6 即可獲得:

The cause was a user-mode write resulting in no page being found.

常見段錯誤舉例

這裏列舉一下常見的段錯誤例子。

scanf 參數:把 &i 寫爲 i

int i;
scanf("%d", i);

分析:i 被定義後,數值是不確定的,而 scanf 把 i 的值當作參數傳入 scanf,而 scanf 則會把 i 當成了地址,把用戶輸入的內容存入該處。而該地址因爲隨機,可能根本就不存在或者不合法。

sprintf/printf 參數:%d/%c 寫成 %s

int i = 10;
printf("%s", i);

分析:打印字串時,實際上是打印某個地址開始的所有字符,而這裏把整數作爲參數傳遞過去,這個整數被當成了一個地址,然後 printf 從這個地址開始打印字符,直到某個位置上的值爲 \0。如果這個整數代表的地址不存在或者不可訪問,自然也是訪問了不該訪問的內存 —— segmentation fault。

數組訪問越界

char test[1];
printf("%c", test[1000000000]);

注:也可能報告爲 Bus Error,可能存在對未對齊的地址讀或寫。

寫只讀內存

char *ptr = "test";
strcpy(ptr, "TEST");

分析:ptr 被定義成了 “test”,是一個只讀的內存段,不能直接寫入,要寫入需要用 malloc 從堆中分配或者定義成一個字符串數組。

堆棧溢出

void main()
{
    main();
}

分析:上面實際上是一個死循環的遞歸調用,會造成堆棧溢出。

pthread_create() 失敗後 pthread_join()

#define THREAD_MAX_NUM
pthread_t thread[THREAD_MAX_NUM];

分析:用 pthread_create() 創建了各個線程,然後用 pthread_join() 來等待線程的結束。剛開始直接等待,在創建線程都成功時,pthread_join() 能順利等到各個線程結束,但是一旦創建線程失敗,用 pthread_join() 來等待那個本不存在的線程時自然會存在未知內存的情況,從而導致段錯誤的發生。解決辦法是:在創建線程之前,先初始化線程數組,在等待線程結束時,判斷線程是否爲初始值,如果是的話,說明線程並沒有創建成功,所以就不能等拉。

小結

綜上所有例子,

  • 定義了指針後記得初始化,在使用時記得判斷是否爲 NULL
  • 在使用數組時記得初始化,使用時要檢查數組下標是否越界,數組元素是否存在等
  • 在變量處理時變量的格式控制是否合理等

其他的就需要根據經驗不斷積累,更多例子會不斷追加到上述列表中。

另外,也務必掌握一些基本的分析和調試手段,即使在遇到新的這類問題時也知道如何應對。

分析和調試手段

分析方法除了最簡便的 catchsegv外,還有諸多辦法,它們的應用場景各異。

catchsegv原理

該工具就是用來撲獲段錯誤的,它通過動態加載器(ld-linux.so)的預加載機制(PRELOAD)把一個事先寫好的庫(/lib/libSegFault.so)加載上,用於捕捉斷錯誤的出錯信息。

gdb調試

gdb ./segfault-scanf 
...
Reading symbols from ./segfault-scanf...done.
(gdb) r
Starting program: segfault-scanf 
100

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7a6b61a in _IO_vfscanf_internal (s=<optimized out>, 
    format=<optimized out>, argptr=argptr@entry=0x7fffffffddc8, 
    errp=errp@entry=0x0) at vfscanf.c:1857
1857    vfscanf.c: No such file or directory.
(gdb) bt
#0  0x00007ffff7a6b61a in _IO_vfscanf_internal (s=<optimized out>, 
    format=<optimized out>, argptr=argptr@entry=0x7fffffffddc8, 
    errp=errp@entry=0x0) at vfscanf.c:1857
#1  0x00007ffff7a72399 in __isoc99_scanf (format=<optimized out>)
    at isoc99_scanf.c:37
#2  0x0000000000400580 in main ()

coredump分析

$ ulimit -c 1024
$ gdb segfault-scanf ./core 
Reading symbols from segfault-scanf...done.
[New LWP 16913]
Core was generated by `./segfault-scanf'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x00007fd2d24ec61a in _IO_vfscanf_internal (s=<optimized out>, 
    format=<optimized out>, argptr=argptr@entry=0x7fff14dfa668, 
    errp=errp@entry=0x0) at vfscanf.c:1857
1857    vfscanf.c: No such file or directory.

程序內捕獲 SIGSEGV信號並啓動 gdb

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

void dump(int signo)
{
        char buf[1024];
        char cmd[1024];
        FILE *fh;

        snprintf(buf, sizeof(buf), "/proc/%d/cmdline", getpid());
        if(!(fh = fopen(buf, "r")))
                exit(0);
        if(!fgets(buf, sizeof(buf), fh))
                exit(0);
        fclose(fh);
        if(buf[strlen(buf) - 1] == '\n')
                buf[strlen(buf) - 1] = '\0';
        snprintf(cmd, sizeof(cmd), "gdb %s %d", buf, getpid());
        system(cmd);

        exit(0);
}

int main(int argc, char *argv[])
{
        int i; 

        signal(SIGSEGV, &dump);
        scanf("%d\n", i);

        return 0;
}

用法如下:

$ gcc -g -rdynamic -o segfault-scanf segfault-scanf.c
$ sudo ./segfault-scanf
100
(gdb) bt
#0  0x00007fb743e065cc in __libc_waitpid (pid=16988, 
    stat_loc=stat_loc@entry=0x7fffb51d8fe0, options=options@entry=0)
    at ../sysdeps/unix/sysv/linux/waitpid.c:31
#1  0x00007fb743d8b1d2 in do_system (line=<optimized out>)
    at ../sysdeps/posix/system.c:148
#2  0x0000000000400ba1 in dump (signo=11) at segfault-scanf.c:21
#3  <signal handler called>
#4  0x00007fb743d9c61a in _IO_vfscanf_internal (s=<optimized out>, 
    format=<optimized out>, argptr=argptr@entry=0x7fffb51da318, 
    errp=errp@entry=0x0) at vfscanf.c:1857
#5  0x00007fb743da3399 in __isoc99_scanf (format=<optimized out>)
    at isoc99_scanf.c:37
#6  0x0000000000400bdd in main (argc=1, argv=0x7fffb51da508)
    at segfault-scanf.c:31

程序內捕獲 SIGSEGV信號並調用 backtrace()獲取回調

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

void dump(int signo)
{
        void *array[10];
        size_t size;
        char **strings;
        size_t i;

        size = backtrace (array, 10);
        strings = backtrace_symbols (array, size);

        printf ("Obtained %zd stack frames.\n", size);

        for (i = 0; i < size; i++)
                printf ("%s\n", strings[i]);

        free (strings);

        exit(0);
}

int main(int argc, char *argv[])
{
        int i; 

        signal(SIGSEGV, &dump);
        scanf("%d\n", i);

        return 0;
}

用法如下:

$ ./segfault-scanf
100
Obtained 7 stack frames.
./segfault-scanf() [0x40077e]
/lib/x86_64-linux-gnu/libc.so.6(+0x36c30) [0x7f249fa43c30]
/lib/x86_64-linux-gnu/libc.so.6(_IO_vfscanf+0x303a) [0x7f249fa6461a]
/lib/x86_64-linux-gnu/libc.so.6(__isoc99_scanf+0x109) [0x7f249fa6b399]
./segfault-scanf-call-backtrace() [0x400837]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7f249fa2eec5]
./segfault-scanf-call-backtrace() [0x400699]

除此之外,還可以通過 dmesg查看內核信息並通過 objdump或者 addr2line把 IP 地址轉化爲代碼行,不過用法沒有catchsegv來得簡單。dmesg獲取的內核信息由 arch/x86/mm/fault.c: show_signal_msg()打印。

總結

段錯誤是 Linux 下 C 語言開發常見的 Bug,本文從原理、案例、分析和調試方法等各個方面進行了詳細分析,希望有所幫助。

如果希望瞭解更多,推薦閱讀如下參考資料。

參考資料

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