c++程序异常定位方法

文章目录

      • (一)、core.dump
      • (二)、dmesg
      • (三)、pstack
      • (四)、strace
      • (五)、valgrind

对于c++程序来说,以segment fault为代表的程序异常行为前奇百怪,没有一套比较丰富的工具集去对付他们,在处理实际问题时就会显得捉襟见肘。本文列举几种程序异常的定位方法。

(一)、core.dump

最有效的一种方法,可以在程序挂断之后通过core文件定位程序错误的地方,而且不影响程序运行。需要以下三个步骤。

  1. 在编译可执行的时候需要给g++加参数-g,否则不能够定位到具体的行数。

如何判断一个可执行是否加了参数-g编译出来的?
readelf -S sonodebug |grep debug,若有输出则是加了-g编译的

  1. 允许系统生成core file
    开启方法为在 /etc/profile 文件中加入ulimit -c unlimited,查看系统是否允许生成core文件可以用命令ulimit -c查看,或者用ulimit -a查看其中的core file size 行。有一点需要注意的是,进程到底是否会在down掉时生成core文件不取决于当前系统是否允许生成core文件,还需要在启动程序时就开启core文件。查看要分析的进程是否会产生core文件的方法:
ps -ef|grep a.out#查询到进程号
cat /proc/pid/limits|grep "Max core file size"#为0则不运行

Max core file size 字段的Soft Limit和Hard Limit值,才是标明了该进程是否真的能生成core文件。

  1. gdb带core文件运行可执行文件
gdb ./aout core.file

gdb调试core文件的一些常用命令:

  • 查看案发现场情况,函数调用关系,一层一层顺藤摸瓜。
(gdb)bt          //这个命令会列出程序崩溃时的堆栈信息,一层一层会有标号  #0  #1  #2 ....
  • 想要局部放大,具体查看某一层具体的堆栈变量等信息
(gdb)f N //其中N是项查看的层数,在bt命令里会有打印
  • 来到某一层之后,想查看具体信息用以下命令
(gdb)info args//打印当前函数的参数以及其值
(gdb)info locals//打印所有的局部变量
(gdb)info catch//打印当前函数的异常处理信息
(gdb)p var//打印具体的某一个变量值

gdb+core文件的方法,可以定位到常见的绝大数错误。但也存在一些不好解决的问题

  • 由于某些原因,例如程序退出时破坏了堆栈等,在用gdb的bt命令打印堆栈信息时,会出现不能定位的情况,出现全是 ?? 的情况,让人不知所以然。以下这个例子就是这种情况,当时用gdb+core不管用,gdb前台运行结果一样,最终使用valgrind定位到了问题。
  gdb ./a.out core.out
 ……………… ………………
 
 Program terminalted with signal 11, segmentation fault.
 #0 __memmove_ssse3_back () at ../sysdeps/x86_64/multiarch/memcpy-ssse3-back.S:1658 
 1658		movdqu  -0x40(%rsi),%xmm4 
 (gdb) bt
 #0 __memmove_ssse3_back () at ../sysdeps/x86_64/multiarch/memcpy-ssse3-back.S:1658
 #1 0x0000000000000000 in ?? () 
 (gdb) quit
  • 由于运行环境和调试环境不同,出现定位不准确的问题。

(二)、dmesg

/var/log/message 中记录了程序的异常退出,dmesg也可以列出程序的段错误。dmesg + grep a.out 可以查出程序是否发生了segment fault。
dmesg中有哪些有用信息呢?它可以告诉我们程序的内存错误类型和错误位置。以下是可执行文件sowithdebug段错误之后,用dmesg |grep sowithdebug获得的错误信息。

sowithdebug[6847]: segfault at 28 ip 00000000004008cc sp 00007fff016d9400 error 4 in sowithdebug[400000+2000]

其中的ip后面的数字是错误的行数(4008cc), in 后面的是错误的文件(sowithdebug)。然后用objdump可以查错误的汇编代码,用addr2line可以定位到具体的行数。

dmesg |grep sowithdebug                    #查询错误文件及行数
objdump -d sowithdebug |grep 4008cc   	   #查询汇编指令
addr2line -e sowithdebug 4008c             #定位行数

error 4是错误的类型,表示用户态程序读操作访问越界。
error后边数字的含义:

bit2: 值为1表示是用户态程序内存访问越界,值为0表示是内核态程序内存访问越界
bit1: 值为1表示是写操作导致内存访问越界,值为0表示是读操作导致内存访问越界
bit0: 值为1表示没有足够的权限访问非法地址的内容,值为0表示访问的非法地址根本没有对应的页面,也就是无效地址

