嵌入式C代码调试利器——backtrace
共 5055字,需浏览 11分钟
·
2024-04-17 08:00
1
backtrace基本原理
大家好,我是bug菌~
backtrace英译为回溯的意思,这听起来有点专业了,其实大部分搞嵌入式的朋友都有听说过函数调用栈callstack。而backtrace说白了就是我们呈现函数调用关系的一项功能。
所以backtrace调试功能的实现原理基于函数调用栈的概念。
那什么是函数调用栈呢?
函数调用栈是一个记录程序中函数调用关系的数据结构,它在程序运行时动态生成和维护。当程序执行函数调用时,它将当前函数的返回地址和一些其他信息压入堆栈中,并跳转到被调用的函数执行。当被调用函数执行完毕后,它将返回地址弹出堆栈,并跳回到调用函数继续执行。
backtrace调试功能的实现原理就是利用函数调用栈中的信息来追踪程序执行的路径和调用关系。当程序出现错误或崩溃时,backtrace可以通过分析函数调用栈信息来确定出错的位置和原因。
在Linux系统中,backtrace通常是通过使用调试器比如我们常用的gdb来实现的。调试器会在程序执行时,动态地获取函数调用栈信息,并将其保存在调试器的内部数据结构中。当程序出现错误或崩溃时,调试器就可以利用保存的函数调用栈信息来进行backtrace操作。
2
backtrace功能
而对于backtrace这个功能在不同的平台和开发环境中的使用是不同的.
比如在我们平时的linux环境中:可以使用glibc提供的backtrace()函数实现backtrace功能。该函数通过解析函数调用栈信息获取函数名、参数和返回地址等信息,并将其打印到标准输出或指定的文件中。
此外,还可以使用gdb或libunwind库来实现backtrace功能。gdb是一个强大的调试器,可以实时追踪程序的执行,获取程序的调用栈信息,并提供各种调试工具和命令。
而其中的libunwind则是一个开源的C/C++库,也可以用于在运行时获取当前程序的调用栈信息,并且在不同的平台和架构上运行,并提供了简单易用的API接口,同样也是非常方便的。
3
glibc下的backtrace功能使用
glibc提供了backtrace函数,可以用来获取当前程序的调用栈信息,使用方法如下:
包含头文件:
#include <execinfo.h>
定义一个数组,用于存储回溯信息:
#define BT_BUF_SIZE 100
void *bt_buffer[BT_BUF_SIZE];
该数组用于存储backtrace信息,数组大小可以根据需要进行调整。
3. 调用backtrace函数:
int bt_size = backtrace(bt_buffer, BT_BUF_SIZE);
该函数会获取当前程序的调用栈信息,并将其存储在bt_buffer数组中。bt_size表示实际获取到的调用栈信息的条数,该值不会超过BT_BUF_SIZE。
4. 使用backtrace_symbols函数将backtrace信息转换成字符串:
char **bt_strings = backtrace_symbols(bt_buffer, bt_size);
该函数将backtrace信息转换成字符串数组,每个字符串表示一个调用栈信息。bt_strings指向字符串数组的首地址,需要在使用完毕后手动释放内存。
5. 打印回溯信息:
for (int i = 0; i < bt_size; i++) {
printf("%!s(MISSING)\n", bt_strings[i]);
}
该代码会将回溯信息打印到标准输出中,可以根据需要进行调整。完整的使用示例代码如下:
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#define BT_BUF_SIZE 100
void print_backtrace() {
void *bt_buffer[BT_BUF_SIZE];
int bt_size = backtrace(bt_buffer, BT_BUF_SIZE);
char **bt_strings = backtrace_symbols(bt_buffer, bt_size);
printf("backtrace:\n");
for (int i = 0; i < bt_size; i++) {
printf("%!s(MISSING)\n", bt_strings[i]);
}
free(bt_strings);
}
int func_c() {
print_backtrace();
return 0;
}
int func_b() {
return func_c();
}
int func_a() {
return func_b();
}
int main() {
return func_a();
}
该程序会输出调用栈信息,格式如下:
backtrace:
./backtrace_demo(func_c+0x16) [0x40069a]
./backtrace_demo(func_b+0xd) [0x4006c5]
./backtrace_demo(func_a+0xd) [0x4006e0]
./backtrace_demo(main+0xe) [0x4006f6]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf1) [0x7f6a69e2b1c1]
./backtrace_demo(_start+0x2a) [0x400529]
其中每一行表示一个调用栈信息,格式为"函数名+偏移量+[地址]"。
4
gdb的backtrace功能
在Linux下进行嵌入式开发,backtrace通常是通过使用调试器来实现的,这样的话,gdb都跟你封装成了相应的命令,使用起来也简单很多。
下面以gdb为例来介绍如何使用backtrace:
1、编译程序时添加-g选项,以在可执行文件中包含调试信息。因为backtrace函数需要获取调用栈信息,因此需要包含符号信息。如果使用了-g选项进行编译,则可以保证符号信息的完整性,如果没有使用-g选项编译程序,则可能会出现获取不到符号信息的情况,导致backtrace函数无法正常工作。
例如,使用gcc编译时可以添加-g选项:
gcc -g -o program program.c
gdb program
(gdb) break main
(gdb) run
(gdb) backtrace
5
跟踪的准确性
在实现backtrace功能时,还需要注意一些细节问题。例如,需要注意函数调用栈的深度和堆栈溢出等问题,以及需要保证backtrace操作的可靠性和准确性,下面简单聊聊如下三个值得注意的方面:
-
优化选项:程序使用了-O选项进行优化时,可能会改变函数调用栈的结构,从而使backtrace函数获取到的信息不完整或不准确。因此,在使用backtrace函数时,建议关闭优化选项,以保证其可靠性。 -
栈溢出:如果程序发生栈溢出,可能会破坏调用栈信息,导致backtrace函数获取到的信息不完整或不准确。因此,在程序中应该避免出现栈溢出的情况,以保证backtrace函数的可靠性。 -
线程安全:如果程序使用多线程,每个线程都有自己的调用栈,因此需要在每个线程中分别调用backtrace函数来获取相应的调用栈信息。此外,在多线程环境下,需要注意避免竞争条件的出现,以保证backtrace函数的可靠性。
总之,在使用glibc提供的backtrace函数时,需要注意编译选项、优化选项、栈溢出和线程安全等因素,以保证其可靠性。此外,不同的硬件平台和操作系统可能有不同的backtrace实现方式和接口,需要使用相应的工具和API来实现。