C,C++程序最常见的崩溃问题就是内存问题,内存越界,访问空指针,野指针等都会造成程序崩溃。Linux系统中当程序运行过程中出现非法操作,系统会先发送对应的错误信号,每种错误信号都有默认的处理方式,比如,当我们给一个空指针赋值的时候,系统会检测到这个内存错误,然后向进程发送SIGSEGV信号,该信号默认的处理方式是退出进程,这种情况下,只能看到进程挂掉,但无法定位错误。当出现这种问题的时候一般往往很难查找原因,下面介绍两种方式定位bug。

方法一:捕获系统信号

Linux操作系统提供了一组接口可以修改信号对应的默认回调。通过这组接口,当出现错误信号的时候,将程序的执行流程引导到我们自定义的回调函数中,在这里我们可以调用另外一组系统接口获得当前错误出现时的堆栈信息,有了堆栈信息,就可以很方便的定位到程序最后出错时的代码位置。

有些错误是致命的,无法再恢复到正常的程序流程,有些则时可以忽略的,比如SIGPIPE信号,可以忽略这个信号。

    __sighandler_t signal (int __sig, __sighandler_t __handler);void (*__sighandler_t) (int);

通过系统函数signal注册消息,第一个参数是要注册的系统消息,第二个参数是消息对应的回调函数。

回调函数是包含一个int参数无返回值的函数类型。

比如我们要捕获SIGSEGV信号,则需要在程序运行之前注册这个消息,如下:

     #include <signal.h>#include <execinfo.h>#include <stdlib.h>#include <stdio.h>void sighandler(int sig){// 处理消息回调}int main(){signal(SIGSEGV, sighandler);int* p = NULL;*p = 1;return 0;}

上面的代码编译之后,运行到*p = 1;这一行时就会出现段错误,sighandler函数会被调用。

接下来我们在sighandler函数中打印出当前程序执行的堆栈信息。

这里要用到下面两个函数:

    int backtrace (void **__array, int __size);char **backtrace_symbols (void *const *__array, int __size);

实现sighandler函数

    #include <signal.h>#include <execinfo.h>#include <stdlib.h>#include <stdio.h>void sighandler(int sig){void *pTrace[256];char **ppszMsg = NULL;int uTraceSize = 0;if (0 == (uTraceSize = backtrace(pTrace, sizeof(pTrace) / sizeof(void *)))){return;}if (NULL == (ppszMsg = backtrace_symbols(pTrace, uTraceSize))){return;}for (int i = 0; i < uTraceSize; ++i){printf("%s\n", ppszMsg[i]);}free(ppszMsg);exit(0);}int main(){signal(SIGSEGV, sighandler);int* p = NULL;*p = 1;return 0;}

运行命令gcc -o a.out test.cpp编译这个文件得到a.out目标文件,当我们运行这个a.out的时候会得到下面输出:

    ./a.out() [0x4006e5]/lib64/libc.so.6(+0x35270) [0x7f0c972d3270]./a.out() [0x40078a]/lib64/libc.so.6(__libc_start_main+0xf5) [0x7f0c972bfc05]./a.out() [0x4005e9]

这个便是堆栈信息,只不过这个堆栈信息里面是地址,而不是我们想要的符号名称。这时需要借助另外一个命令工具来还原具体的符号信息

addr2line

addr2line命令可以将地址转换成该地址对应的符号以及位置

当我们运行下面命令:

addr2line 0x4006e5 -e a.out -f

输出如下:

_Z10sighandleri
??:?

这里打印出了sighandler这个函数名,但是第二行信息没有。这是因为我们编译的目标文件里面没有调试信息,因此,无法正常显示。

我们增加-g编译参数,再试一次。

// 编译
$ gcc -g -o a.out test.cpp// 运行
$ ./a.out
./a.out() [0x4006e5]
/lib64/libc.so.6(+0x35270) [0x7fc43673f270]
./a.out() [0x40078a]
/lib64/libc.so.6(__libc_start_main+0xf5) [0x7fc43672bc05]
./a.out() [0x4005e9]// 查看0x4006e5地址
$ addr2line 0x4006e5 -e a.out -f
_Z10sighandleri
/home/test/test.cpp:12

