调试方法 - 崩溃问题定位 - backtrace

说明

  • 在嵌入式开发中,由于资源限制,有时无法使用gdb进行问题调试,这种情况下如果出现程序崩溃问题,定位会比较麻烦。

研究方案

根据崩溃信息定义代码行

  1. 寄存器ip + addr2line

获取函数调用栈

  1. 网上流行开源工具:Google breakpad。
  2. backtrace + addr2line。

Google breakpad

  • Google breakpad是一个非常实用的跨平台崩溃转储和分析模块,支持Windows,Linux和Mac和Solaris多个平台。由于它本身跨平台,所以很大的减少我们在平台移植时的工作,毕竟崩溃转储,每个平台下都不同,使用起来很难统一,而Google breakpad就帮我们做到了这一点,不管是哪个平台下的崩溃,都能够进行统一的分析。现在很多工程都在使用它,比如Chrome,Firefox,Picasa和Google Earth。另外它支持BSD协议,也就是说,我们可以在商业软件中使用。
  • 代码下载
https://github.com/google/breakpad
  • 编译和使用
https://www.linuxidc.com/Linux/2018-09/154032.htm
  • 网上比较好的分析和心得经验
https://blog.csdn.net/wpc320/article/details/8290501
https://www.jianshu.com/p/1687c92efb89

个人研究

  • 查看网上介绍,总的来说Google breakpad是非常强大,有用的工具,因此尝试使用,直接clone的github上的代码。
  • 编译时会遇到一些问题,按照上面所列博客的指导,问题能够得到解决,编译正常。
  • 使用时发现不管怎么编译使用都定位不到具体的问题代码,只能获取崩溃的内存地址,查看工具输出发现如下错误:
2020-04-11 17:02:59: simple_symbol_supplier.cc:196: INFO: No symbol file at ./test.sym/test/9AA903F3858F0D1EFB50062D271F76450/test.sym
* 个人工具使用:processor/minidump_stackwalk d33b9634-8a9d-41ee-cc519c9f-657052a6.dmp test.sym
* 个人分析:查看工具帮助,认为使用方式没问题,dump分析工具会自动对路径进行改变,导致找不到符号文件,因此手动创建该目录,并且将文件拷贝过去,错误消息消失,但是依然只能获取到内存地址。
  • 查看以上所列心得博客,发现别人使用时也是使用addr2line将内存地址转换为问题代码,和方案2没什么区别,故舍去使用。

backtrace + addr2line

  • backtrace是glibc提供了接口;能够回溯当前的函数调用栈,方便在程序崩溃时打印出问题代码。
  • addr2line 可以实现将程序代码段的内存地址转换成具体的代码行。
  • 网上对backtrace的介绍和分析
https://www.2cto.com/kf/201107/97270.html

相关接口

  • 头文件
#include <execinfo.h> 
  1. backtrace
函数原型:int backtrace(void **buffer, int size);
  • 函数作用:获取函数调用栈,返回的是函数地址。
  1. backtrace_symbols
函数原型:char **backtrace_symbols(void *const *buffer, int size);
  • 函数作用:将backtrace返回的函数地址,转换成函数名和代码行作为字符串数组返回,原理是去符号表中查找,如果不存在符号表,则查找不到。
  1. backtrace_symbols_fd
函数原型:void backtrace_symbols_fd(void *const *buffer, int size, int fd);
  • 函数作用:将backtrace返回的函数地址,转换成函数名和代码行并写入文件描述符。

例子

#include<stdio.h>  
#include<stdlib.h>  
#include<signal.h>  
#include<string.h>  
#include<execinfo.h>  
  
void SystemErrorHandler(int signum)  
{  
    const int len=1024;  
    void *func[len];  
    size_t size;  
    int i;  
    char **funs;  
  
    signal(signum,SIG_DFL);  
    size=backtrace(func,len);  
    funs=(char**)backtrace_symbols(func,size);  
    fprintf(stderr,"System error, Stack trace:\n");  
    for(i=0;i<size;++i) fprintf(stderr,"%d %s \n",i,funs[i]);  
    free(funs); 
}  
  
void Fun1()  
{  
    char *p=NULL;  
    *p = 'A';  
}  
  
void Fun()  
{  
    Fun1();  
}  
  
int main(const int argc,const char* argv[])  
{  
    signal(SIGSEGV,SystemErrorHandler); //Invaild memory address  
    signal(SIGABRT,SystemErrorHandler); // Abort signal  
    Fun();  
    return 0;  
}
  • 编译
