1.介绍打印堆栈信息函数
头文件:
#include
函数原型
int backtrace (void **buffer, int size);
char **backtrace_symbols (void *const *buffer, int size);
void backtrace_symbols_fd (void *const *buffer, int size, int fd);
函数描述
backtrace()函数:获取函数调用堆栈帧数据,即回溯函数调用列表。数据将放在buffer中。参数size用来指定buffer中可以保存多少个void*元素(表示相应栈帧的地址,一个返回地址)。如果回溯的函数调用大于size,则size个函数调用地址被返回。为了取得全部的函数调用列表,应保证buffer和size足够大。backtrace函数返回通过buffer返回的地址个数,这个数目不会超过size。如果这个返回值小于size,那么所有的函数调用列表都被保存;如果等于size,那么函数调用列表可能被截断,此时,一些最开始的函数调用没有被返回。
backtrace_symbols()函数,参数buffer是从backtrace()函数获取的数组指针,size是该数组中的元素个数(backtrace()函数的返回值)。该函数主要功能:将从backtrace()函数获取的地址转为描述这些地址的字符串数组。每个地址的字符串信息包含对应函数的名字、在函数内的十六进制偏移地址、以及实际的返回地址(十六进制)。需注意的是,当前,只有使用elf二进制格式的程序才能获取函数名称和偏移地址,此外,为支持函数名功能,可能需要添加相应的编译链接选项如-rdynamic;否则,只有十六进制的返回地址能被获取。backtrace_symbols()函数返回值是一个字符串指针,是通过malloc函数申请的空间,使用完后,调用者必需把它释放掉。注:如果不能为字符串获取足够的空间,该函数的返回值为NULL。成功时,backtrace_symbols()函数返回一个由malloc分配的数组;失败时,返回NULL。
backtrace_symbols_fd()函数,与backtrace_symbols()函数具有相同的功能,不同的是它不会给调用者返回字符串数组,而是将结果写入文件描述符为fd的文件中,每个函数对应一行。它不会调用malloc函数,因此,它可以应用在函数调用可能失败的情况下。
2.示例
首先来一个C语言调用上述函数的例子:
#include
#include
#include
#include
#include
#define MAX_SIZE 1024
void print_trace(void)
{
size_t i, size;
void *array[MAX_SIZE];
size = backtrace(array, MAX_SIZE);
char **strings = backtrace_symbols(array, size);
for (i = 0; i < size; i++)
printf("%d# %s\n",i, strings[i]);
free(strings);
}
void my_func_3(void)
{
print_trace();
}
void my_func_1(void)
{
my_func_3();
}
int main(void)
{
my_func_1();
return 0;
}
编译:
gcc -g main.c -o out
执行结果:
0# ./out(+0x1210) [0x559e1547d210]
1# ./out(+0x12c9) [0x559e1547d2c9]
2# ./out(+0x12d9) [0x559e1547d2d9]
3# ./out(+0x12e9) [0x559e1547d2e9]
4# /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0x7fed731d8083]
5# ./out(+0x110e) [0x559e1547d10e]
这种打印存在两个问题:
问题1:没有打印出调用函数信息
解决方法:编译时候增加-rdynamic
gcc -g -rdynamic main.c -o out
结果:
0# ./out(print_trace+0x47) [0x559283455210]
1# ./out(my_func_3+0xd) [0x5592834552c9]
2# ./out(my_func_1+0xd) [0x5592834552d9]
3# ./out(main+0xd) [0x5592834552e9]
4# /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0x7f3d1665c083]
5# ./out(_start+0x2e) [0x55928345510e]
问题2:我们可以用addr2line命令通过地址查看调用函数信息
addr2line -Cif -e out 0x559283455210
结果:产生??
??
??:0
原因:addr2line输入的地址并非其所接受的地址,addr2line接受的地址是相对偏移地址。
解决方法:编译的时候增加-no-pie
gcc -g -no-pie -rdynamic main.c -o out
结果:这里的地址和上面的地址就不同了,用这个地址输入到addr2line中
0# ./out(print_trace+0x47) [0x4011fd]
1# ./out(my_func_3+0xd) [0x4012b6]
2# ./out(my_func_1+0xd) [0x4012c6]
3# ./out(main+0xd) [0x4012d6]
4# /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0x7fca00584083]
5# ./out(_start+0x2e) [0x4010fe]
root@ubuntu:~# addr2line -Cif -e out 0x4011fd
print_trace
/root/zby/demo/backtrace_demo/main.c:13
注:(这里有个问题解释不明白,希望有会的大佬看到可以解释一下,以供学习)addr2line显示的函数行数是13,但是从代码中看到这个函数对应的行数不是13,为什么?
现在来一个c++例子:
#include
#include
#include
#include
#include
using namespace std;
void backtrace()
{
void* addresses[256];
const int n = backtrace(addresses, extent< decltype(addresses) > ::value );
const unique_ptr< char*, decltype(&free) > symbols(backtrace_symbols(addresses, n), &free);
for(int i=0; i<n; ++i) {
char* symbol = symbols.get()[i];
char* end = symbol;
cout << symbol << endl;
}
}
void func2()
{
backtrace();
}
void func1()
{
func2();
}
int main()
{
func1();
}
编译:
g++ main1.cpp -g -rdynamic -no-pie -o out
结果:
./out(_Z9backtracev+0x33) [0x403410]
./out(_Z5func2v+0xd) [0x403538]
./out(_Z5func1v+0xd) [0x403548]
./out(main+0xd) [0x403558]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0x7f0500e10083]
./out(_start+0x2e) [0x4031de]
上面的调用堆栈中函数名大致能看出来,但是有些奇怪的字母,这种情况需要做处理,c++中应用demangled来解决这个问题,具体操作见下面代码:
#include
#include
#include
#include
#include
using namespace std;
static string demangle(const char* symbol)
{
const unique_ptr< char, decltype( &free ) > demangled(abi::__cxa_demangle( symbol, 0, 0, 0 ), &free );
if (demangled) {
return demangled.get();
}
else {
return symbol;
}
}
void backtrace()
{
void* addresses[256];
const int n = backtrace(addresses, extent< decltype(addresses) > ::value );
const unique_ptr< char*, decltype(&free) > symbols(backtrace_symbols(addresses, n), &free);
for(int i=0; i<n; ++i) {
char* symbol = symbols.get()[i];
char* end = symbol;
int p = 0;
int q = 0;
while (*end) {
++end;
}
while (end != symbol && *end != '+') {
--end;
}
char* begin = end;
while(begin != symbol && *begin != '(') {
--begin;
}
if (begin != symbol) {
string str1 = string(symbol, ++begin - symbol);
*end++ = '\0';
string result = str1 + demangle(begin) + '+' + end;
cout << "result ==== " << result << endl;
}
else {
cout << symbol << endl;;
}
}
}
void func2()
{
backtrace();
}
void func1()
{
func2();
}
int main()
{
func1();
}
相比于上面的代码增加了函数名处理,结果:
./out(backtrace()+0x33) [0x4034f0]
./out(func2()+0xd) [0x4038c4]
./out(func1()+0xd) [0x4038d4]
./out(main+0xd) [0x4038e4]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0x7f6a0c4e9083]
./out(_start+0x2e) [0x4032be]
注:中间指针操作可以优化。
总结
1.linux平台下可以利用函数backtrace、backtrace_symbols、backtrace_symbols_fd来获取当时的函数调用堆栈信息
2.使用上述函数时,需要引用头文件
3.处理函数名格式c++中可以用demangled解决。
4.可以通过addr2line命令获取详细的函数信息。
码字不易,如有帮助,点赞收藏,如有错误,评论指正,谢谢。
END。
—————————————————追加指针操作优化———————————————————
用到了strsep()函数
void backtrace()
{
void* addresses[256];
const int n = backtrace(addresses, extent< decltype(addresses) > ::value );
const unique_ptr< char*, decltype(&free) > symbols(backtrace_symbols(addresses, n), &free);
for(int i=0; i<n; ++i) {
char* symbol = symbols.get()[i];
char *token = NULL;
char *token1 = NULL;
if ((token = strsep(&symbol, "(")) != NULL) {
if ((token1 = strsep(&symbol, "+")) != NULL) {
string result = (string)token + '(' + demangle(token1) + '+' + symbol;
cout << "result === " << result << endl;
}
}
}
}
END.