你的java/c/c++程式崩潰了?揭祕段錯誤(Segmentation fault)(3)
前言
接上兩篇:
寫到這裡,越跟,越發現真的是核心上很白,非一般的白。
但是既然是研究,就定住心,把段錯誤搞到清楚明白。
本篇將作為終篇,來結束這個系列,也算是對段錯誤和程式除錯、尋找崩潰原因(通常不會給你那麼完美的stackstrace和人性化的錯誤提示)的再深入。
本篇使用到的工具或命令:
- dmesg
- strace
- gdb
- 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崩潰哦,那檢查下多執行緒的共享變數吧!
如有錯誤,請不吝賜教.