这一次我们得到了我们想要的堆栈信息,我们把剩下的两个地址信息也打印出来

addr2line 0x4006e5 -e a.out -f
_Z10sighandleri
/home/test/test.cpp:12addr2line 0x40078a -e a.out -f
main
/home/test/test.cpp:35addr2line 0x4005e9 -e a.out -f
_start
??:?

我们只需要a.out程序相关的堆栈信息,因此我们只打印了三个地址的堆栈信息,通过这个堆栈信息,我们就可以定位到出错的位置,在test.cpp的35行,正好是*p = 1;这句代码。

方法二:core dump

Linux程序在崩溃的时候可以产生core文件,这个文件记录了程序崩溃时的状态,有了core文件,就可以还原发生崩溃时的场景,并且还可以使用gdb进行调试。

一般情况下,Linux系统默认是不开启core文件生成的,需要自己手动设置。

在命令行运行ulimit -a可以查看各种参数的系统限制值

core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 30224
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1048576
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 4096
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

第一行信息 core file size 就是系统控制产生core文件的大小,默认是0,即程序崩溃时不会产生core文件。

运行命令ulimit -c unlimited 将core文件大小设置为不受限制,这样当程序崩溃时就可以产生core文件了。

继续运行上面的程序,并没有产生core文件,这是因为捕获了对应系统消息之后,就不会产生core文件。

去掉信号捕获之后的程序:

    #include <stdio.h>int main(){int* p = NULL;*p = 1;return 0;}

运行gcc -g -o a.out test.cpp

注意这里仍然需要-g选项,使目标文件包含调试信息

再次运行a.out

 ./a.out
段错误(吐核)

再看看当前目录

a.out  core.80798  test.cpp

多了一个core.80798文件,这个文件就是程序崩溃时产生的core文件。

使用gdb a.out core.80798就可以进入gdb调试

