菜鸟笔记
提升您的技术认知

Linux信号处理程序的一个应用—定位故障

当程序突然暴毙崩溃之后我们首先要查明白三件事:

  1. 程序什么时候死的;
  2. 程序死在哪里(哪个函数,哪行代码);
  3. 怎么死的。

为了查明白这三件事情最常用的方法是:

  1. 取出内核在程序临终前生成的core文件,core文件就是程序将死之时留下的遗言;
  2. 通过反汇编工具和core还原案发现场。

关于core dump的详情见连接–wiki
但是这种方式最大的缺点就是不够智能,需要手动反汇编,操作麻烦,容易让人眼花。如下是一段汇编代码,看着眼花。

00000048 <Sherlock-Holmes>:
  48:	e2422001 	sub	r2, r2, #1
  4c:	e1520003 	cmp	r2, r3
  50:	1afffffc 	bne	48 <Sherlock-Holmes>
  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 <signal.h>

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 <errno.h> */
    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文件,流程比较复杂。

	预知后事如何请听下回讲解