C++程序core dump分析

一、前言

  • 对于C/C++程序员而言,在开发程序的过程中,遇到的最多的程序问题莫过于程序core dump了,也就是常说的程序core了。那么程序出core的情况有哪些的?如果程序core了之后,我们应该如何对这类问题进行定位呢?本文给出了程序出core的常见原因和定位方法。

二、 问题分类及定位方法

  • 为了给大家一个直观的认识,我们首先分析一下程序出core的常见原因及分类方法。通过这些分类,我们可以对分core的原因、定位方法有初步的认识。

  • 通常在程序出core之后,最常用的工具就是gdb,通过gdb工具分析core文件,可以得到程序在崩溃前的堆栈信息。通过对堆栈信息的分析,我们可以了解到程序的运行状态,从而分析判断程序崩溃的原因。

  • 根据core文件中程序的堆栈信息的完整程度、稳定复现的程度,可以将定位方法分为三类。每类问题的定位方法和原因也有所区别。

2.1堆栈完整且稳定

  • 大多数core文件都保留了完整的堆栈,我们可以使用gdb看到core在哪里,这类问题的定位通常都比较快速而容易。常见的错误有:
1)堆栈行本身代码有错误
  • 很多时候,结合出core文件,从堆栈行中可以直接看出错误原因。如,除零错误、参数为空指针、迭代器失效等问题。这类问题通过堆栈信息,直接可以看出core的原因和为止。

  • 通常在出现这类错误时,在gdb时程序会给出明确的提示信息。如,除零错误,gdb时提示的信息是“Program terminated with signal 8, Arithmetic exception”。结合堆栈信息中的函数调用关系,可以很快的定位问题。

2)堆栈行所在函数上下文有错误
  • 有些错误,并不是在有bug的代码处立即出core,而是在后面的逻辑中出错。只需要从出core的代码行往上多看几行,往往就行找到问题代码。比如:变量未初始化、内存溢出导致部分变量值被破坏。
3)堆栈行所在函数没有错误
  • 我们也经常会碰到堆栈行所在函数是稳定版本的基础库或某些久经考验的代码段,亦或是简单得几乎不可能出bug的代码,这时,我们就要看堆栈中更上层调用者的代码了。

  • 对于基础库等稳定代码,有必要了解其使用规则,检查应用场景和使用方式是否违反了基础库的要求。务必记得查看基础库的相关wiki资料或联系其提供者,很有可能基础库提供者早已公布了用户可能踩到的坑以及对应的解决方案,这比自己慢慢摸索的效率要高的多。

  • 另外一种可能就是野指针,特别是在使用系统库时。一个理论上不会失败的操作居然core了,那么在review代码时需要1)重点检查是否存在有野指针的风险;2)升级后的代码是否使用了可能被别人释放的资源;3)升级后的代码是否释放了别人可能使用的资源。通常需要通过增加开关和日志甚至回滚代码的方式进行分析。

2.2堆栈完整但不稳定

  • 有的时候,程序时好时坏,出core时堆栈的内容也并不稳定,这类问题的根因和堆栈行可能并没有紧密关联,排查有一些困难。

  • 这类问题,往往和多线程相关,可以尝试将并发数改为1,观察能否避免出现此类问题从而明确问题类型。应当重点检查各次出core的堆栈行是否存在多线程冲突风险,适当加锁加日志可以有效辅助问题排查。不过因为日志打印需要消耗一些性能,有可能增加详细日志打印后反而不再出core,这时可以该日志所在上下文中重点review。

  • 如果在串行时仍然出现此类随机core问题,则很有可能是某处存在内存越界问题,写坏了少量数据。此类问题的排查方式和前述的野指针问题有些相似,重点在于review近期改动的代码,通过增加开关和日志来辅助定位。

2.3堆栈被破坏

  • 最坏的情况无非是堆栈完全被写坏,这类问题的根因很固定,但具体到代码上却五花八门,排查极为困难。
1)编译环境与运行环境
  • 如果近期存在对依赖库和编译参数的变更,则它们的嫌疑非常大,建议首先尝试回滚相关修改,验证问题是否恢复。

  • 对于新的程序或新的机器,编译环境和运行环境的差异往往也是此类问题的罪魁祸首,应当仔细排查其差异,或使用一致的环境进行验证。