gcc -g -rdynamic BackTraceTest.c -o BackTraceTest 
* 注意:需要带上选项-rdynamic,不然函数调用栈打印只有地址,没有函数名等其它信息。
  • 运行
# ./BackTraceTest 
System error, Stack trace:
0 ./BackTraceTest(SystemErrorHandler+0xa5) [0x400a62] 
1 /lib/x86_64-linux-gnu/libc.so.6(+0x36cb0) [0x7f76de058cb0] 
2 ./BackTraceTest(Fun1+0x10) [0x400b11] 
3 ./BackTraceTest(Fun+0xe) [0x400b24] 
4 ./BackTraceTest(main+0x37) [0x400b5d] 
5 /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7f76de043f45] 
6 ./BackTraceTest() [0x4008f9] 
Segmentation fault (core dumped)
  • 运行后程序崩溃,会打印出函数调用栈可得最后调用的代码是,函数Fun1中偏移0x10,内存地址是0x400b11,通过该地址可以转换成代码行,转换如下:
# addr2line -f -C -e BackTraceTest 0x400b11
Fun1    //函数名
/home/xxxx/src/BackTraceTest.c:27 //代码行
  • 这样就定位到了问题代码;由于使用的函数和工具都是GUN自带的,不需要安装环境等,使用非常方便和通用。
  • C/C++都支持,QT程序中也能使用,失败请注意使用方式和使用细节。

backtrace 实现原理

  • 不同平台实现原理可能不同,以下是在x86的机器上。
  • 函数调用时CPU处理如下:
  1. 涉及到的两个寄存器:
  • EIP;作用:保存下一条指令的地址。
  • EBP;作用:保存当前函数的栈顶。
  1. 函数代码段
  • 进程会有一段内存来保存代码编译而成的二进制指令;每个函数是独立的一段,并不一定是连续的,发生函数调用时,EIP寄存器保存的地址就会发生改变,为了实现函数执行完恢复到调用处的下一条指令,CPU需要将EIP和EBP保存到内存栈中。
  1. 函数调用时,内存栈中数据的如下:
  • ebp+4 保存的是 EIP寄存器数据
  1. 较好的分析博客
https://www.xuebuyuan.com/1504689.html
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回溯算法是一种解决问题的算法思想,它通过不断地尝试所有可能的解决方案来寻找问题的最优解。而装载问题是回溯算法的一个经典应用,它是指在给定一些集装箱和一艘载重量为C的轮船的情况下,如何将这些集装箱装载到轮船上,使得轮船的载重量最大。 具体来说,装载问题可以通过回溯算法来解决。我们可以定义一个递归函数来尝试所有可能的装载方案,每次递归时,我们可以选择将当前集装箱装载到轮船上或者不装载,然后继续递归下去。当所有集装箱都被考虑过后,我们就可以得到一个装载方案,然后比较这个方案的载重量和当前最优解的载重量,如果更优则更新最优解。 下面是一个简单的装载问题的回溯算法实现: ```c #include <stdio.h> #define MAX_N 100 int n; // 集装箱数量 int c; // 轮船载重量 int w[MAX_N]; // 集装箱重量 int best[MAX_N]; // 当前最优解 int cur[MAX_N]; // 当前解 int cur_weight; // 当前载重量 int best_weight; // 当前最优解的载重量 void backtrack(int i) { if (i == n) { // 所有集装箱都被考虑过 if (cur_weight > best_weight) { // 更新最优解 best_weight = cur_weight; for (int j = 0; j < n; j++) { best[j] = cur[j]; } } return; } if (cur_weight + w[i] <= c) { // 装载当前集装箱 cur[i] = 1; cur_weight += w[i]; backtrack(i + 1); cur_weight -= w[i]; } cur[i] = 0; // 不装载当前集装箱 backtrack(i + 1); } int main() { scanf("%d%d", &n, &c); for (int i = 0; i < n; i++) { scanf("%d", &w[i]); } backtrack(0); printf("最大载重量为:%d\n", best_weight); printf("装载方案为:"); for (int i = 0; i < n; i++) { if (best[i]) { printf("%d ", i + 1); } } printf("\n"); return 0; } ``` 在这个实现中,我们使用了一个数组`cur`来记录当前的解,使用`cur_weight`来记录当前的载重量,使用`best`来记录当前最优解,使用`best_weight`来记录当前最优解的载重量。在回溯函数`backtrack`中,我们首先判断是否可以装载当前集装箱,如果可以则装载,然后递归下去;如果不可以则不装载,然后递归下去。当所有集装箱都被考虑过后,我们就可以得到一个装载方案,然后比较这个方案的载重量和当前最优解的载重量,如果更优则更新最优解。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值