一、导读
最近重构了部分屎山代码,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