前言

接上两篇:

你的C/C++程序为什么无法运行?揭秘Segmentation fault (1)

你的C/C++程序为什么无法运行?揭秘Segmentation fault (2)

写到这里,越跟,越发现真的是内核上很白,非一般的白。

但是既然是研究,就定住心,把段错误搞到清楚明白。

本篇将作为终篇,来结束这个系列,也算是对段错误和程序调试、寻找崩溃原因(通常不会给你那么完美的stackstrace和人性化的错误提示)的再深入。

本篇使用到的工具或命令:

  1. dmesg
  2. strace
  3. gdb
  4. linux 内核3.10源码

情景再现

上两篇围绕着一个这样的问题进行展开:

  1. //野指针
  2. char ** p;
  3. //零指针或空指针
  4. p = NULL;
  5. //段错误(Segmentation Fault)
  6. *p = (char *)malloc(sizeof(char));

问题代码

为了本篇的可读性,围绕上述问题编织问题代码:

  1. #include "stdio.h"
  2. #include "string.h"
  3. #include "stdlib.h"
  4. int main(int argc,char** args) {
  5. char * p = NULL;
  6. *p = 0x0;
  7. }

段错误

找出问题


第1步 strace 查信号描述

上篇已经介绍了gbd+coredump的方法来找到出现段错误的代码,本篇直接上strace:

  1. strace -i -x -o segfault.txt ./segfault.o

得到如下信息:

可以知道:

1.错误信号:SIGSEGV

3.错误码:SEGV_MAPERR

3.错误内存地址:0x0

4.逻辑地址0x400507处出错.

可以猜测:

程序中有空指针访问试图向0x0写入而引发段错误.

第2步 dmesg 查错误现场

上dmesg:

  1. dmesg

得到:

可知:

1.错误类型:segfault ,即段错误(Segmentation Fault).

2.出错时ip:0x400507

3.错误号:6,即110

第3步 收集已知结论


这里 错误号和ip 是关键,错误号对照下面:

  1. /*
  2. * Page fault error code bits:
  3. *
  4. * bit 0 == 0: no page found 1: protection fault
  5. * bit 1 == 0: read access 1: write access
  6. * bit 2 == 0: kernel-mode access 1: user-mode access
  7. * bit 3 == 1: use of reserved bit detected
  8. * bit 4 == 1: fault was an instruction fetch
  9. */
  10. /*enum x86_pf_error_code {
  11. PF_PROT = 1 << 0,
  12. PF_WRITE = 1 << 1,
  13. PF_USER = 1 << 2,
  14. PF_RSVD = 1 << 3,
  15. PF_INSTR = 1 << 4,
  16. };*/

对照后可知:

错误号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:

  1. 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流程,注意,这里为了尽量减少篇幅,删去了源代码的一些注释,而与我们有关的命中代码都做了注释:

  1. /*
  2. * This routine handles page faults. It determines the address,
  3. * and the problem, and then passes it off to one of the appropriate
  4. * routines.
  5. */
  6. static void __kprobes
  7. __do_page_fault(struct pt_regs *regs, unsigned long error_code./* 注意我们的错误是6,即110 */)
  8. {
  9. struct vm_area_struct *vma;
  10. struct task_struct *tsk;
  11. unsigned long address;
  12. struct mm_struct *mm;
  13. int fault;
  14. int write = error_code & PF_WRITE;
  15. unsigned int flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE |
  16. (write ? FAULT_FLAG_WRITE : 0);
  17. tsk = current;
  18. mm = tsk->mm;
  19. /* 这里会去取到我们的 地址=0x0 */
  20. /* Get the faulting address: */
  21. address = read_cr2();
  22. if (kmemcheck_active(regs))
  23. kmemcheck_hide(regs);
  24. prefetchw(&mm->mmap_sem);
  25. if (unlikely(kmmio_fault(regs, address)))
  26. return;
  27. if (unlikely(fault_in_kernel_space(address))) {
  28. //这里略去,不会命中
  29. /* ... */
  30. return;
  31. }
  32. //略去很多代码
  33. // ...
  34. retry:
  35. down_read(&mm->mmap_sem);
  36. } else {
  37. might_sleep();
  38. }
  39. vma = find_vma(mm, address);
  40. if (unlikely(!vma)) {
  41. /* 到这里处理 */
  42. bad_area(regs, error_code, address);
  43. //处理后返回
  44. return;
  45. }
  46. //略去很多代码
  47. // ...
  48. }

