嵌入式开发之堆栈调试打印

简介

打印堆栈的常用方法包括:

  • glibc中的backtrace函数
  • gcc内置函数__builtin_return_address
  • 第三方库libunwind

1 glibc中的backtrace

1. 1函数原型

#include <execinfo.h>

/*
 * 功能: 获取当前线程的调用堆栈并存放在buffer中(指向字符串数组的指针)
 * @param buffer: 存放当前线程的调用堆栈
 * @param size: 指定buffer中可以保存多少个 void* 元素(void* 元素实际上是从堆栈中获取的返回地址)
 * @return 实际返回的 void* 元素个数
 * */
int backtrace(void **buffer, int size);

/*
 * 功能: 将backtrace函数获取的信息转化为一个字符串数组
 * @param buffer: backtrace获取的堆栈指针
 * @param size: backtrace返回值
 * @return: 一个指向字符串数组的指针, 包含 size 个 char* 元素, 每个元素包含了一个相对于buffer中对应元素的可打印信息(函数名、函数偏移地址和实际返回地址)
 * */
char **backtrace_symbols(void *const *buffer, int size);

/*
 * 功能: 与backtrace_symbols函数功能相同, 但是不会malloc内存, 而是将结果写入文件描述符为fd的文件中
 * */
void backtrace_symbols_fd(void *const *buffer, int size, int fd);

1.2问题

backtrace()backtrace_symbols()都是不可重入的函数,原因在于它们内部都使用了malloc函数,而malloc内部是有锁的。假设某个线程正在调用malloc分配堆空间,此时程序捕捉到信号发生中断,而信号处理函数中恰好也调用了malloc函数就会发生死锁导致该线程hang住,随后所有调用malloc的其他线程也会相继hang住。

backtrace_symbols_fd函数是可重入的,我们用它代替backtrace_symbols打印堆栈信息。

1.3使用注意事项

  • backtrace的实现依赖于栈指针(fp寄存器),在gcc编译过程中任何非零的优化等级(-On参数)或加入了栈指针优化参数-fomit-frame-pointer后可能不能正确得到程序栈信息
  • backtrace_symbols的实现需要符号名称的支持,在gcc编译时需要加入-rdynamic参数
  • 内联函数没有栈帧,它在编译过程中被展开在调用位置
  • 尾调用优化Tail-Call Optimization将复用当前函数栈而不再生成新的函数栈,这将导致栈信息不能被正确获取

1.4例子

#include <libgen.h>
#include <execinfo.h>

void show_backtrace(void)
{
#define SIZE 200
    int nptrs;
    void *buffer[SIZE];
    char **strings;

    nptrs = backtrace(buffer, SIZE);
    printf("backtrace() returned %d address\n", nptrs);
    
    // backtrace_symbols函数不可重入, 可以使用backtrace_symbols_fd替换
    strings = backtrace_symbols(buffer, nptrs);
    if (strings == NULL)
    {
        perror("backtrace_symbols");
        exit(EXIT_FAILURE);
    }

    for (int j = 0; j < nptrs; j++)
    {
        printf("%s\n", strings[j]);
    }

    free(strings);
}

这种方法使用起来很方便,也不需要引入什么第三方库,但是这种方法要求交叉编译器必须是支持glibc的,比如海思的hi3536是uclibc的,就没有backtrace接口,只能使用别的方法。

2 gcc内置函数

我们可以使用gcc内置函数__builtin_return_address(level)打印出一个函数的堆栈地址,其中level表示堆栈中第几层调用地址。

#include <cstdio>

void f() {
    printf("%p,%p\n", __builtin_return_address(0), __builtin_return_address(1));
}

void g() {
    f();
}
int main() {
    g();
}

3 第三方库libunwind

3.1下载

下载地址:http://download-mirror.savannah.gnu.org/releases/libunwind/

这里我下载1.5版本,文件名为:libunwind-1.5.0.tar.gz。

3.2安装

$tar -zxvf libunwind-1.5.0.tar.gz
$cd libunwind-1.5.0
$CFLAGS=-fPIC ./configure --prefix=$(pwd)/.libs
$make CFLAGS=-fPIC
$make CFLAGS=-fPIC install 

如果是编译arm版本,需要在configure的时候指定交叉编译链用 --host=xxxxxx,比如海思hi3536:

$CFLAGS=-fPIC ./configure --host=arm-hisiv500-linux --prefix=$(pwd)/.libs

3.3测试代码

<backtrace.cpp>

#include "backtrace.h"

#ifdef __cplusplus
#include <cxxabi.h>
#endif
#include <dlfcn.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define UNW_LOCAL_ONLY
#include <libunwind.h>
#ifndef __USE_GNU
#define __USE_GNU
#endif
#include <ucontext.h>


