你的java/c/c++程序崩潰了?揭祕段錯誤(Segmentation fault)(3)

前言

接上兩篇:

你的C/C++程序爲什麼無法運行?揭祕Segmentation fault (1)
你的C/C++程序爲什麼無法運行?揭祕Segmentation fault (2)

寫到這裏,越跟,越發現真的是內核上很白,非一般的白。
但是既然是研究,就定住心,把段錯誤搞到清楚明白。

本篇將作爲終篇,來結束這個系列,也算是對段錯誤和程序調試、尋找崩潰原因(通常不會給你那麼完美的stackstrace和人性化的錯誤提示)的再深入。

本篇使用到的工具或命令:

  1. dmesg
  2. strace
  3. gdb
  4. linux 內核3.10源碼

情景再現

上兩篇圍繞着一個這樣的問題進行展開:

//野指針
char ** p;
//零指針或空指針
p = NULL;
//段錯誤(Segmentation Fault)
*p = (char *)malloc(sizeof(char));

問題代碼

爲了本篇的可讀性,圍繞上述問題編織問題代碼:

#include "stdio.h"
#include "string.h"
#include "stdlib.h"


int main(int argc,char** args) {
    char * p = NULL;
    *p = 0x0;
}

段錯誤

這裏寫圖片描述

找出問題


第1步 strace 查信號描述

上篇已經介紹了gbd+coredump的方法來找到出現段錯誤的代碼,本篇直接上strace:

strace -i -x -o segfault.txt ./segfault.o

得到如下信息:
這裏寫圖片描述

可以知道:

1.錯誤信號:SIGSEGV
3.錯誤碼:SEGV_MAPERR
3.錯誤內存地址:0x0
4.邏輯地址0x400507處出錯.

可以猜測:

程序中有空指針訪問試圖向0x0寫入而引發段錯誤.

第2步 dmesg 查錯誤現場

上dmesg:

dmesg

得到:
這裏寫圖片描述

可知:

1.錯誤類型:segfault ,即段錯誤(Segmentation Fault).
2.出錯時ip:0x400507
3.錯誤號:6,即110

第3步 收集已知結論


這裏 錯誤號和ip 是關鍵,錯誤號對照下面:

    /*
     * 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 = (PF_USER | PF_WIRTE | 0).
即“用戶態”、“寫入型頁錯誤 ”、“沒有與指定的地址相對應的頁”.

上面的信息與我們最初的推斷吻合.

現在,對目前已知結論進行概括如下:

1.錯誤類型:segfualt ,即段錯誤(Segmentation Fault).

2.出錯時ip:0x400507

3.錯誤號:6,即110

4.錯誤碼:SEGV_MAPERR 即地址沒有映射到對象.

5.錯誤原因:對0x0進行寫操作引發了段錯誤,原因是0x0沒有與之對應的頁或者叫映射.

第4步 根據結論找到出錯代碼

上gdb:

gdb ./segfault.o

根據結論中的ip = 0x400507立即得到:

這裏寫圖片描述

顯然,這驗證了我們的結論:

我們試圖將值0x0寫入地址0x0從而引發寫入未映射的地址的段錯誤.

這裏寫圖片描述並且我們找到了錯誤的代碼stack.c的第9行:

查根溯源

顯然,我們不滿足於此,爲什麼訪問了0x0會造成這個錯誤從而讓程序崩潰?

第二篇已經說了進程虛擬地址空間的問題,事實上我們進行寫入操作的時候,會引發虛擬地址到物理地址的映射,因爲你最終要將數據(本篇是0x0,注意和我們的地址0x0區分)寫入到物理內存中。

0x0是個邏輯地址,linux按頁式管理內存映射,0x0不會對應任何頁,那麼內存中就不會有主頁,所以對其進行寫入就會引發一個缺頁中斷,這一部分由linux內存映射管理模塊(memory mapping,縮寫mm)處理。

缺頁錯誤處理

1. __do_page_fault

缺頁後進入__do_page_fault流程,注意,這裏爲了儘量減少篇幅,刪去了源代碼的一些註釋,而與我們有關的命中代碼都做了註釋:

/*
 * This routine handles page faults.  It determines the address,
 * and the problem, and then passes it off to one of the appropriate
 * routines.
 */
static void __kprobes
__do_page_fault(struct pt_regs *regs, unsigned long error_code./*  注意我們的錯誤是6,即110 */)
{
    struct vm_area_struct *vma;
    struct task_struct *tsk;
    unsigned long address;
    struct mm_struct *mm;
    int fault;
    int write = error_code & PF_WRITE;
    unsigned int flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE |
                    (write ? FAULT_FLAG_WRITE : 0);

    tsk = current;
    mm = tsk->mm;

    /* 這裏會去取到我們的 地址=0x0 */
    /* Get the faulting address: */
    address = read_cr2();

    if (kmemcheck_active(regs))
        kmemcheck_hide(regs);
    prefetchw(&mm->mmap_sem);

    if (unlikely(kmmio_fault(regs, address)))
        return;

    if (unlikely(fault_in_kernel_space(address))) {
        //這裏略去,不會命中
        /* ... */
        return;
    }

    //略去很多代碼
    // ...

retry:
        down_read(&mm->mmap_sem);
    } else {
        might_sleep();
    }

    vma = find_vma(mm, address);
    if (unlikely(!vma)) {

        /* 到這裏處理 */
        bad_area(regs, error_code, address);
        //處理後返回
        return;
    }

    //略去很多代碼
    // ...
}

2. bad_area