2. bad_area

其中的一个关键调用bad_area(regs, error_code, address);

  1. static noinline void
  2. bad_area(struct pt_regs *regs, unsigned long error_code, unsigned long address)
  3. {
  4. /* 注意这里讲错误码设为了SEGV_MAPERR */
  5. __bad_area(regs, error_code, address, SEGV_MAPERR);
  6. }

可以明确

我们结论中的SEGV_MAPERR的出处.

这个类型就是无法映射到对象的意思!看下面strace得到的东西,其中

si_code=SEGV_MAPERR.

  1. --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0} ---
  2. +++ killed by SIGSEGV (core dumped) +++

最后会来到这里:

  1. static void
  2. __bad_area_nosemaphore(struct pt_regs *regs, unsigned long error_code,
  3. unsigned long address, int si_code)
  4. {
  5. struct task_struct *tsk = current;
  6. /* 我们的错误码是6 = 110,PF_USER = 100,所以会进入这个if */
  7. if (error_code & PF_USER) {
  8. /* 关中断 */
  9. local_irq_enable();
  10. //...略
  11. if (address >= TASK_SIZE)
  12. error_code |= PF_PROT;
  13. /* 这里会将出错信息打印 */
  14. if (likely(show_unhandled_signals))
  15. show_signal_msg(regs, error_code, address, tsk);
  16. tsk->thread.cr2 = address;
  17. tsk->thread.error_code = error_code;
  18. tsk->thread.trap_nr = X86_TRAP_PF;
  19. /* 这里会强制发送 SIGSEGV=段错误 信号 */
  20. force_sig_info_fault(SIGSEGV, si_code, address, tsk, 0);
  21. return;
  22. }
  23. //...略
  24. }

注意上面的代码的两个关键调用:

  1. show_signal_msg //用于打印出错信息
  2. force_sig_info_fault //用于强制发送信号

3. show_signal_msg

  1. /*
  2. * Print out info about fatal segfaults, if the show_unhandled_signals
  3. * sysctl is set:
  4. */
  5. static inline void
  6. show_signal_msg(struct pt_regs *regs, unsigned long error_code,
  7. unsigned long address, struct task_struct *tsk)
  8. {
  9. //...略
  10. /* 打印段错误信息 -> /proc/kmsg */
  11. printk("%s%s[%d]: segfault at %lx ip %p sp %p error %lx",
  12. task_pid_nr(tsk) > 1 ? KERN_INFO : KERN_EMERG,
  13. tsk->comm, task_pid_nr(tsk), address,
  14. (void *)regs->ip, (void *)regs->sp, error_code);
  15. print_vma_addr(KERN_CONT " in ", regs->ip);
  16. printk(KERN_CONT "\n");
  17. }

其中,打印段错误的信息的代码,就是我们使用dmesg得到的东西.

可以对比下我们的段错误的图:

4. force_sig_info_fault

最后就是发送信号了。

  1. static void
  2. force_sig_info_fault(int si_signo, int si_code, unsigned long address,
  3. struct task_struct *tsk, int fault)
  4. {
  5. unsigned lsb = 0;
  6. siginfo_t info;
  7. info.si_signo = si_signo;
  8. info.si_errno = 0;
  9. info.si_code = si_code;
  10. info.si_addr = (void __user *)address;
  11. if (fault & VM_FAULT_HWPOISON_LARGE)
  12. lsb = hstate_index_to_shift(VM_FAULT_GET_HINDEX(fault));
  13. if (fault & VM_FAULT_HWPOISON)
  14. lsb = PAGE_SHIFT;
  15. info.si_addr_lsb = lsb;
  16. /* 强制发送SIGSEGV信号 */
  17. force_sig_info(si_signo, &info, tsk);
  18. }

