前言
接上兩篇:
你的C/C++程序為什么無法運行?揭秘Segmentation fault (1)
你的C/C++程序為什么無法運行?揭秘Segmentation fault (2)
寫到這里,越跟,越發現真的是內核上很白,非一般的白。
但是既然是研究,就定住心,把段錯誤搞到清楚明白。
本篇將作為終篇,來結束這個系列,也算是對段錯誤和程序調試、尋找崩潰原因(通常不會給你那么完美的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崩潰哦,那檢查下多線程的共享變量吧!
如有錯誤,請不吝賜教.