你的java/c/c++程序崩溃了?揭秘段错误(Segmentation fault)(3)
前言
接上两篇:
你的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崩溃哦,那检查下多线程的共享变量吧!
如有错误,请不吝赐教.
你的java/c/c++程序崩溃了?揭秘段错误(Segmentation fault)(3)的更多相关文章
- golang 程序 在linux 出现 段错误
刚做的 golang 程序 发布到linux 竟然出现 段错误 原因是 内核版本过低,请升级内核
- 转载:Linux服务器Cache占用过多内存导致系统内存不足最终java应用程序崩溃解决方案
原文链接: https://blog.csdn.net/u014740338/article/details/66975550 问题描述 Linux内存使用量超过阈值,使得Java应用程序无可用内存, ...
- Android 程序崩溃后的处理
在应用发布以后,由于安卓机型的千差万别 ,可能会出现各种各样的问题,这时候如果我们可以将这些信息收集起来,并进行修改就很不错了.下面就来讨论一下怎么处理程序崩溃以后,错误信息的手机. Java中已经提 ...
- 捕android程序崩溃日志
主要类别: package com.example.callstatus; import java.io.File; import java.io.FileOutputStream; import j ...
- android 程序崩溃crash日志的捕捉
android 程序崩溃crash日志的捕捉 之前在项目开发过程中,一直会遇到程序崩溃了,但是测试組的哥哥们又没及时的导出日志.... 后来在诳群的时候听别人说起,腾讯有那么一个叫bugly的东西 将 ...
- android程序崩溃后重启
有时候由于测试不充分或者程序潜在的问题而导致程序异常崩溃,这个是令人无法接受的,在android中怎样捕获程序的异常崩溃,然后进行一些必要的处理或重新启动 应用这个问题困恼了我很久,今天终于解决了该问 ...
- Android程序崩溃异常收集框架
最近在写Android程序崩溃异常处理,完成之后,稍加封装与大家分享. 我的思路是这样的,在程序崩溃之后,将异常信息保存到一个日志文件中,然后对该文件进行处理,比如发送到邮箱,或发送到服务器. 所以, ...
- Android在程序崩溃或者捕获异常之后重新启动app
在Android应用开发中,偶尔会因为测试的不充分导致一些异常没有被捕获,这时应用会出现异常并强制关闭,这样会导致很不好的用户体验,为了解决这个问题,我们需要捕获相关的异常并做处理. 首先捕获程序崩溃 ...
- Android 应用程序崩溃日志捕捉
程序崩溃是应用迭代中不可避免的问题,即使有着5年或者10年经验的程序猿也无法完全保证自己的代码没有任何的bug导致崩溃,现在有一些第三方平台可以帮助我们搜集应用程序的崩溃,比如友盟,详情如下图 虽然能 ...
随机推荐
- GraphChi/graphchi-java程序配置
1.导入graphchi-java maven项目时报错: Plugin execution not covered by lifecycle configuration: org.scala-too ...
- html5 canvas简单的直线路径
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- ReactJS -- 初学入门
<!DOCTYPE html> <html> <head> <script src="build/react.js"></sc ...
- C 语言结构体之点运算符( . )和箭头运算符( -> )的区别
很多时候,在对结构体进行相应的编码时,时而发现是用点运算符( . ),时而是用箭头运算符( -> ):那么这两者之间的使用有什么区别么? 相同点:两者都是二元操作符,而且右边的操作数都是成员的名 ...
- brotli压缩
brotli压缩 https://www.cnblogs.com/shanyou/p/9154816.html Brotli是一种全新的数据格式,可以提供比Zopfli高20-26%的压缩比.据谷歌研 ...
- springcloud使用Zuul构建微服务网关入门
为什么要使用微服务网关 不同的微服务一般会经过不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求. 如果让客户端直接与各个微服务通信,会有以下的问题: 客户端会多次请求不同的微 ...
- 初识 Asp.Net数据验证控件
在我们建立一个Asp.Net Web应用程序的时候我一般都会注意我们工具如图
- eclipse导入项目报错multiple annotations found at this line
eclipsewindow-->preference-->Valdation-->将Manual和Build下复选框全部取消选择
- Explain之key_len长度计算
以前我很好奇通过执行计划Explain去分析SQL的时候看到的key_len值有时很小,有时看到很大,那时也不知道它是怎么算出来的,现在终于搞懂了,嘻.因为网上对key_len的长度的计算的资料也很少 ...
- interface关键字定义接口
package interface0; public interface InterfaceTest { /* * 接口的定义,使用interface关键字定义接口 */ public interfa ...