你的java/c/c++程序崩溃了?揭秘段错误(Segmentation fault)(3)

前言

接上两篇:

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

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

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

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

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

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

  1. dmesg
  2. strace
  3. gdb
  4. 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崩溃哦,那检查下多线程的共享变量吧!

如有错误,请不吝赐教.

时间: 2024-07-29 00:17:54

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

Qt中遇到QLineEdit设置文本setText时程序崩溃

参考例子:https://blog.csdn.net/u014252478/article/details/80377103学习QT Socket编程.运行TcpServer时程序崩溃了,讨厌的Segmentation fault又出现了! 调用栈显示QLineEdit调用setText的位置,很可能是QLineText指针有问题了! 将QNetworkInterface().allAddresses().at(1).toString()级联调用拆开调试,获取ip字符串没有问题,判断ui->IP

Reflex框架新特性-本地事务,再也不用担心程序崩溃了!

我们知道程序的可用性或者说健壮性非常重要,如果在用户使用的过程中,出现了程序崩溃,或者数据错误都是灾难性的. 为了最小化出错的概率,我们想各种办法来减错.容错.纠错.不管怎么减错,比如说提高代码质量.测试驱动开发.大量测试等等,但仍不可避免,还是有各式各样的错误出现.尤其是有UI,需要用户参与的话,错误会更多,因为你不知道用户到底是怎么使用应用程序的.所以容错不可避免! 在Java中,错误有两种类型,Error和Exception.我们能处理的主要是Exception.如果要容错,首先需要把错误

未捕获异常,现实程序崩溃闪退

碰到程序崩溃时,闪退效果,不会提示"xxx程序异常,退出程序".这样的效果就要使用到未捕获异常来实现,这里记录了我的一个写法.其实原理很简单,设置程序的未捕获异常监听,实现监听的一个方法,在该方法中现实直接没有提示的退出程序. 捕获异常工具类 package com.tdh.http; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.Thread.UncaughtExceptionHan

Android 关于没有处理异常导致应用程序崩溃的处理

大家在调试过程中,经常会遇见莫名其妙的程序崩溃,那我们应该怎么查看这些崩溃的详情呢? 经百度,有以下方法: 首先是编写两个类 CrashApplication package com.example.endtwo; import android.app.Application; public class CrashApplication extends Application { @Override public void onCreate() { super.onCreate(); Crash

Android记录程序崩溃Log写入文件

将导致程序崩溃的堆栈调用Log写入文件,便于收集bug.在调试安卓程序,由于某些原因调试时手机不能连接PC端,无法通过IDE查看程序崩溃的Log,希望log能够写入文件中,对于已经发布的App可以通过该功能收集Bug. 01import java.io.FileNotFoundException; 02import java.io.FileOutputStream; 03import java.io.IOException; 04import java.io.PrintStream; 05imp

Android socket 学习记录 之 执行new socket(ip, port)程序崩溃

这段时间在学习Android的socket编程,我不是专做APP的,做的是bootloader.驱动.hal.framework这个线的,也就是系统搭建和功能优化设计.为了打通这整条线,为此学习了不少东西,今天把Android的socket学习记录一下,以防止以后会出现这样的低级错误. 我这里是在极客学院的源码基础上做的自己的一些添加和修改,学习开始不就是先会修改么,举一反三,自然就很快学会了.由于看过视频和资料后就迫不及待的按照自己的想法想做一个功能,但是遇到麻烦了,就是执行new socke

捕获android程序崩溃日志

主要类: package com.example.callstatus; import java.io.File; import java.io.FileOutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.Thread.UncaughtExceptionHandler; import java.lang.reflect.Field; import java.net.Unkn

程序崩溃, 没有任何提示!没有异常!没有任何错误日志。。。

今天竟然遇到这样一个问题, 好好的tomcat, 执行上传的时候, 原因竟然是hibernate的这一行: Department department = departmentService.queryByPK(Department.class, departmentId); hbm 文件如下: <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE hibernate-mapping PUBLIC &qu

当您的应用程序崩溃时,您希望知道所有一切信息

为何应该关心崩溃报告 设想一个用户安装了一个移动应用程序的情况.由于抱有很高的预期,该应用程序被频繁地使用,或许在前几个星期一天使用好几次.应用程序创建的数据以及用户投入该应用程序中的精力在不断积累.然后,突然之间,应用程序崩溃了.用户的数据现在被损坏或丢失.尽管等待了好几天,该应用程序仍未更新,这些问题仍未得到解决.用户为该应用程序提供了一星评价和批评性评论,然后放弃了该应用程序.即使以后某个时刻提供了修复程序,用户也不可能返回再使用该应用程序. 作为应用程序供应商,您投入了数月时间来设计和开