force_sig_info:

  1. int
  2. force_sig_info(int sig, struct siginfo *info, struct task_struct *t)
  3. {
  4. unsigned long int flags;
  5. int ret, blocked, ignored;
  6. struct k_sigaction *action;
  7. spin_lock_irqsave(&t->sighand->siglock, flags);
  8. /* 这里就指定信号的处理程序了 */
  9. action = &t->sighand->action[sig-1];
  10. //...略
  11. /* 必须强制发送 */
  12. if (action->sa.sa_handler == SIG_DFL)
  13. /* 不需要递归式的发送SEGSIGV信号,所以清掉SIGNAL_UNKILLABLE */
  14. t->signal->flags &= ~SIGNAL_UNKILLABLE;
  15. // 发送
  16. ret = specific_send_sig_info(sig, info, t);
  17. spin_unlock_irqrestore(&t->sighand->siglock, flags);
  18. return ret;
  19. }

上面的代码告诉我们,信号的处理程序如何被指定的,那么关于段错误的信号SEGSIGV默认就是core dump.

5. core dump

到此,我们已经可以拿到core dump,那么第二篇中找到引发段错误的代码的方法就可以用了,这也是推荐的做法:

  1. gdb ./segfault.o core.36054

是不是立即可知stack.c第9行的代码*p = 0x0是罪魁祸首了呢?

结语

到此,整个段错误的探索就结束了,希望读者和我一样不虚此行。

列出几种常见段错误原因:

1.数组越界

  1. int a[10] = {0,1};
  2. printf("%d",a[10000]);

2.零指针或空指针

  1. //本系列所用实例
  2. char * p = NULL;
  3. *p = 0x0;

3.悬浮指针

如果指针p悬浮,它指向的地址有可能能用,也有可能不能,你不知道那块地址什么时候被写入,什么时候被保护(mprotect).

如果被保护为可读,你写就出现段错误!

4.访问权限,非法访问

参见3.

5.多线程对共享指针变量操作

不仅c/c++,android中、java程序中有可能也会出现jvm崩溃哦,那检查下多线程的共享变量吧!

如有错误,请不吝赐教.

你的java/c/c++程序崩溃了?揭秘段错误(Segmentation fault)(3)的更多相关文章

  1. golang 程序 在linux 出现 段错误

    刚做的 golang 程序 发布到linux 竟然出现 段错误 原因是 内核版本过低,请升级内核

  2. 转载:Linux服务器Cache占用过多内存导致系统内存不足最终java应用程序崩溃解决方案

    原文链接: https://blog.csdn.net/u014740338/article/details/66975550 问题描述 Linux内存使用量超过阈值,使得Java应用程序无可用内存, ...

  3. Android 程序崩溃后的处理

    在应用发布以后,由于安卓机型的千差万别 ,可能会出现各种各样的问题,这时候如果我们可以将这些信息收集起来,并进行修改就很不错了.下面就来讨论一下怎么处理程序崩溃以后,错误信息的手机. Java中已经提 ...

  4. 捕android程序崩溃日志

    主要类别: package com.example.callstatus; import java.io.File; import java.io.FileOutputStream; import j ...

  5. android 程序崩溃crash日志的捕捉

    android 程序崩溃crash日志的捕捉 之前在项目开发过程中,一直会遇到程序崩溃了,但是测试組的哥哥们又没及时的导出日志.... 后来在诳群的时候听别人说起,腾讯有那么一个叫bugly的东西 将 ...

  6. android程序崩溃后重启

    有时候由于测试不充分或者程序潜在的问题而导致程序异常崩溃,这个是令人无法接受的,在android中怎样捕获程序的异常崩溃,然后进行一些必要的处理或重新启动 应用这个问题困恼了我很久,今天终于解决了该问 ...

  7. Android程序崩溃异常收集框架

    最近在写Android程序崩溃异常处理,完成之后,稍加封装与大家分享. 我的思路是这样的,在程序崩溃之后,将异常信息保存到一个日志文件中,然后对该文件进行处理,比如发送到邮箱,或发送到服务器. 所以, ...

  8. Android在程序崩溃或者捕获异常之后重新启动app

    在Android应用开发中,偶尔会因为测试的不充分导致一些异常没有被捕获,这时应用会出现异常并强制关闭,这样会导致很不好的用户体验,为了解决这个问题,我们需要捕获相关的异常并做处理. 首先捕获程序崩溃 ...

  9. Android 应用程序崩溃日志捕捉

    程序崩溃是应用迭代中不可避免的问题,即使有着5年或者10年经验的程序猿也无法完全保证自己的代码没有任何的bug导致崩溃,现在有一些第三方平台可以帮助我们搜集应用程序的崩溃,比如友盟,详情如下图 虽然能 ...