dmesg的不足之处:

  • 在可执行没有加 -g 编译时依然不能定位行数
    c++程序异常定位方法_第1张图片
  • 如果dmesg显示的出错文件是动态库so文件,问题位置依然比较难以定位
    这篇博文非常非常精彩的介绍了dmesge定位出错行数的一些方法,包括处理静态库和动态库的情况:《在Linux中如何利用backtrace信息解决问题》

(三)、pstack

要想清楚进程此刻正在忙什么,pstack是一个很好的工具。它可以告诉我们各线程的函数调用情况。

pstack PID  #PID既可以是进程号也可以是线程号

当pstack加进程号的时候,它会打印出所有的线程运行情况。如果加线程号,就打印此线程的函数调用情况。
c++程序异常定位方法_第2张图片
其中的LWP就是线程号。

pstack的弊端:
只是进程的一个静态照片,不能把握程序的整体运行流程。多次执行pstack,或者结合watch -n 0.1,通过“高频率的快照近似看视频”的方式,掌握程序的运行情况。但watch的最高频率是0.1s。

(四)、strace

如果说pstack是给各线程照了一张照片,那么strace就是给各线程录视频。
strace可以实时动态的查看程序的运行情况,使用起来也很方便。

ps -ef|grep a.out      #查出进程的PID
strace -p PID		   #更踪此进程的运行(既可以跟踪进程PID,也可以是线程PID)

strace 也可以直接运行程序:

strace -f -F -o out.log a.out   #a.out是可执行文件,out.log是运行结果

其中的 -f , -F 告诉strace同时跟踪fork和vfork出来的进程,否则strace只会跟踪主线程。

strace的弊端:
它打印的都是一些系统调用,需要对这些系统调用有比较熟悉的掌握,才能清楚strace告诉我们的到底是什么。
这篇博文很精彩的介绍了strace的一些使用方法,很巧妙的解决一些问题:《strace命令详解》

(五)、valgrind

valgriind是一个代码动态检测工具,它是一个大的工具集,有几个小组件。其中比较常用的是memcheckhelgrind

  1. memcheck检测内存问题
valgrind --leak-check=full --show-reachable=yes --trace-children=yes --log-file=./valgrind.log  ./a.out  

–leak-check=full指的是完全检查内存泄漏,–show-reachable=yes是显示内存泄漏的地点,–trace-children=yes是跟入子进程。
以上介绍的我遇到的gdb+core出现的不能定位问题,gdb只仍给我一行又一行的问号问题,就是用valgrind的找到原因的。用以上参数运行程序,最后的日志会非常庞大而杂乱,可以重点关注其中的内存写(write)错误,程序断掉一般都是这些错误导致的。

  1. helgrind检测线程问题
    用工具helgrind检测线程问题:
valgrind --tool=helgrind --trace-children=yes ./a.out

它可以检测到大部分的共享资源竞争。错误类型也分的比较细致。

  1. 检测内存泄漏
    运行命令同 1. memcheck检测内存问题 ,但是需要注意的是要让valgrind运行结束,他才会给出内存报告。所以,如果我们的程序是一个可以自动推出的程序,就不会存在问题,等程序运行完毕,valgrind也就会推出,内存报告就给出了。但是如果我们的程序是一个不会自己退出的程序,就需要用killall 命令杀死valgrind,以让其生成内存报告。然后在valgrind的输出中grep “definitely lost”,是内存泄漏最严重的地方,是必须修改的地方。如果valgrind报告因为错误太多超过1千,不再显示新的错误,就需要修改参数改为不限制错误数量。
killall memcheck-    #先用top看一下valgrind实际运行的进程名称,killall杀死

killall 默认发送SIGTERM(15),如果用kill -9 直接杀死 memcheck是不能生成内存报告的

用valgrind定位程序问题的弊端:

  1. valgrind是动态工具,用valgring带程序跑,必须要等到问题复现,valgrind才能报告给我们有用的信息。
  2. valgrind带程序跑之后,会大大降低程序的运行速度。如果在生产环境上调试,这有可能是一个必须忍受的问题。

c++程序的异常定位,是一个非常复杂的问题,写出没有bug的c++代码,也是一项几乎不可能完成的任务。掌握排错工具和方法,就是一个非常重要的技能。后续实践中又好的排错工具和方法,会继续更新本文。

你可能感兴趣的:(C/C++)