2)内存越界
  • 排除环境问题后,堆栈破坏的根本原因基本上可以确定是内存越界,通常是代码某处不小心写坏内存,而且是直接连函数调用堆栈都写坏了,这类问题的排查某种程度上可以说是基本靠猜。

  • 排查方式与前述的排查野指针以及内存越界的方法比较相似,但一个比较大的难点是,没有堆栈信息,想猜都难以下手。

  • 二分回滚代码是比较推荐的一种排查方式,这样可以将问题锁定到某一些具体的代码变更,之后再仔细review这部分代码,就比较有希望找到问题的根因了。

三、案例分析

  • C/C++程序出core的行为相对固定,通过对我们收集问题的分析,共将程序出core的原因分为了5类。下面将对这些问题进行分类,发现问题的共性和特性,为以后的问题定位提供思路。由于篇幅问题,本次我们先介绍3类问题。

3.1异常处理不合理

  • 程序中总是有很多地方涉及异常处理,稍有不慎就会给程序埋下出core的隐患。
1)函数的返回值做适当的处理
  • 对于所有可能失败的操作,都必须检查返回值,当返回值不符合预期时,应当进行合适的处理,通常进行的操作有:

    a.记录日志

    b.中断操作

    c.必要的数据清理

    d.跳转到到合适的位置

2)指针为空
  • 在C/C++代码中,最容易出问题的就是指针,比如申请内存或打开文件等操作,没有判断返回的指针是否为空就直接使用;比如清理数据时,释放了指针所指向的内存地址,但没有将指针置空,导致其成为野指针。
3)异常未正常捕获
  • 有的时候,执行失败是通过抛出异常来传递而不是通过返回值传递,这时应当正确进行捕获和异常处理,避免程序直接崩溃。

3.2多线程/多进程

  • 随着多核CPU的普及,单线程的程序现在已经基本绝迹了,而多线程/多进程编程,很考验开发者的基本功,在大型程序中,一不小心就会在并发上栽跟头。
1)使用不可重入函数
  • 最常见的问题,是在多线程中使用了非线程安全的库函数,比如strtok、gethostbyname等,在多线程编程中,应当时刻保持警惕:这个函数是否是线程安全的?它有没有对应的线程安全版本?通常来讲,多数非线程安全的库函数,都会有一个带_r后缀的线程安全版本。
2)临界区保护不到位
  • 试图在多线程中访问\修改公共资源时,必须合理设计临界区,既要保证多个线程互不冲突,又要尽量减小临界区以防止性能下降,还要避免死锁等问题。代码片段2是一段没有进行恰当的临界区保护的代码。

3.3第三方库

  • 实际开发工作中,代码复用是非常有必要的,开发者往往需要使用第三方库(包括外部开源库以及由其它部门或产品团队开发的公共库),这时,应当至少阅读对应的文档和wiki,熟悉接口,明确使用时需要遵守的规则,并且对其可能存在的坑有所了解。
1)protobuf相关
  • google出品的protobuf在业界广泛使用,时常见到有开发者直接修改了proto文件中一些字段的类型,并导致上线后上下游无法兼容产生问题,如果遵守protobuf相关的增删改字段的规则,根本不会发生这种问题。
2)nginx插件
  • nginx是一款广泛使用的http和反向代理服务器,很多时候我们会根据需求开发nginx插件,曾有开发者使用nginx共享内存时,没有检查同名内存池直接开辟新内存池,导致reload时发生内存泄漏,最终OOM(Out Of Memory)。
3)函数返回值
  • 有些公共库,它的一个函数在成功时返回可用的指针,在失败时返回的却不是空指针,而是某个错误码,需要能够通过给定的函数解析出错误的原因。如果使用者对使用方法不熟悉,只进行了空指针检查,导致程序出错。例如在公共库的mcpack中就有这样一系列函数,如pt = mc_pack_get_str(req_pack, “pt”)这样的调用。pt可以是一个指针,也可以是一个错误码,如果不做判断,直接把pt当指针使用在不合适的地方,程序就出core。

  • 对于公共库的问题,建议开发者多查阅文档,了解各种公共库的标准用法。

四、总结

  • 本文对收集到的C/C++程序出core的案例进行了分类和整理,给出了一些建议,希望对大家有所启发。另外,也希望大家开发程序进行,做到规范开发。
  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值