前言
程序崩溃令人很崩溃,特别是让人找不到原因的崩溃,但是合适的工具可以帮助人很快的定位到问题,在AI基础能力ASR服务开发时,找到了一种比较实用和简单的内存崩溃的dump分析工具breakpad,
可以帮助在Linux下C++开发程序时发生崩溃快速定位
breakpad简介
Google breakpad是一个非常实用的跨平台的崩溃转储和分析模块,支持Linux、mac、solaris、windows。可以借助Google breakpad来捕捉程序程序崩溃的错误报告。即在程序崩溃时会生成dump文件。
而dump文件是进程的内存镜像,能够保存程序中断时的进程状态,让我们在程序崩溃后能够了解具体原因。
breakpad 结构和原理示意图
获取breakpad
breakpad在github网站上的地址为: GitHub - google/breakpad: Mirror of Google Breakpad project
在ASR工程化服务中也已经集成好了breakpad库breakpad.zip
breakpad使用
代码示例
网上搜索的办法不一而足,缺陷较多,很多没有兼顾到的地方,而且使用过程中有许多需要注意的地方;在AI基础能力开发过程中,我们已经形成了比较简单和易操作的方式来使用breakpad,使用breakpad需要嵌入的代码十分简单,以下是示例:
breakpad示例
#include "breakpad/src/client/linux/handler/exception_handler.h"
#include <string>
bool DumpCallback(const google_breakpad::MinidumpDescriptor &descr, void *context, bool succeeded) {
return succeeded;
}
int main(int argc, char** argv){
//breakpad 只接受绝对路径的dump dir
std::string dump_dir = "/home/alex/dumpdir";
//为breakpad创建dump存放目录
mkdir(dump_dir.data(), 0775);
google_breakpad::MinidumpDescriptor descriptor(dump_dir);
// minidump文件目录
google_breakpad::ExceptionHandler eh(descriptor, NULL, CppProcess::DumpCallback, NULL, true, -1);
do_your_stuff();
}
编写代码工程时,包含breakpad的头文件,并指定程序链接libbreakpad_client.a库
如上,在为breakpad需要生成dump文件准备好相应的目录后,创建一个descriptor和eh实例即可,在程序崩溃后,breakpad会在/home/alex/dumpdir目录下创建一个后缀为dump的文件
注意事项
- breakpad所创建的实例,descriptor和eh,属于栈上的对象,在其生命期内可以接受异常,它要尽可能早的创建,和尽可能晚的关闭,基于这个原则,最好是把它放在main函数的开头
- breakpad生成dump的目录需要传入绝对路径
- 对于 DumpCallback回调函数,应当尽量写的简短,就像内联函数一样,因为程序在崩溃后所能做的操作有限,某些阻塞性的系统调用如申请内存,调用其他库的函数等操作可能无法完成
- 为了生成有用的信息,编译程序时,需要加上-g编译选项,使程序和库包含调试信息
崩溃分析
当程序发生崩溃时,通过前面的方式获取到dump文件后,接下来就是分析崩溃文件,找到程序崩溃的位置和原因,需要做以下几步:
-
从breakpad的结构和原理示意图中可以了解到,要得到最终的信息,需要结合程序和库的符号信息,和dump文件,来生成可读的栈信息,breakpad提供了从程序和库中分离出符号的工具,附带在breakpad库中,会随着breakpad库一起编译出来,
工具程序是dump_syms,下面是一个从程序或者库文件中分离出符号信息(注意编译时的-g选项)的示例代码:分离程序或库中的符号信息
#!/bin/bash for program in `ls ./` do #生成相应库文件的sym文件 ./dump_syms ./$program > $program.sym #获取属于该库文件的一个唯一编号,如00A5F6B1C92FB3657CC65C7B1C4E62920 uuid=`head -n1 $program.sym | awk '{print $4}'` #获取该文件在符号信息中的名称,可能和程序名一致,如http_service prodir=`head -n1 $program.sym | awk '{print $5}'` #创建存放sym文件的目录 mkdir -p ./symbols/$prodir/$uuid #将符号文件移动到相应位置下 mv $program.sym ./symbols/$prodir/$uuid/$prodir.sym done
以上是一个小脚本,可以为当前目录下所有文件生成符号信息,为后续做准备。
-
当程序崩溃时,会生成一个dump文件,一般是这种格式:59638c7c-ae27-4d04-bf4c4eac-75e328be.dmp在做好了上一步准备后,下一步是生成人类可读的栈信息,仍然需要使用从breakpad库中编译得到的一个工具命令minidump_stackwalk,使用方法如下:
-
生成堆栈信息
./minidump_stackwalk 59638c7c-ae27-4d04-bf4c4eac-75e328be.dmp symbols/
紧接上一步,就会生成本次程序崩溃的相关信息,以下是对于栈崩溃的分析示例:
崩溃栈示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
|
示例开头描述了操作系统的一下信息:
Operating system: Linux
0.0.0 Linux 4.15.0-52-generic #56-Ubuntu SMP Tue Jun 4 22:49:08 UTC 2019 x86_64
紧接着描述CPU指令集
CPU: amd64
family 6 model 85 stepping 4
1 CPU
崩溃原因描述:这里是进程的SIGSEGV/SEGV_MAPERR信号,一般是段错误造成的崩溃
Crash reason: SIGSEGV /SEGV_MAPERR
Crash address: 0x8
Process uptime: not available
其后就是我们所需要关注的程序的栈信息,它以线程thread为分组,从编号0开始向下递增,每一个编号的部分表示一层函数调用栈,也就是0层是本线程的最顶层栈,标号行也描述了栈调用的函数所在的库,文件和行号,以方便程序员定位和分析崩溃的代码
如下示例,Thread 35 (crashed)表示在35号线程崩溃,0号栈是线程调用栈顶层,右侧描述了该函数在libtlvkaldi.so库的tlv_kaldi_dec_sub_func.cc文件,函数名sub_func_add_timeinfo,在文件的34行,紧接着是该函数的栈的各寄存器的值,2号栈调用1号栈,1号栈
调用0号栈,崩溃发生在0号所在位置,此时即可分析代码,找出可能造成崩溃的原因
崩溃栈示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
breakpad与工程代码的管理
在实际工程应用中,编译出的二进制文件与符号表应当是分开存放的,二进制文件中不应当附带符号信息,从编译到部署到生产环境,大致分为以下几步:
1)编译代码后,使用dump_syms工具将编译出的二进制文件内的符号表(symblos)分离出来,按照版本存放
2)使用strip命令将编译后的二进制文件内的符号信息剥除
3)将剥除后的程序部署,后按照正常测试,上线
4)若运行过程中发生崩溃,生成了dump文件,将dump文件和1)中保存的相应版本的符号表信息按照前文所说的步骤获得栈崩溃上下文信息,然后分析崩溃原因
总结
工欲善其事,必先利其器!
在C++工程所生成的库和程序中,不应当附带符号信息,以防止逆向工程,而且带有符号信息会显著增加程序和库的文件大小,不方便传输;
可以在分离出符号信息并保存后,使用linux的stip命令将工程的C++程序和库的符号信息去除,缩小文件大小;