Linux下使用backtrace开发崩溃日志输出库

Linux下使用backtrace开发崩溃日志输出库

一、导读

最近重构了部分屎山代码,bug寥寥无几。翻翻项目的其他功能模块的源代码过过瘾。以前做的项目比较小,出现崩溃了查下日志,差不多就找到哪里蹦了。这个项目很巨,多线程并发很猛。而且程序崩溃查最后断档日志很low。这个项目的做法我第一次见,觉得很棒,果断学习下来。由于源码被封装成库,只能看到函数。加上面向百度编程原理终于被我破解出来了。文章很长,研究的路上也涉及到其他知识点。这波我血成长!!!

二、原理

1.通过虚拟地址结算地址偏移。
接到崩溃信号后,打印处程序的 map数据。然后用崩溃处的函数地址 - 起始地址 = 偏移地址。
2.通过 函数名 + 偏移的方式计算
函数明后边会有个 + 0x666. 所以 函数的相对地址 + 函数内的偏移位置 = 问题发生的偏移地址。

获取相对地址:
ELF格式里。Section区域有个. text 区域,此区域包含了程序可执行的指令。使用 objdump 可将指令进行反汇编。其中就包含了偏移地址。
objdump -S XXX.so | grep “函数名” 会得到 函数当前的地址。再加上上面的666就是错处所在。
3.使用add2line定位
addr2line -Cfe XXX.so 计算后的地址。
4.再看 一下反汇编,
分析问题所在

linux工具函数
在Linux上的C/C++编程环境下,我们可以通过如下三个函数来获取程序的调用栈信息。它们由GNU C Library提供

#include <execinfo.h>
 
/* Store up to SIZE return address of the current program state in
   ARRAY and return the exact number of values stored.  */
int backtrace(void **array, int size);
 
/* Return names of functions from the backtrace list in ARRAY in a newly
   malloc()ed memory block.  */
char **backtrace_symbols(void *const *array, int size);
 
/* This function is similar to backtrace_symbols() but it writes the result
   immediately to a file.  */
void backtrace_symbols_fd(void *const *array, int size, int fd);

使用它们的时候有一下几点需要我们注意的地方:

1.backtrace的实现依赖于栈指针(fp寄存器),在gcc编译过程中任何非零的优化等级(-On参数)或加入了栈指针优化参数-fomit-frame-pointer后多将不能正确得到程序栈信息;
2.backtrace_symbols的实现需要符号名称的支持,在gcc编译过程中需要加入-rdynamic参数;
3.内联函数没有栈帧,它在编译过程中被展开在调用的位置;
4.尾调用优化(Tail-call Optimization)将复用当前函数栈,而不再生成新的函数栈,这将导致栈信息不能正确被获取。

三、撸码

先做个崩溃库,C/C++都可用
dump.h

#ifndef DUMP_H
#define DUMP_H

#ifdef __cplusplus
extern "C"{
#endif

/**
 * @brief 设置崩溃路径
 * @param[in] path: 崩溃日志路径
 * @return 0:设置成功;1:路径无效;2:路径过长
 */
int set_dump_path(const char *path);

/**
 * @brief 监听信号的回调函数,收到信号后会堆栈信息输入到指定路径下,
 *        若没有设置崩溃日志路径或路径无效将默认保存程序执行路径中
 * @param[in] sig: 信号值
 */
void dump_signal_hadler(int sig);

#ifdef __cplusplus
}
#endif

#endif // DUMP_H

dump.c

#include "dump.h"
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>       /* for signal */
#include <execinfo.h>     /* for backtrace() */

#define DUMP_NAME "/dump.txt"
#define PATH_BUF_LEN 120 + sizeof (DUMP_NAME)
#define BACKTRACE_SIZE 16


char path_buf[PATH_BUF_LEN] = {"./"};

void write_dump();

int set_dump_path(const char *path)
{
    if (strlen(path) >= PATH_BUF_LEN -1)
    {
        return 2;
    }

    if (access(path, R_OK) != 0)
    {
        return 1;
    }

    memset(path_buf, '\0', PATH_BUF_LEN);
    strcpy(path_buf, path);

    return 0;
}

void dump_signal_hadler(int sig)
{
    write_dump();
    signal(sig, SIG_DFL); /* 恢复信号默认处理 */
    raise(sig);           /* 对所在进程重新发送信号,该崩溃让它崩溃 */
}

