Yanyg - Software Engineer

使用信号机制与系统_Unwind_Backtrace输出程序退出堆栈信息

目录

1 描述

进程执行某些异常指令(比如除0或非法内存访问),或者其他进程或进程自己通过kill 调用向进程发送信号时,操作系统强制进程对信号作出处理。进程启动时操作系统设置默认的信号处理函数,通过signal、sigaction函数进程可以更改信号处理策略。Linux 系统提供有64种信号,使用kill -l可查看:

yanyg@t:~$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

不同信号默认行为不同,Man 7 Sginal对此有完整描述,如下是可能的处理策略:

行为 描述
Term 进程退出
Ign 忽略
Core 生成Core文件,需要同时设置ulimit -c
Stop 进程停止运行
Cont 进程继续运行

并非所有信号均可Ign或Cont,部分信号在处理后,操作系统会强制进程退出,比如SIGKILL 或SIGSEGV信号。SIGKILL信号即不能被捕获,也不能被忽略。进程常见的信号包括终端挂起(SIGHUP)、键盘中断(SIGINT)、非法地址访问(SIGSEGV)、总线错误(SIGBUS)、杀死进程(SIGKILL)。

默认的信号处理函数在退出时,通常不能给出足够的信息帮助快速定位问题。同时,主动防御的编程技术中,在关键点会插入assert确保软件符合预期。无论信号或者assert,在退出时如能打印当前进程的调用栈,对于快速分析问题均有较大帮助。

本文描述进程在退出前(无论因信号或assert),打印调用栈的方法。预期实现如下几个目标:

  • 准确记录调用栈,帮助快速定位分析问题;
  • 能提供-g编译的二进制文件时,记录调用栈对应文件名、代码行;
  • 支持多线程,退出时记录每个线程的调用栈;
  • 如果进程已设置信号处理函数,在记录调用栈后调用;
  • 结束后还原系统默认信号处理函数,并再次触发信号。

2 signal

3 assert

assert是主动防御机制,用于检测不可能发生的情况,记录信息包括assert所在的文件名、函数名、代码行与调用栈。考虑可扩展,允许提供回调函数,辅助打印更多信息。

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdint.h>
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <ucontext.h>
#include <unistd.h>
#include <unwind.h>

#define sig_err(str) write(STDERR_FILENO, (str), strlen(str))
#define sig_out(str) write(STDERR_FILENO, (str), strlen(str))

static int pr_reg = 1;

char *func_name(void *addr, char *buf)
{
        Dl_info di;
        const void *name = "_end";

        if (0 == dladdr(addr, &di)) { // error
                return "nil";
        }

        if (di.dli_sname) {
                name = di.dli_sname;
        }

        sprintf(buf, "%s+%lx, base-offset %lx (%p)",
                name, addr - di.dli_saddr,
                addr - di.dli_fbase, di.dli_saddr);

        return buf;
}

_Unwind_Reason_Code read_stacktrace(struct _Unwind_Context *uc, void *p)
{
        int n, used = 0;
        char buf[1024], fname[256];
        uint64_t regs[16], r;
        void **rt, *caller = (void*)_Unwind_GetIP(uc);

        if (!caller)
                return _URC_END_OF_STACK;

        rt = (void**)uc;
        for (r = 0; r < 16; ++r) {
                if (rt[r] != NULL)
                        regs[r] = _Unwind_GetGR(uc, r);
                else
                        regs[r] = 0xCCCCCCCCEEEEEEEEUL;
        }

        if (pr_reg) {
                pr_reg = 0;
                used = snprintf(buf + used, 1024 - used, "Regs:\n\t");
                for (r = 0; r < 16; ++ r) {
                        n = snprintf(buf + used, 1024 - used, " %#018lx", regs[r]);
                        used += n;
                        if ((r+1)%4 == 0) {
                                snprintf(buf + used, 1024 - used, "\n\t");
                                used += 2;
                        }
                }
                n = snprintf(buf + used, 1024-used, "\n");
                used += n;
        }
        n = snprintf(buf + used, 1024 - used, "Caller: %p(%s)\n",
                     caller, func_name(caller, fname));
        used += n;
        //snprintf(buf + used, 1024 - used, "RIP=%lx, RBP=%lx, RSP=%lx\n",
        //       ((ucontext_t*)uc)->uc_mcontext.gregs[REG_RIP],
        //       ((ucontext_t*)uc)->uc_mcontext.gregs[REG_RBP],
        //       ((ucontext_t*)uc)->uc_mcontext.gregs[REG_RSP]);

        sig_out(buf);

        return _URC_NO_REASON;
}

void stacktrace()
{
        /* in multithread environment, may be you add tid here */
        _Unwind_Backtrace(read_stacktrace, 0);
}

static inline void __assert_stack(const char *expr, const char *file,
                                  const char *func, size_t line,
                                  void (*cb)(void*), void *arg)
{
        ucontext_t uc;

        fprintf(stderr, "Assert expr: %s [%s, %s, %zu]\n",
                expr, file, func, line);

        getcontext(&uc);
        stacktrace();

        if (cb)
                cb(arg);

        exit(1);
}

#define __assert_string_expr(expr)      #expr

#define assert_stack(expr, callback, arg)       ({                      \
                if (expr)                                               \
                        __assert_stack(#expr, __FILE__, __func__,       \
                                       __LINE__, callback, arg);        \
                (void)NULL; })

int main(int argc, char *argv[argc])
{
        int i = 50, j = 100;
        assert_stack(i*2 == j, NULL, NULL);
        return 0;
}

4 实例代码

5 References