linux c开发: 程序崩溃时保存堆栈信息并解析具体代码行

写服务器程序最怕的是百分之一的概率崩溃了,你却不知道为啥,想重现又重现不出来。所以在崩溃时将当时的堆栈保存下来非常重要。网上有很多文章讲解怎么保存,但我使用了发现可以保存,但是没有函数名称和行号,仍然没法定位问题。在stack overflow上有人说只有动态库的代码才能显示出函数名和行号,想完整显示还需要使用某某第三方开源库,不过我幸好发现使用addr2line命令可以将文件名和行号显示出来,轻松定位问题。如下就总结一下整个流程。

  • 首先,我们需要在进程崩溃时调用某个函数。请参考上一篇文章:linux c开发: 在程序退出时进行处理
  • 然后,我们获取堆栈信息并保存的一个文件,代码如下所示,使用了网上的一些代码:
void server_backtrace(int sig)
{
    //打开文件
    time_t tSetTime;
    time(&tSetTime);
    struct tm* ptm = localtime(&tSetTime);
    char fname[256] = {0};
    sprintf(fname, "core.%d-%d-%d_%d_%d_%d",
            ptm->tm_year+1900, ptm->tm_mon+1, ptm->tm_mday,
            ptm->tm_hour, ptm->tm_min, ptm->tm_sec);
    FILE* f = fopen(fname, "a");
    if (f == NULL){
        return;
    }
    int fd = fileno(f);

    //锁定文件
    struct flock fl;
    fl.l_type = F_WRLCK;
    fl.l_start = 0;
    fl.l_whence = SEEK_SET;
    fl.l_len = 0;
    fl.l_pid = getpid();
    fcntl(fd, F_SETLKW, &fl);

    //输出程序的绝对路径
    char buffer[4096];
    memset(buffer, 0, sizeof(buffer));
    int count = readlink("/proc/self/exe", buffer, sizeof(buffer));
    if(count > 0){
        buffer[count] = '\n';
        buffer[count + 1] = 0;
        fwrite(buffer, 1, count+1, f);
    }

    //输出信息的时间
    memset(buffer, 0, sizeof(buffer));
    sprintf(buffer, "Dump Time: %d-%d-%d %d:%d:%d\n",
            ptm->tm_year+1900, ptm->tm_mon+1, ptm->tm_mday,
            ptm->tm_hour, ptm->tm_min, ptm->tm_sec);
    fwrite(buffer, 1, strlen(buffer), f);

    //线程和信号
    sprintf(buffer, "Curr thread: %u, Catch signal:%d\n",
            (int)pthread_self(), sig);
    fwrite(buffer, 1, strlen(buffer), f);

    //堆栈
    void* DumpArray[256];
    int    nSize =    backtrace(DumpArray, 256);
    sprintf(buffer, "backtrace rank = %d\n", nSize);
    fwrite(buffer, 1, strlen(buffer), f);
    if (nSize > 0){
        char** symbols = backtrace_symbols(DumpArray, nSize);
        if (symbols != NULL){
            for (int i=0; i<nSize; i++){
                fwrite(symbols[i], 1, strlen(symbols[i]), f);
                fwrite("\n", 1, 1, f);
            }
            free(symbols);
        }
    }

    //文件解锁后关闭
    fl.l_type = F_UNLCK;
    fcntl(fd, F_SETLK, &fl);
    fclose(f);
}
  • 注意编译的时候,需要在makefile里面加入编译选项rdynamic。例如:
    CFLAGS :=-g -rdynamic -Wall -Werror -std=gnu99 -D MY_SERVER_DEBUG
  • 然后我们运行编译好的程序,在崩溃的时候就可以获取一个core dump文件了,例如:core.2017-8-28_23_4_55。内容大概是这样:
/usr/local/bin/my_server
Dump Time: 2017-8-25 23:4:55
Curr thread: 2857228032, Catch signal:6
backtrace rank = 18
my_server() [0x40ce9d]
my_server() [0x401ebf]
/lib64/libc.so.6(+0x32510) [0x7f9da9aeb510]
/lib64/libc.so.6(gsignal+0x35) [0x7f9da9aeb495]
/lib64/libc.so.6(abort+0x175) [0x7f9da9aecc75]
/lib64/libc.so.6(+0x703a7) [0x7f9da9b293a7]
/lib64/libc.so.6(+0x75dee) [0x7f9da9b2edee]
/lib64/libc.so.6(+0x78c80) [0x7f9da9b31c80]
my_server() [0x40cbc3]
my_server() [0x41080f]
my_server() [0x4100fc]
my_server() [0x4039e8]
/usr/lib64/libev.so.4(ev_invoke_pending+0x61) [0x7f9daa0bb071]
/usr/lib64/libev.so.4(ev_run+0x71a) [0x7f9daa0c023a]
my_server() [0x4064cd]
my_server() [0x402d3d]
/lib64/libc.so.6(__libc_start_main+0xfd) [0x7f9da9ad7d1d]
my_server() [0x401de9]

