• 记录:C++打印堆栈信息并优化打印结果


    1.介绍打印堆栈信息函数
    头文件:

    #include 
    
    • 1

    函数原型

    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);
    
    • 1
    • 2
    • 3

    函数描述
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    编译:

    gcc -g  main.c -o out
    
    • 1

    执行结果:

    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
    • 2
    • 3
    • 4
    • 5
    • 6

    这种打印存在两个问题:
    问题1:没有打印出调用函数信息
    解决方法:编译时候增加-rdynamic

    gcc -g -rdynamic  main.c -o out
    
    • 1

    结果:

    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]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    问题2:我们可以用addr2line命令通过地址查看调用函数信息

    addr2line -Cif -e out 0x559283455210
    
    • 1

    结果:产生??

    ??
    ??:0
    
    • 1
    • 2

    原因:addr2line输入的地址并非其所接受的地址,addr2line接受的地址是相对偏移地址。
    解决方法:编译的时候增加-no-pie

    gcc -g -no-pie -rdynamic main.c -o out
    
    • 1

    结果:这里的地址和上面的地址就不同了,用这个地址输入到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]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    root@ubuntu:~# addr2line -Cif -e out 0x4011fd
    print_trace
    /root/zby/demo/backtrace_demo/main.c:13
    
    • 1
    • 2
    • 3

    注:(这里有个问题解释不明白,希望有会的大佬看到可以解释一下,以供学习)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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    编译:

    g++ main1.cpp -g -rdynamic -no-pie -o out
    
    • 1

    结果:

    ./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]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    上面的调用堆栈中函数名大致能看出来,但是有些奇怪的字母,这种情况需要做处理,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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    相比于上面的代码增加了函数名处理,结果:

    ./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
    • 2
    • 3
    • 4
    • 5
    • 6

    注:中间指针操作可以优化。

    总结
    1.linux平台下可以利用函数backtrace、backtrace_symbols、backtrace_symbols_fd来获取当时的函数调用堆栈信息
    2.使用上述函数时,需要引用头文件,编译时最好加上-rdynamic选项和-no-pie选项。
    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;
    			}
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    END.

  • 相关阅读:
    JZ4 二维数组中的查找
    维格云身份认证入门教程
    【全】差分模板
    9.Springboot整合Security很全
    合作QA是大聪明?撸个接口校验工具保命(5)
    解决报错:RuntimeError: “LayerNormKernelImpl“ not implemented for ‘Half‘
    【k8s】pod控制器
    【测试】1. 概念 + 基础篇
    鲜花绿植学生网页设计模板 静态HTML鲜花学生网页作业成品 DIV CSS网上鲜花植物主题静态网页
    Istio Arch-Overview
  • 原文地址:https://blog.csdn.net/weixin_42736510/article/details/126307156