其中的一個關鍵調用bad_area(regs, error_code, address);

static noinline void
bad_area(struct pt_regs *regs, unsigned long error_code, unsigned long address)
{
    /* 注意這裏講錯誤碼設爲了SEGV_MAPERR */
    __bad_area(regs, error_code, address, SEGV_MAPERR);
}

可以明確

我們結論中的SEGV_MAPERR的出處.

這個類型就是無法映射到對象的意思!看下面strace得到的東西,其中
si_code=SEGV_MAPERR.

--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0} ---
+++ killed by SIGSEGV (core dumped) +++

最後會來到這裏:

static void
__bad_area_nosemaphore(struct pt_regs *regs, unsigned long error_code,
               unsigned long address, int si_code)
{
    struct task_struct *tsk = current;

    /* 我們的錯誤碼是6 = 110,PF_USER = 100,所以會進入這個if */
    if (error_code & PF_USER) {

        /* 關中斷 */
        local_irq_enable();

        //...略 

        if (address >= TASK_SIZE)
            error_code |= PF_PROT;

        /* 這裏會將出錯信息打印 */
        if (likely(show_unhandled_signals))
            show_signal_msg(regs, error_code, address, tsk);

        tsk->thread.cr2     = address;
        tsk->thread.error_code  = error_code;
        tsk->thread.trap_nr = X86_TRAP_PF;

        /* 這裏會強制發送 SIGSEGV=段錯誤 信號 */
        force_sig_info_fault(SIGSEGV, si_code, address, tsk, 0);

        return;
    }

    //...略
}

注意上面的代碼的兩個關鍵調用:

show_signal_msg  //用於打印出錯信息
force_sig_info_fault  //用於強制發送信號

3. show_signal_msg

/*
 * Print out info about fatal segfaults, if the show_unhandled_signals
 * sysctl is set:
 */
static inline void
show_signal_msg(struct pt_regs *regs, unsigned long error_code,
        unsigned long address, struct task_struct *tsk)
{
    //...略

    /* 打印段錯誤信息 -> /proc/kmsg */
    printk("%s%s[%d]: segfault at %lx ip %p sp %p error %lx",
        task_pid_nr(tsk) > 1 ? KERN_INFO : KERN_EMERG,
        tsk->comm, task_pid_nr(tsk), address,
        (void *)regs->ip, (void *)regs->sp, error_code);

    print_vma_addr(KERN_CONT " in ", regs->ip);

    printk(KERN_CONT "\n");
}

其中,打印段錯誤的信息的代碼,就是我們使用dmesg得到的東西.

可以對比下我們的段錯誤的圖:
這裏寫圖片描述

4. force_sig_info_fault

最後就是發送信號了。

static void
force_sig_info_fault(int si_signo, int si_code, unsigned long address,
             struct task_struct *tsk, int fault)
{
    unsigned lsb = 0;
    siginfo_t info;

    info.si_signo   = si_signo;
    info.si_errno   = 0;
    info.si_code    = si_code;
    info.si_addr    = (void __user *)address;
    if (fault & VM_FAULT_HWPOISON_LARGE)
        lsb = hstate_index_to_shift(VM_FAULT_GET_HINDEX(fault)); 
    if (fault & VM_FAULT_HWPOISON)
        lsb = PAGE_SHIFT;
    info.si_addr_lsb = lsb;

    /* 強制發送SIGSEGV信號 */
    force_sig_info(si_signo, &info, tsk);
}

force_sig_info:

int
force_sig_info(int sig, struct siginfo *info, struct task_struct *t)
{
    unsigned long int flags;
    int ret, blocked, ignored;
    struct k_sigaction *action;

    spin_lock_irqsave(&t->sighand->siglock, flags);

    /* 這裏就指定信號的處理程序了 */
    action = &t->sighand->action[sig-1];

    //...略

    /* 必須強制發送 */
    if (action->sa.sa_handler == SIG_DFL)
        /* 不需要遞歸式的發送SEGSIGV信號,所以清掉SIGNAL_UNKILLABLE */
        t->signal->flags &= ~SIGNAL_UNKILLABLE;

    // 發送
    ret = specific_send_sig_info(sig, info, t);
    spin_unlock_irqrestore(&t->sighand->siglock, flags);

    return ret;
}

上面的代碼告訴我們,信號的處理程序如何被指定的,那麼關於段錯誤的信號SEGSIGV默認就是core dump.

5. core dump

到此,我們已經可以拿到core dump,那麼第二篇中找到引發段錯誤的代碼的方法就可以用了,這也是推薦的做法:

gdb ./segfault.o core.36054

這裏寫圖片描述

是不是立即可知stack.c第9行的代碼*p = 0x0是罪魁禍首了呢?

結語

到此,整個段錯誤的探索就結束了,希望讀者和我一樣不虛此行。

列出幾種常見段錯誤原因:

1.數組越界

    int a[10] = {0,1};
    printf("%d",a[10000]);

2.零指針或空指針

    //本系列所用實例
    char * p = NULL;
    *p = 0x0;

3.懸浮指針

如果指針p懸浮,它指向的地址有可能能用,也有可能不能,你不知道那塊地址什麼時候被寫入,什麼時候被保護(mprotect).
如果被保護爲可讀,你寫就出現段錯誤!

4.訪問權限,非法訪問

參見3.

5.多線程對共享指針變量操作

不僅c/c++,android中、java程序中有可能也會出現jvm崩潰哦,那檢查下多線程的共享變量吧!

如有錯誤,請不吝賜教.

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