进程在运行过程中遇到逻辑错误, 比如除零, 空指针等等, 系统会触发一个软件中断.
这个中断会以信号的方式通知进程, 这些信号的默认处理方式是结束进程.
发生这种情况, 我们就认为进程崩溃了.
进程崩溃后, 我们会希望知道它是为何崩溃的, 是哪个函数, 哪行代码引起的错误.
另外, 在进程退出前, 我们还希望做一些善后处理, 比如把某些数据存入数据库, 等等.
下面, 我会介绍一些技术来达成这两个目标.
1. 在core文件中查看堆栈信息
如果进程崩溃时, 我们能看到当时的堆栈信息, 就能很快定位到错误的代码.
在 gcc 中加入 -g 选项, 可执行文件中便会包含调试信息. 进程崩溃后, 会生成一个 core 文件.
我们可以用 gdb 查看这个 core 文件, 从而知道进程崩溃时的环境.
在调试阶段, core文件能给我们带来很多便利. 但是在正式环境中, 它有很大的局限:
1. 包含调试信息的可执行文件会很大. 并且运行速度也会大幅降低.
2. 一个 core 文件常常很大, 如果进程频繁崩溃, 硬盘资源会变得很紧张.
所以, 在正式环境中运行的程序, 不会包含调试信息.
它的core文件的大小, 我们会把它设为0, 也就是不会输入core文件.
在这个前提下, 我们如何得到进程的堆栈信息呢?
2. 动态获取线程的堆栈
c 语言提供了 backtrace 函数, 通过这个函数可以动态的获取当前线程的堆栈.
要使用 backtrace 函数, 有两点要求:
1. 程序使用的是 ELF 二进制格式.
2. 程序连接时使用了 -rdynamic 选项.
-rdynamic可用来通知链接器将所有符号添加到动态符号表中, 这些信息比 -g 选项的信息要少得多.
下面是将要用到的函数说明:
#include <execinfo.h>
int backtrace(void **buffer,int size);
用于获取当前线程的调用堆栈, 获取的信息将会被存放在buffer中, 它是一个指针列表。
参数 size 用来指定buffer中可以保存多少个void* 元素。
函数返回值是实际获取的指针个数, 最大不超过size大小
注意: 某些编译器的优化选项对获取正确的调用堆栈有干扰,
另外内联函数没有堆栈框架; 删除框架指针也会导致无法正确解析堆栈内容;
char ** backtrace_symbols (void *const *buffer, int size)
把从backtrace函数获取的信息转化为一个字符串数组.
参数buffer应该是从backtrace函数获取的指针数组,
size是该数组中的元素个数(backtrace的返回值) ;
函数返回值是一个指向字符串数组的指针, 它的大小同buffer相同.
每个字符串包含了一个相对于buffer中对应元素的可打印信息.
它包括函数名,函数的偏移地址, 和实际的返回地址.
该函数的返回值是通过malloc函数申请的空间, 因此调用者必须使用free函数来释放指针.
注意: 如果不能为字符串获取足够的空间, 函数的返回值将会为NULL.
void backtrace_symbols_fd (void *const *buffer, int size, int fd)
与backtrace_symbols 函数具有相同的功能,
不同的是它不会给调用者返回字符串数组, 而是将结果写入文件描述符为fd的文件中,每个函数对应一行.
3. 捕捉信号
我们希望在进程崩溃时打印堆栈, 所以我们需要捕捉到相应的信号. 方法很简单.
#include <signal.h>
void (*signal(int signum,void(* handler)(int)))(int);
或者: typedef void(*sig_t) ( int );
&n