1. 程式人生 > 實用技巧 >segment fault異常及常見定位手段【轉】

segment fault異常及常見定位手段【轉】

轉自:https://www.cnblogs.com/wahaha02/p/8034112.html

問題背景

最近boot中遇到個使用者態程式的segment fault異常,除了一句“Segment fault”列印外無其他任何列印。該問題復現概率較低,定位起來比較棘手。我們的boot是個經過裁剪的最小linux系統,由於bootflash大小的限制,加上在boot階段也沒有掛載其他儲存裝置,所以沒有沒法放gdb、動態庫等體積較大的除錯工具。本文以linux 3.10核心和mips cpu小系統為基礎,記錄下對這個問題的研究總結。

segment fault異常處理流程

使用者態程式由於系統呼叫或異常等原因,系統陷入核心,並伴隨著CPU特權的切換和從使用者態棧到核心態棧的切換,核心呼叫SAVE_ALL儲存陷入核心前的現場(即pt_regs結構)到核心棧上,然後核心通過查詢異常跳轉表或系統呼叫跳轉表獲得相應的處理程式入口,處理完成後,給使用者態程式傳送SIGSEGV訊號,並通過pt_regs恢復現場返回到使用者態程式,使用者態程式收到SIGSEGV訊號並進行處理。至此,完成全部處理流程。

可見異常前的現場資訊,即pt_regs是個很重要的資訊,其具體定義如下,包括CPU通用暫存器、error pc、error cause、bad address等資訊。

在linux kernel中,如下場景都會觸發pt_regs壓棧動作:

  • tlb異常
  • NMI中斷
  • 中斷
  • 異常
  • 系統呼叫 
struct pt_regs {
#ifdef CONFIG_32BIT
    /* Pad bytes for argument save space on the stack. */
    unsigned long pad0[6];
#endif

    /* Saved main processor registers. */
    unsigned long regs[32];

    /* Saved special registers. */
    unsigned long cp0_status;
    unsigned long hi;
    unsigned long lo;
#ifdef CONFIG_CPU_HAS_SMARTMIPS
    unsigned long acx;
#endif
    unsigned long cp0_badvaddr;
    unsigned long cp0_cause;
    unsigned long cp0_epc;
#ifdef CONFIG_MIPS_MT_SMTC
    unsigned long cp0_tcstatus;
#endif /* CONFIG_MIPS_MT_SMTC */
} __attribute__ ((aligned (8)));

segment fault 常見觸發源

核心會依據下列條件來判斷是否發生了使用者態段錯誤,並上報SIGSEGV資訊給使用者態task:

  • 使用者態資料段的地址越界
  • 使用者態程式碼段的指令讀取異常
  • 訪問操作與所訪問的記憶體頁面許可權不匹配
  • 非對齊訪問(一般是上報SIGBUS,但mips會上報SIGSEGV)

導致段錯誤的常見程式設計正規化有:

  • 使用未初始化變數
  • 使用已釋放的記憶體
  • 陣列越界
  • 多程序下使用不可重入函式
  • 記憶體被踩(如棧被踩導致pc或資料定址錯誤等)

segment fault常用定位手段

最佳的定位手段是能直接定位到產生異常的程式碼,差一點的,至少能提供相關資訊,通過分析能間接定位到異常程式碼。segment fault的定位手段還是比較豐富的,但也各有優缺點,需要根據具體場景進行選用。

gdb

gdb的優點是除錯手段豐富,可以逐步跟蹤除錯,適用於穩定復現的故障。缺點是故障必須能必現。

coredump

coredump的優點是對於偶現的段錯誤故障,核心會匯出一個coredump檔案,然後可以用gdb離線除錯coredump檔案來定位。缺點是如果環境對段錯誤等異常有重啟保護,coredump檔案需要有地方儲存。

使用者態backtrace

glibc的execinfo庫提供一套介面:backtrace、backtrace_symbols,可以通過這套介面,捕獲到SIGSEGV異常後列印異常發生時的backtrace。缺點是依賴glibc的excinfo,而各CPU對其實現支援情況不一。

核心態backtrace

核心態call trace列印一般通過stack_dump來列印,由於linux的核心態棧和使用者態棧是獨立分開了,所以stack_dump並不支援使用者態call trace列印。但核心提供了save_stack_trace_user/print_stack_trace介面,可以在異常處理程式中列印使用者態程序的呼叫鏈。缺點是這套介面在arch下實現,而各CPU對其實現支援情況不一。

catchsegv

catchsegv是libc提供的支援段錯誤back trace列印指令碼,可以在發生SIGSEGV時直接打印出異常點的backtrace。缺點是依賴libc的libSegFault.so和addr2line工具。

pt_regs

如前文所說,pt_regs物件提供了異常發生時的error pc、error cause、bad address等資訊,反彙編使用者程式後,通過error pc等資訊可以找到具體的異常彙編指令和函式,分析彙編程式碼找到對應的C程式碼。缺點是需要人工分析彙編程式碼。

解決方案

回到本文一開始的問題,由於bootflash大小的限制,加上在boot階段也沒有掛載其他儲存裝置,gdb、coredump、catchsegv都沒法用;libc對mipc arch下的backtrace實現有問題,使用者態backtrace也沒法用;mips arch核心沒有實現核心態backtrace的介面,所以也沒法用。所以只剩下列印pt_regs這一條路了,在上報SIGSEGV前,呼叫列印即可。雖然mips arch也沒有實現列印方法,不過實現很簡單,具體實現如下:

static inline void
show_signal_msg(struct pt_regs *regs, unsigned long error_code,
        unsigned long address, struct task_struct *tsk)
{
    unsigned long sp = regs->regs[29];    
    unsigned long pc = regs->cp0_epc;

    if (!unhandled_signal(tsk, SIGSEGV))
        return;

    if (!printk_ratelimit())
        return;

    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 *)pc, (void *)sp, error_code);

    print_vma_addr(KERN_CONT " in ", pc);

    printk(KERN_CONT "\n");
}

例項演示:

模擬segv:
const char* p = "abcd";
*(char*)p = 'a';
核心列印:
4:<6>lxImage[1813]: segfault at 1200e5e98 ip 0000000120012628 sp 000000ffffb08140 error 1 4:<c> in lxImage[120000000+124000] 4:<c>

反彙編獲知在 BSP_RstReason_Print 函式中sb v1,0(v0) 指令對地址1200e5e98寫操作引起segment fault異常
00000001200125a0 <BSP_RstReason_Print>:
...
120012628: a0430000 sb v1,0(v0)