void write_dump()
{
    void *buffer[BACKTRACE_SIZE];
    char **strings = NULL;

    int nptrs = backtrace(buffer, BACKTRACE_SIZE);

    printf("backtrace() returned %d addresses\n", nptrs);

    strings = backtrace_symbols(buffer, nptrs);
    if (strings == NULL) {
        perror("backtrace_symbols");
        exit(EXIT_FAILURE);
    }


    strcat(path_buf, DUMP_NAME);
    printf("path: %s \n", path_buf);
    FILE *fp = fopen(path_buf, "w");
    if(fp == NULL){
        printf("open fail!\n");
        exit(0);
    }

    int i = 0;
    char addr_buf[150] = {0};
    for (; i < nptrs; i++){
        memset(addr_buf, '\0', 150);
        snprintf(addr_buf, 150, "  [%02d] %s\n", i, strings[i]);
        fputs(addr_buf, fp);
    }

    free(strings);
    fclose(fp);
}


编译成动态库

lvxu@ubuntu:~/dump_test$ gcc dump.h dump.c -fPIC -shared -o libdump.so

搞个C++ 程序装在这个库。
main.cpp

#include <iostream>
#include "dump.h"
#include <unistd.h>
#include <signal.h>       /* for signal */
#include <execinfo.h>     /* for backtrace() */

using namespace std;

void dump()
{
    int *p = NULL;
    *p = 3;
}

int main()
{

    signal(SIGSEGV, dump_signal_hadler);

    int ret = set_dump_path("/home/lvxu/log");
    cout << "set path return :" << ret <<endl;
    sleep(1);
    dump();
    while (1) {
        sleep(1);
    }
    return 0;
}


1.编译程序,运行并获得崩溃日志
-g 添加调试信息,用于定位错误代码所在文件行号。
-rdynamic 不仅是已使用到的外部动态符号,还包括本程序内定义的符号,比如自定义的函数。
.dynsym表里的数据并不能被strip掉
-I (大写的i)编译时头文件搜索路径
-L 编译时寻找动态库的路径
-Wl,-rpath=. 运行时去加载动态库的路径

lvxu@ubuntu:~/dump_test$ g++ -g main.cpp -rdynamic -I. -L. -ldump -Wl,-rpath=.
lvxu@ubuntu:~/dump_test$ ./a.out 
set path return :0
backtrace() returned 7 addresses
path: /home/lvxu/log/dump.txt 
Segmentation fault
lvxu@ubuntu:~/dump_test$ cat /home/lvxu/log/dump.txt
  [00] ./libdump.so(write_dump+0x39) [0x7fbfc6398cad]
  [01] ./libdump.so(dump_signal_hadler+0x15) [0x7fbfc6398c58]
  [02] /lib/x86_64-linux-gnu/libc.so.6(+0x3efd0) [0x7fbfc5c5cfd0]
  [03] ./a.out(_Z4dumpv+0x10) [0x55ad61bcbc3a]
  [04] ./a.out(main+0x72) [0x55ad61bcbcb5]
  [05] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7fbfc5c3fb97]
  [06] ./a.out(_start+0x2a) [0x55ad61bcbb4a]

2.反汇编找到函数地址
根据堆栈可知在执行到dump函数的时候,之后就是掉c库和奔溃库的代码,dump函数编译后叫_Z4dumpv地址为0000000000000c2a,后面的+0x10是具体崩溃地点。

lvxu@ubuntu:~/dump_test$ cat /home/lvxu/log/dump.txt
  [00] ./libdump.so(write_dump+0x39) [0x7fbfc6398cad]
  [01] ./libdump.so(dump_signal_hadler+0x15) [0x7fbfc6398c58]
  [02] /lib/x86_64-linux-gnu/libc.so.6(+0x3efd0) [0x7fbfc5c5cfd0]
  [03] ./a.out(_Z4dumpv+0x10) [0x55ad61bcbc3a]
  [04] ./a.out(main+0x72) [0x55ad61bcbcb5]
  [05] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7fbfc5c3fb97]
  [06] ./a.out(_start+0x2a) [0x55ad61bcbb4a]
lvxu@ubuntu:~/dump_test$ objdump -S a.out | grep _Z4dumpv
0000000000000c2a <_Z4dumpv>:
 cb0:	e8 75 ff ff ff       	callq  c2a <_Z4dumpv>
0000000000000d0a <_GLOBAL__sub_I__Z4dumpv>:


3.addr2line 定位代码
0000000000000c2a + 0x10 = 0000000000000c3a

lvxu@ubuntu:~/dump_test$ addr2line -Cfe a.out 0xc3a
dump()
/home/lvxu/dump_test/main.cpp:12

在这里插入图片描述

名词解释

1.尾调用优化
https://blog.csdn.net/tang_yi_/article/details/77479658

参考文章

https://blog.csdn.net/gongmin856/article/details/79192259

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值