在Unix-like系统中,所有的崩溃都是编程错误或者硬件错误相关的,系统遇到不可恢复的错误时会触发崩溃机制让程序退出,如除零、段地址错误等。
异常发生时,CPU通过异常中断的方式,触发异常处理流程。不同的处理器,有不同的异常中断类型和中断处理方式。linux把这些中断处理,统一为信号量,可以注册信号量向量进行处理。
常见的空指针奔溃时,我们看看ARM平台下的linux是如何处理的。ARM支持7种异常。空指针属于数据中止异常,尝试读写一个非法内存地址时会发生。
step 1:中断向量调用do_kernel_fault
空指针在ARM平台上属于data abort异常。当中断发生时,会在中断模式下,保存现场信息,设置相关标志位。然后进入SVC模式,根据中断向量表跳转至指定的异常处理函数。内核处理数据中止的模块为dabt_svc,调用do_DataAbort,do_DataAbort调用过程不细表。
Linux根据data abort最后调用的链为arch/arm/mm/fault.c中do_DataAbort -> do_translation_fault -> do_page_fault -> __do_kernel_fault
step 2:do_kernel_fault及其调用
arm/mm/fault.c中的arm_syscall,如果传入的no未0,同样也会抛出SIGSEGV错误。
asmlinkage int arm_syscall(int no, struct pt_regs *regs)
{
struct thread_info *thread = current_thread_info();
siginfo_t info;
if ((no >> 16) != (__ARM_NR_BASE>> 16))
return bad_syscall(no, regs);
switch (no & 0xffff) {
case 0: /* branch through 0 */
info.si_signo = SIGSEGV;
info.si_errno = 0;
info.si_code = SEGV_MAPERR;
info.si_addr = NULL;
arm_notify_die("branch through zero", regs, &info, 0, 0);
return 0;
......
}
void arm_notify_die(const char *str, struct pt_regs *regs,
struct siginfo *info, unsigned long err, unsigned long trap)
{
if (user_mode(regs)) {
current->thread.error_code = err;
current->thread.trap_no = trap;
force_sig_info(info->si_signo, info, current);
} else {
die(str, regs, err);
}
}
debuggerd.c是Android自带的的程序异常退出诊断的程序,在侦测到程序奔溃时将进程状态输出,供开发人员使用。Android的连接程序linker会通过debugger_init()注册信号处理函数。
void debugger_init()
{
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_sigaction = debugger_signal_handler;
act.sa_flags = SA_RESTART | SA_SIGINFO;
sigemptyset(&act.sa_mask);
sigaction(SIGILL, &act, NULL);
sigaction(SIGABRT, &act, NULL);
sigaction(SIGBUS, &act, NULL);
sigaction(SIGFPE, &act, NULL);
sigaction(SIGSEGV, &act, NULL);
#if defined(SIGSTKFLT)
sigaction(SIGSTKFLT, &act, NULL);
#endif
sigaction(SIGPIPE, &act, NULL);
}
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
signum:可以是除了SIGKILL和SIGSTOP外的所有信号量
act:act如果不是null,则设置当前为新的信号处理函数。如果oldact是非Null则保存老的处理函数到该指针。
struct sigaction {
//sa_handler和sa_sigaction只能存一,sa_flags设置为SA_SIGINFO则为后者
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);//非用户使用
};
sa_handler:sa_handler可以注册默认的SIG_DFL或者SIG_IGN忽略,也可以注册只有一个signum为参数的处理函数。
sa_mask:设置屏蔽信号量,该信号处理函数时,其他信号量应该被屏蔽。如果SA_NODEFER被设置则无效。
sa_flags:修改信号量的处理行为。
SA_ONSTACK:在sigalstack(2)提供的栈上面运行。如果被选的栈不可用,则在默认的栈上运行。该参数只有在建立信号量处理函数时有用。
SA_SIGINFO:使用sa_sigaction而不是,sa_handler。该函数可以设置和查询指定信号量的处理函数。sigaction(SIGSEGV, NULL, &older_handler_tmp)可以获取到老的信号处理函数,sigaction(SIGSEGV, &new_handler, NULL)注册new_handler为SIGSEGV函数。
我们在前面提到SIGSEGV很有可能是栈溢出引起的,如果在默认的栈上运行很有可能会破坏程序运行的现场,无法获取到正确的上下文。因此,我们应该开辟一块新的空间作为运行信号处理函数的栈。只要在注册时,将sa_flags设置为SA_ONSTACK即可。
创建时,可以先查看是否已经有创建sigal栈空间。如果没有,或者创建的大小太小,则需要创建一块足够大的栈空间。
stack_t old_stack,new_stack;
if (sigaltstack(NULL, &old_stack) == -1 || !old_stack.ss_sp ||
old_stack.ss_size < SIGSTKSZ) {
new_stack.ss_sp = calloc(1, SIGSTKSZ);
new_stack.ss_size = SIGSTKSZ;
if (sys_sigaltstack(&new_stack, NULL) == -1) {
free(new_stack.ss_sp);
return;
}
}
void SignalHandler(int sig, siginfo_t* info, void* uc){
......
}
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask,[signal_value]);//将所有注册的信号均加入信号集,不然sigaction()中的参数内容可能是未定义的。
sa.sa_sigaction = SignalHandler;
sa.sa_flags = SA_ONSTACK | SA_SIGINFO;//使用sa_sigaction和其他栈上运行
sigaction([signal_value], &sa, NULL);
sigaction一个信号量只能注册一个处理函数,这意味着我们的处理函数会覆盖其他人的处理信号。集成我们的异常检测程序的同时,程序也可能会集成其他应用的异常检测程序。
在前面,我们提到sigaction既可以注册也可以获取到原有的处理函数。如果,我们保存老的处理函数,在处理完我们的信号处理函数后,在重新运行老的处理函数就能完成兼容。
因为,我们的处理函数可能会被覆盖掉。所以,我们需要允许我们的程序多次注册而不会出错。其实,检测到老的处理函数就是自己注册的处理函数时,返回不做处理即可。
struct sigaction older_handler_tmp;
if (sigaction([signal_value], NULL, &older_handler_tmp) == -1){
return false;
}
//现在处理程序为自己定义的函数,则无需进一步处理
if(older_handler_tmp.sa_sigaction==SignalHandler){
LOG_DEBUG("Wetest CrashMonitor has registered");
return false;
} else{
sigaction([signal_value], NULL, &old_handlers);
}
//注册自己的处理函数
在我们自己的信号处理函数中,调用玩处理后,我们需要重新注册回老的处理函数。信号量将会重新触发老的处理函数。
if (sigaction([signal_value], &old_handlers, NULL) == -1) {
//注册出错,则尝试注册系统默认的处理函数。
}
这样,我们就完成了插入,我们自己的crash又不影响到其他crash检测模块的运行。这里有一个巨大的风险,就是当捕获到crash的时候,你的crash捕获模块不会退出,是否退出完全交给上一个注册的该信号量的函数。
在崩溃的瞬间,如果能够提供崩溃的原因、崩溃点(崩溃的代码所在行)、调用栈及崩溃点附近的变量这是最理想的,这样开发人员就能在最短的时间内定位到问题。在这方面Java、C#等语言,相对处理的比较好,但是C和C++在这方面表现较差。
崩溃时注册的信号量处理函数sa_sigaction会被调用,void (sa_sigaction)(int, siginfo_t , void *)。这个函数调用中分别提供信号量、siginfo_t结构体和一个指针。
siginfo_t:
siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value,Linux中通常无用*/
int si_code; /* Signal code */ 文档查看具体原因,如SEVG_MAPPERR.如果是<0的表示内核,
……
}
不同的信号量,对应不同的错误码,每个错误码均有自己的含义。
如果,我们能够知道崩溃时的pc,就能知道崩溃时执行的是那条指令。sa_sigaction在调用时,还会提供第三个指针参数,ucontext_t。在官方文档中,说这是一个很少使用到的参数,这里却恰巧提供了我们所需的内容。
在<ucontext.h>中定义了两个类型mcontext_t和ucontext_t。mcontext_t是与机器相关的,并且不透明的。ucontext_t类型至少包含以下信息。
typedef struct ucontext {
struct ucontext *uc_link;
sigset_t uc_sigmask;
stack_t uc_stack;
mcontext_t uc_mcontext;
...
} ucontext_t;
sigset_t和stack_t定义在<signal.h>。uc_link指向另一个ucontext_t,当前上下文被毁时,可以恢复。uc_sigmask是信号量屏蔽的集合。uc_stack是当前使用的栈地址。uc_mcontext是cpu相关的上下文,包括当前线程的寄存器信息。uc_context如果是信号量获取到的,获取后程序是处于中断状态,还是继续执行并没有详细描述(因此,crash进程的现场可能是会被破坏掉的)。
如果是手机上市ARM的CPU,那么uc_context保存即为ARM相关的内容。
ARM处理器包含31个通用寄存器,包括程序计数器pc。ARM处理器在7中模式(用户模式,系统模式,特权模式,中止模式,未定义指令,外部中断模式,快速中断模式)。
未备份(R0-R7):所有模式公用。异常中断切换时,现场会被破坏。
备份(R8-R14):两个寄存器,FIQ使用独有寄存器。(FIQ,快速中断,比如说数据读取)。R9,称之为SB栈底指针;R10,称之为SL栈限制;R11,称之为FP栈帧指针;R12,称之为IP内部过程调用寄存器;R13,通常称之为堆栈指针(SP),会指向当前模式独有的堆栈。异常模式时,可以把通用寄存器压入堆栈,返回时出栈,保证完成性。R14,称之为连接寄存器,保存子程序返回地址。异常模式时,可以保存异常返回地址,如处理嵌套中断。
程序计数器pc:PC寄存器,总是存储下一条指定的地址。
程序状态寄存器(CPSR):保存当前程序状态寄存器。最后5位,状态。
通过寄存器中的PC程序计数器我们就可以知道,程序崩溃时执行的指令地址。不过,这个地址并不是我们能够阅读的。我们需要通过进程内存解析出,我们能够阅读的信息。
任何一个程序通常都包括代码段和数据段,这些代码和数据本身都是静态的。程序要想运行,首先要由操作系统负责为其创建进程,并在进程的虚拟地址空间中为其代码段和数据段建立映射。光有代码段和数据段是不够的,进程在运行过程中还要有其动态环境,其中最重要的就是堆栈。图3所示为Linux下进程的地址空间布局:
char* str=0;
str[0]='1';
通过解析lib*.so的异常处理机制,来完成堆栈的还原。
在crash的进程中输出堆栈信息,存在两个问题。1、crash进程的关键数据或堆栈可能已经遭到破坏,在当前线程中输出堆栈信息存在风险;2、signalaction函数里面使用的函数有非常大的限制。
Crash进程仅仅简单的创建子进程,子进程能够共享crash进程的地址空间、文件资源、信号处理函数等。通过ptrace获取crash进程信息。
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack,int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
以类似于fork的方式创建一个新的进程。不同于fork,clone允许子进程共享父进程的部分上下文,如内存、文件描述符表、信号量处理表等。
其中一个应用是,多个线程跑在一个内存空间里面。
fn:子线程创建完成后,会执行fn函数。fn是在子进程执行前执行。当fn返回时,子进程将会终结。fn返回的int,是子进程的退出码。子进程也可以显式的调用exit或者处理错误信号量来退出。
child_stack:该参数用于设定子进程的堆栈。虽然,子进程可以与父进程共享内存,但是在共享同一个栈是不允许的。所以,必须要指定该栈区。
flags:中的低字节保存子进程结束码。如果信号不是SIGCHLD,那么父进程必须指定_WALL或者__WCLONE选项。如果,没有选项设置,则子进程退出时,父进程不会有任何信号量。
CLONE_FILFS:父子进程共享文件描述符列表。父进程或者子进程创建的文件描述符,两边都能使用。如果进程调用execve,描述符表将不再共享。
CLONE_FS:父子进程共享文件系统信息。共享的文件系统包括,现在的工作目录,权限。任何改变都会影响到其他进程。
CLONE_UNTRACED:如果该参数被指定,将不能再为CLONE_PTRACE。如果是CLONE_PTRACE,则父进程将被追踪,子进程也是。
https://code.google.com/p/google-breakpad/wiki/ClientDesign
http://blog.csdn.net/zangdongming/article/details/38543059
http://read.pudn.com/downloads153/doc/672477/%E6%9D%A8%E5%AE%97%E5%BE%B7_%E5%B5%8C%E5%85%A5%E5%BC%8FARM%E7%B3%BB%E7%BB%9F%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E4%BE%8B%E5%BC%80%E5%8F%91/CH06.pdf
https://android.googlesource.com/kernel/msm/+/3ab322a9e0a419e7f378770c9edebca17821bf6e/arch/arm/mm/fault.c
@126/blog/static/236614472009112693539253/"">http://blog.163.com/lhmood@126/blog/static/236614472009112693539253/
http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042e/IHI0042E_aapcs.pdf
http://blog.csdn.net/skyflying2012/article/details/37510171