void show_backtrace(void)
{
    unw_cursor_t cursor;
    unw_context_t uc;
    unw_word_t ip, sp;
    char func_name_cache[4096];
    func_name_cache[sizeof(func_name_cache) - 1] = 0;
    unw_word_t unw_offset;
    unw_proc_info_t unw_proc;
    int frame_id = 0;
#ifdef __cplusplus
    int status;
#endif

    unw_getcontext(&uc);
    unw_init_local(&cursor, &uc);
    printf("++++++++ backtrace ++++++++\n");
    while (unw_step(&cursor) > 0)
    {
        unw_get_reg(&cursor, UNW_REG_IP, &ip);
        unw_get_reg(&cursor, UNW_REG_SP, &sp);

        unw_get_proc_info(&cursor, &unw_proc);
        unw_get_proc_name(&cursor, func_name_cache, sizeof(func_name_cache) - 1,
                          &unw_offset);
#ifdef __cplusplus
        char *func_name = abi::__cxa_demangle(func_name_cache, 0, 0, &status);
#else
        const char *func_name = func_name_cache;
#endif
        printf("Frame #%02d: (%s+0x%llx) [0x%llx]\n", frame_id,
                      func_name ? func_name : func_name_cache,
                      static_cast<unsigned long long>(unw_offset),
                      static_cast<unsigned long long>(unw_proc.start_ip));
#ifdef __cplusplus
        if (func_name)
            free((void *)func_name);
#endif
        frame_id++;
    }
    printf("+++++++++++++++++++++++++++\n");
}

<backtrace.h>

#ifndef __BACKTRACE_H
#define __BACKTRACE_H

#ifdef __cplusplus
extern "C"
{
#endif

void show_backtrace();

#ifdef __cplusplus
}
#endif

#endif /* __BACKTRACE_H */

<main.cpp>

#include <string.h>
#include <unistd.h>
#include <stdbool.h>
#include <limits.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>

#include "backtrace.h"

typedef void (*sighandler_t)(int);

static const char *program_exec = "trace_cpp";

void crash()
{
    volatile int i = *(int *)7;
    (void)i;
}

void foo2(void)
{
    crash();
}

void foo1(void)
{
    foo2();
}

static void signal_handler(int signo)
{
    printf("Aborting (signal %d) [%s]\n", signo, program_exec);
    show_backtrace();
    exit(EXIT_FAILURE);
}

static void signal_setup(sighandler_t handler)
{
    struct sigaction sa;
    sigset_t mask;

    sigemptyset(&mask);
    sa.sa_handler = handler;
    sa.sa_mask = mask;
    sa.sa_flags = 0;
    sigaction(SIGBUS, &sa, NULL);
    sigaction(SIGILL, &sa, NULL);
    sigaction(SIGFPE, &sa, NULL);
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGABRT, &sa, NULL);
    sigaction(SIGPIPE, &sa, NULL);
}

int main(int argc, char **argv)
{
    signal_setup(signal_handler);

    foo1();

    return 0;
}

编译:

可以通过命令行编译,或者通过cmake构建文件进行编译,然后链接libunwind的静态库以及头文件即可生成测试可执行文件。

运行log:

/mnt/hi3536-workspace # ./trace_cpp
Aborting (signal 11) [trace_cpp]
++++++++ backtrace ++++++++
Frame #00: (signal_handler(int)+0x2c) [0x10eac]
Frame #01: (_setjmp+0xc) [0x10eac]
Frame #02: (crash()+0x10) [0x10e64]
Frame #03: (foo2()+0xc) [0x10e8c]
Frame #04: (foo1()+0xc) [0x10e9c]
Frame #05: (main+0x20) [0x10fa4]
Frame #06: (__uClibc_main+0x298) [0x10fa4]
+++++++++++++++++++++++++++

分析:

从log中,我们看到最上面的一个接口并且是我们测试代码中用到的是,Frame #02,后面的地址是0x10e64,偏移地址是0x10,我们用addr2line在pc上找到具体出问题的地方:

zl@zl-Lenovo:~/vstdio-workspace/hi3536-webapp/backtrace-test/build$ arm-hisiv500-linux-addr2line -C -f -e trace_cpp 0x10e74
crash()
/home/zl/vstdio-workspace/hi3536-webapp/backtrace-test/main.cpp:18

注意addr2line必须要用执行测试代码的交叉编译链里面的,从结果结合我们的测试程序,很明显打印的堆栈信息可以分析出出问题的文件,以及行号,以及出问题的接口。

4 扩展

其实对于异常崩溃的调试,可以通过上面的方法,崩溃的时候打印堆栈信息,然后结合工程源码来找到出问题的地方,另外其实还可以用linux非常非常强大的工具,gdb来运行程序调试。比如上面的应用,我们用gdb来运行:

/mnt/hi3536-workspace # gdb trace_cpp
GNU gdb (GDB) 7.10
Copyright (C) 2015 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "arm-hisiv500-linux".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from trace_cpp...done.
(gdb) r
Starting program: /mnt/hi3536-workspace/trace_cpp

Program received signal SIGSEGV, Segmentation fault.
0x00010e74 in crash () at /home/zl/vstdio-workspace/hi3536-webapp/backtrace-test/main.cpp:18
18      /home/zl/vstdio-workspace/hi3536-webapp/backtrace-test/main.cpp: No such file or directory.
(gdb) pt
The history is empty.
(gdb) bt
#0  0x00010e74 in crash () at /home/zl/vstdio-workspace/hi3536-webapp/backtrace-test/main.cpp:18
#1  0x00010e98 in foo2 () at /home/zl/vstdio-workspace/hi3536-webapp/backtrace-test/main.cpp:24
#2  0x00010ea8 in foo1 () at /home/zl/vstdio-workspace/hi3536-webapp/backtrace-test/main.cpp:29
#3  0x00010fc4 in main (argc=1, argv=0xbefffe04) at /home/zl/vstdio-workspace/hi3536-webapp/backtrace-test/main.cpp:61
(gdb)

可以很清楚的看到出问题的接口,以及位于哪个文件的哪一行。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值