当程序突然暴毙崩溃之后我们首先要查明白三件事:
- 程序什么时候死的;
- 程序死在哪里(哪个函数,哪行代码);
- 怎么死的。
为了查明白这三件事情最常用的方法是:
- 取出内核在程序临终前生成的core文件,core文件就是程序将死之时留下的遗言;
- 通过反汇编工具和core还原案发现场。
关于core dump的详情见连接–wiki
但是这种方式最大的缺点就是不够智能,需要手动反汇编,操作麻烦,容易让人眼花。如下是一段汇编代码,看着眼花。
00000048 :
48: e2422001 sub r2, r2, #1
4c: e1520003 cmp r2, r3
50: 1afffffc bne 48
54: e1a0f00e mov pc, lr
58: 11111111 tstne r1, r1, lsl r1
5c: e0200240 eor r0, r0, r0, asr #4
60: e0200244 eor r0, r0, r4, asr #4
64: 00895440 addeq r5, r9, r0, asr #8
那如何才能更加智能地破案,更加直观的还原现象呢?
为了解决这个问题,第一步我们要知道什么发生了程序异常并及时赶到现场,然后保护现场。
一支穿云箭,千军万马来相见。当内核一不小心碰到致命异常,通常会发送一个信号。下面列出几种常见异常场景及其对应的信号。
信号名 | 场景 |
---|---|
sigsegv | 大名鼎鼎段错误,无效内存引用,比如访问一个未经初始化的指针, 访问空指针,栈溢出等等 |
sigfpe | 算术运算异常,如除零异常,浮点溢出等 |
sigbus | 硬件故障,某些类型的的内存故障,如未对齐的内存访问 |
sigill | 执行了非法硬件指令,未知指令 |
sigabrt | 调用abort导致进程异常终止,double free |
– | – |
接下来的问题是,当收到这些信号的时候要做什么。在linux系统中用来处理信号的函数叫信号处理程序,对于上面的信号linux默认的处理程序会杀死进程。我们可以使用sigaction函数来修改信号的处理程序,或者说给信号注册一个我们自己的处理程序。我们可以在这个处理程序中将线程的上下文,及案发现场保存到磁盘文件中。
#include
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
struct sigaction
{
void (*sa_handler) (int); //信号处理函数,
sigset_t sa_mask; //信号屏蔽字,用来设置在处理该信号时暂时将sa_mask 指定的信号搁置
int sa_flags; //指定对信号进行处理的选项,这里主要使用两个,见下表
void (*sa_restorer) (void);
}
sa_onstack | 如果利用sigaltstack()建立信号专用堆栈,则此标志会把所有信号送往该堆栈。 |
---|---|
sa_siginfo | 提供附加信息,一个指向siginfo结构的指针以及一个指向进程上下文标识符的指针 |
sa_onstack选项的作用是指定信号处理程序在一个新的栈上面去运行,如果直接在原有的栈上面运行有可能会破话现场,而且旧的栈可能已经遭到破坏而不能使用。
sa_siginfo的作用则是让内核在发送信号的时候要捎带一个上下文,用来作为信号处理程序的参数。
如果设置了sa_siginfo标志,那么按下列方式调用信号处理程序:
void handler(int signo, siginfo_t *info, void *context);
siginfo_t结构包含了信号产生原因的有关信息。该结构的大致样式如下所示:
struct siginfo {
int si_signo; /* signal number */
int si_errno; /* if nonzero, errno value from */
int si_code; /* additional info (depends on signal) */
pid_t si_pid; /* sending process id */
uid_t si_uid; /* sending process real user id */
void *si_addr; /* address that caused the fault */
int si_status; /* exit value or signal number */
long si_band; /* band number for sigpoll */
/* possibly other fields also */
};
信号处理程序的context参数是无类型指针,它可被强制转换为ucntext_t结构类型,用于标识信号传递时进程的上下文。
在类system v环境中,在头文件< ucontext.h > 中定义了ucontext_t结构体。而ucontext是一个协程库。
ucontext_t结构体则至少拥有以下几个域:
typedef struct ucontext {
struct ucontext *uc_link; //指向下一个上下文的指针,当要实现协程库是有用
sigset_t uc_sigmask;//阻塞信号集合
stack_t uc_stack;//上下文中使用的栈
mcontext_t uc_mcontext;//保存的上下文的特定机器表示,包括调用线程的特定寄存器
...
} ucontext_t;
uc_stack字段描述了当前上下文使用的栈,至少包括下列成员:
void *ss_sp; //栈顶指针
size_t ss_size; //栈大小
int ss_flags; //flags
linux 系统提供了backtrace库来给应用调试,能够获取当前栈帧并将栈帧转换成函数名称,方便快速定位到出问题的函数。
函数 功能
backtrace 获取栈帧
backtrace_symbols 将栈帧转换成函数名称
backtrace_symbols_fd 将栈帧转换成函数名称并输出到指定文件
使用backtrace需要在编译的时候要添加-rdynamic编译参数,实现依赖于栈指针(fp寄存器),在gcc编译过程中任何非零的优化等级(-on参数)或加入了栈指针优化参数-fomit-frame-pointer后可能不能正确得到程序栈信息;
解析栈帧的另一种方法是加载解析elf文件,elf文件中的符号表符号表保存了查找程序符号、为符号赋值、重定位符号所需的全部信息,因此可以反过来利用符号表查找栈帧对应的符号。对于动态链接库需要配合maps文件。
这种方法的缺点是需要加载elf文件,流程比较复杂。
预知后事如何请听下回讲解