你的java/c/c++程序崩溃了?揭秘段错误(Segmentation fault)(3)
2015-05-01 11:11
387 查看
前言
接上两篇:你的C/C++程序为什么无法运行?揭秘Segmentation fault (1)
你的C/C++程序为什么无法运行?揭秘Segmentation fault (2)
写到这里,越跟,越发现真的是内核上很白,非一般的白。
但是既然是研究,就定住心,把段错误搞到清楚明白。
本篇将作为终篇,来结束这个系列,也算是对段错误和程序调试、寻找崩溃原因(通常不会给你那么完美的stackstrace和人性化的错误提示)的再深入。
本篇使用到的工具或命令:
dmesgstrace
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崩溃哦,那检查下多线程的共享变量吧!
如有错误,请不吝赐教.
相关文章推荐
- 你的java/c/c++程序崩溃了?揭秘段错误(Segmentation fault)(3)
- 代写 程序 C C++ Java Python VC VB 数据结构 留学生作业 毕业设计
- scala和java通过JNI调用C++程序例子
- 用Swig将c/c++程序转为java代码(使用swig实现java调用c、c++的方法)
- C++程序崩溃生成dump
- 求 float数据在内存中的存储方法 的 java PHP C/C++ 程序
- 让c++程序优雅的崩溃
- Java调用c/c++程序时报错
- C++ 记录Windows程序崩溃时的dumpfile
- Java通过JNI调用C++程序
- Linux使用Java将C++程序发布为webservice(wsdl)
- c++ windows程序崩溃生成转储dump
- C程序员(特别是Linux下的)学习C++/Java有必要吗?
- 请教如何将c++程序转换成java程序?
- 我刚翻译的一些用Swig将c/c++程序转为java代码的英文文档
- Java版本和C++版本的简单Stack程序
- CORBA技术系列一:JAVA客户程序与C++服务程序互操作
- socket communication between a Java and/or C++ programs (现成的程序)
- java程序和C/C++程序的通讯
- 运行java小程序引起IE崩溃一例