gdb a.out core.80798
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-100.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/weiwei.wen/test/coretest/a.out...done.
[New LWP 80798]
Core was generated by `./a.out'.
Program terminated with signal 11, Segmentation fault.
#0  0x00000000004004fd in main () at test.cpp:6
6           *p = 1;
(gdb)

直接定位到了test.cpp第6行,执行 *p = 1;时出错。

这里可以用p命令(print的缩写)打印p值

(gdb) p p
$1 = (int *) 0x0

发现指针p的地址为0x0,是个空指针。

对于堆栈层数比较多的情况,使用bt命令打印所有堆栈信息,通过f命令查看每个堆栈帧的状态。

总结:

以上两种方式都可以定位崩溃bug,第一种方式稍微麻烦一点,需要注册消息回调,在消息回调里面获取堆栈信息,而且还需要通过addr2line命令再处理一次才能得到我们想要的信息。但也有好处,就是最终得到的堆栈信息数据量很小,可以通过脚本处理得到需要的信息,发送到后台管理系统,方便开发人员检查并处理。第二种方式可以完全还原出错的场景,因此可以获得更多的信息来定位bug。不方便的地方就是,core文件有可能很大,不方便传输,需要开发人员登陆到出错机器调试。两种方式各有好坏,使用哪种方式就看实际的应用场景了。

Linux C/C++程序崩溃bug调试方法相关推荐

  1. linux下开启程序崩溃生成core文件开关之ulimit详解

    运行服务器程序经常会出现崩溃,而我们不可能一天24小时都等着他出现.在实际运行中也不能总是使用gdb启动,毕竟gdb绑定运行会消耗很大一部分性能. 不过linux系统在程序崩溃时会生成一个coredu ...

  2. Ros 应用程序的多种调试方法

    Ros 应用程序的多种调试方法 关于ROS调试 对乌龟节点程序的调试: 第一种调试方式: rosrun --prefix 'gdb -ex run --args' turtlesim turtlesi ...

  3. Linux环境下段错误分析及调试方法

    年轻时的每一个段错误,都会成为你程序人生上的垫脚石.如果是还在学习阶段的同学,希望能先通过自己的判断来找出段错误的地方. 本篇文章系转载及整理,原文链接如下: http://www.cnblogs.c ...

  4. Linux平台Segmentation fault(段错误)调试方法

    1. 段错误是什么 一句话来说,段错误是指访问的内存超出了系统给这个程序所设定的内存空间,例如访问了不存在的内存地址.访问了系统保护的内存地址.访问了只读的内存地址等等情况. 2. 段错误的原因 段错 ...

  5. iOS 几种常用的 crash log 崩溃信息调试方法. (转载)

    前言:crash log 对 定位崩溃问题 ,并且不容易复现,尤其是及时对appstore 上正在运营的 app 的迭代改进来说 非常重要. 1 crash两种情况 1.1 测试环境下 追踪bug 1 ...

  6. iOS 几种常用的 crash log 崩溃信息调试方法

    前言:crash log 对 定位崩溃问题 ,并且不容易复现,尤其是及时对appstore 上正在运营的 app 的迭代改进来说 非常重要. 1 crash两种情况 1.1 测试环境下 追踪bug 1 ...

  7. linux系统c 如何使用教程,基于Linux操作系统的C语言编译和调试方法解析

    摘 要:文章先介绍了GCC编译器相关内容,包括GCC编译程序和GCC编译选项,随后介绍了GDB调试程序相关内容,包括GDB具体操作和GDB基础命令,最后介绍了C语言编写中的注意事项,希望能给相关人士提 ...

  8. iOS-几种常用的 crash log 崩溃信息调试方法

    前言:crash log 对 定位崩溃问题 ,并且不容易复现,尤其是及时对appstore 上正在运营的 app 的迭代改进来说 非常重要. 1 crash两种情况 1.1 测试环境下 追踪bug 1 ...

  9. java 崩溃日志_Android收集程序崩溃日志的方法

    安卓Android如何手机程序崩溃日志并上传到服务器呢?直接会用到Thread线程里面的UncaughtExceptionHandler接口方法,我们可以自定义一个类CrashHandler,代码如下 ...

最新文章

  1. readline 库简写版本,测试可用
  2. paip.mysql 5.6 安装总结
  3. Python基础—06-函数基础
  4. 安卓虚拟机与Hyper-V冲突
  5. MySQL存储过程语句(if,while)的使用
  6. windows远程禁止登陆
  7. 写你自己 android 多通道打包工具 可以包libs和.so文件
  8. 使用python制作ArcGIS插件(2)代码编写
  9. c#先进行uri解码_JavaScript、C# URL编码、解码总结
  10. sphinx系列之中文分词LibMMSeg安装(三)
  11. 【Ubuntu】ubuntu 16.04 设置root用户初始密码
  12. 使用Java制作一款简单的小游戏
  13. 指定selenium chrome下载文件路径
  14. 一种DC-DC转换器的分析
  15. Windows Server 2008 (IIS)
  16. 27.(cesium篇)cesium接入百度影像地图
  17. 创建虚拟机步骤以及开启电脑虚拟设置方法
  18. 彩色喷头程序开发项目外包
  19. 【数学基础知识】证明三角形的中线交于一点
  20. 信息技术服务风险评估

热门文章

  1. Hashicorp Vault(金库)
  2. python可以爬取wind数据库吗_如何利用Python来爬取近百万条数据?数据库会炸吧?...
  3. Win10如何更改C:\Users\下的用户名
  4. NI的LabView2022工具的安装与使用
  5. 计算机是怎么跑起来的——简记
  6. 大文件中返回频数最高的100个词
  7. Python爬虫之爬取酷狗音乐歌曲
  8. 第一章 常用半导体器件-----------------本征半导体
  9. 逻辑回归模型logistic原理详解
  10. 【程序源代码】微信小程序商城