随机推荐

  1. 关于Web安全的那些事(XSS攻击)

    概述 XSS攻击是Web攻击中最常见的攻击方法之一,它是通过对网页注入可执行代码且成功地被浏览器执行,达到攻击的目的,形成了一次有效XSS攻击,一旦攻击成功,它可以获取用户的联系人列表,然后向联系人发 ...

  2. aps.net webform框架下页面服务器端控件和html控件用法

    (1)select 下拉框 前端: <select name="gameserverlist" id="gameserverlist" runat=&qu ...

  3. 【转】SpringMVC Controller 介绍

    转自:原文url 一.简介 在SpringMVC 中,控制器Controller 负责处理由DispatcherServlet 分发的请求,它把用户请求的数据经过业务处理层处理之后封装成一个Model ...

  4. 利用gcc自带的功能-fstack-protector检测栈溢出及其实现【转】

    转自:https://www.cnblogs.com/leo0000/p/5719186.html 最近又遇到了一个崩溃,栈回溯非常怪异. /lib/i386-linux-gnu/libc.so.6( ...

  5. 苹果ANCS协议学习【转】

    苹果ANCS协议学习 转自:http://www.cnblogs.com/alexcai/p/4321514.html 综述 苹果通知中心(Apple Notification Center Serv ...

  6. 百度编辑器ueditor 字符限制

    百度编辑器ueditor 字符限制 默认只能输入10000个字符 修改 ueditor.config.js // ,wordCount:true //是否开启字数统计 // ,maximumWords ...

  7. 数据库SQL中case when函数的用法

    Case具有两种格式,简单Case函数和Case搜索函数.这两种方式,可以实现相同的功能.简单Case函数的写法相对比较简洁,但是和Case搜索函数相比,功能方面会有些限制,比如写判断式. 简单Cas ...

  8. Linux下创建软Raid

    1- Linux下创建软Raid   步骤1.创建磁盘,并转换为fd #fdisk /dev/sdb //这里使用新的磁盘sdb 然后输入n ,创建分区 使用默认的起始点 输入大小为+100M 然后重 ...

  9. C++ code:低级编程

    1 C编程 所谓低级编程,是相对于面向对象或基于对象的抽象层次更高的高级编程而言,就是: (1)不用C++STL的资源库,尽量减少内在的创建.调用.分配等的开销: (2)对程序管辖的内存进行直接操作访 ...

  10. Visual C++中最常用的类与API函数

    这篇文章能让初学者快速了解visual C++ MFC中常见的核心的类与函数,虽然全部看下来有点枯燥,但对初学者快速了解MFC的框架结构很有好处. 常用类 CArchive类:用于二进制保存档案 CB ...