问题是这里的堆栈信息,只有模块的名字,比如my_server,但是里面没有函数名和行号,这样定位问题就难了。但是我们看到libev.so的函数名称都在。而这里显示的信息,都是backtrace_symbols这个函数返回的,并且我们已经加上编译选项-rdynamic了,甚至我的编译选项里面还有-g,所以这个锅我不背。上面说过,stack overflow上有人认为只有动态链接库才有具体的信息,而解决方法是使用某某库,不过对于我已经来不及了。幸好发现了addr2line这个工具可以从地址解析出文件名和行号。(注意是文件名而不是函数名)

  • 使用addr2line解析出文件名和行号
    addr2line的使用方法很简单 addr2line -e <执行文件> <代码地址>。使用例子:
    addr2line -e /usr/local/bin/my_server 0x4039e8
    这里的两个参数就是我上面core dump文件里面能看到的内容。得到结果如下:
    /root/build/my_server/src/my_server.c:129
    代码轻松定位了,然后继续向上看堆栈上各个地址,找到崩溃的地方。需要注意的是,定位出来的代码行可能是括号,并不是函数调用语句或表达式,不过不要紧,结合堆栈里面上下文关系还是很容易确定具体代码位置的。
  • 1
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
好的,下面是一个简单的 Linux kernel panic 堆栈信息解析示例: ``` Kernel panic - not syncing: Attempted to kill init! exitcode=0x0000000b [<c0102a9e>] (unwind_backtrace+0x0/0xe8) from [<c042f8f6>] (panic+0x64/0x13c) [<c042f8f6>] (panic+0x64/0x13c) from [<c042d7c2>] (do_exit+0x5fe/0x602) [<c042d7c2>] (do_exit+0x5fe/0x602) from [<c0101e80>] (die+0x20c/0x214) [<c0101e80>] (die+0x20c/0x214) from [<c0113a98>] (__do_kernel_fault.part.3+0x5c/0x84) [<c0113a98>] (__do_kernel_fault.part.3+0x5c/0x84) from [<c0113b88>] (do_page_fault+0x2a0/0x2b8) [<c0113b88>] (do_page_fault+0x2a0/0x2b8) from [<c0100e6e>] (do_DataAbort+0x34/0x98) [<c0100e6e>] (do_DataAbort+0x34/0x98) from [<c0102d24>] (__dabt_svc+0x44/0x60) ---[ end trace 5a7926b607e5b2d5 ]--- ``` 这个堆栈信息表示系统发生了一个 kernel panic,导致系统无法正常运。下面是对每一解析: - 第一kernel panic 的描述信息,告诉我们出现了什么问题。 - 第二中的 `<c0102a9e>` 表示函数 `unwind_backtrace` 在代码中的位置,`0x0/0xe8` 表示函数内部的偏移量。这个函数是用来跟踪函数调用栈的,可以帮助我们定位问题发生的位置。 - 第三中的 `<c042f8f6>` 表示函数 `panic` 在代码中的位置,`0x64/0x13c` 表示函数内部的偏移量。这个函数会导致系统进入 panic 模式,因此我们需要关注它。 - 第四中的 `<c042d7c2>` 表示函数 `do_exit` 在代码中的位置,`0x5fe/0x602` 表示函数内部的偏移量。这个函数是用来退出进程的,可能与问题有关。 - 第五中的 `<c0101e80>` 表示函数 `die` 在代码中的位置,`0x20c/0x214` 表示函数内部的偏移量。这个函数是用来输出错误信息和导致系统进入 panic 模式的,因此我们需要关注它。 - 第六中的 `<c0113a98>` 表示函数 `__do_kernel_fault.part.3` 在代码中的位置,`0x5c/0x84` 表示函数内部的偏移量。这个函数是用来处理页面故障的,可能与问题有关。 - 第七中的 `<c0113b88>` 表示函数 `do_page_fault` 在代码中的位置,`0x2a0/0x2b8` 表示函数内部的偏移量。这个函数也是用来处理页面故障的,可能与问题有关。 - 第八中的 `<c0100e6e>` 表示函数 `do_DataAbort` 在代码中的位置,`0x34/0x98` 表示函数内部的偏移量。这个函数是用来处理数据异常的,也可能与问题有关。 - 最后一表示跟踪结束的信息,可以忽略。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值