backtrace函数与assert断言宏封装

这篇文章是在阅读 sylar 框架时,对断言宏的封装所做的总结。

在实际开发中,我们经常会遇到一种境况:如果程序执行的不是我们想要的正确结果,需要程序立即中断执行,我们希望得到其有效的错误信息,比如其出现错误的函数、文件、代码行号、和参数文本、调用堆栈信息等。通常我们会在程序中使用断言 assert,因为如果出现了不符合条件的情况,程序将终止执行,而且会打印出一些有限的信息。

assert 函数

断言就说明是绝对不可能出现的错误,一旦出现就不能让程序继续执行下去,而且需要在 debug 阶段将 assert 检查出来的问题全部修复掉。受到以前硬件资源限制,断言太多非常影响效率,在 release 版本中断言都会去掉。断言一般出现在核心模块内部,主要作用就是在 debug 阶段清除所有bug,而这些 bug 也绝不能留到 release 阶段。

assert 如果断言其表达式为假,则终止程序,并输出有限的调试信息,这个宏定义可以帮助我们在程序中发现错误,或者通过崩溃处理异常情况。

比如我们有以下程序:

void my_assert()
{
    int a = 10;
    assert(a == 100);
}

void Func2()
{
    my_assert();
}

void Func1()
{
    Func2();
}

int main(int argc, char *argv[])
{
    Func1();
    return 0;
}

程序执行到 assert 表达式时,判断 a == 100 是否成立,因为不成立,将终止程序,程序的运行结果如下:

$ ./bin/test_util 
test_util:tests/test_util.cpp:71: void my_assert(): Assertion `a == 100' failed.
Aborted (core dumped)

可以看到输出的信息中包含了调用的文件、函数、代码行号和参数文本信息。

虽然可以得到基本的信息,但是得到的信息还是不够充分,比如我们想知道该函数的调用堆栈信息,这时候就需要另一个强大的函数 backtrack了。

backtrace 系列函数

Linux 下提供了 backtracebacktrace_symbolsbacktrace_symbols_fd 函数来支持对应用程序的调试。

其函数签名如下:

#include <execinfo.h>
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* 元素。函数的返回值是实际返回的 void* 元素个数。buffer 中的 void* 元素实际是从堆栈中获取的返回地址。

backtrace_symbols 函数

该函数将 backtrace 函数获取的信息转化为一个字符串数组,参数 buffer 是 backtrace 获取的堆栈指针,size 是backtrace 返回值。函数返回值是一个指向字符串数组的指针,它包含 char* 元素个数为 size 。每个字符串包含了一个相对于 buffer 中对应元素的可打印信息,包括函数名、函数偏移地址和实际返回地址。

backtrace_symbols 生成的字符串占用的内存是 malloc 出来的,但是是该一次性 malloc 出来的,释放是只需要一次性释放返回的二级指针即可。

backtrace_symbols_fd 函数

该函数与 backtrace_symbols 函数功能相同,只是它不会 malloc 内存,而是将结果写入文件描述符为 fd 的文件中,每个函数对应一行。该函数可重入。

backtrace函数注意事项

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

捕获异常信号并打印堆栈

当程序出现崩溃等异常时,会接收到内核发送给进程的异常信号,进程接收到异常信号后,可以在处理信号的时候将程序的堆栈信息打印出来,以便于程序调试。

backtrace函数实例

这是 backtrack 函数的示例程序,可以先参考一下:

#include <stdio.h>
#include <execinfo.h>
#include <unistd.h>
#include <stdlib.h>
#define BACKTRACE_SIZE 100

void print_backtrace()
{
    void *buffer[BACKTRACE_SIZE] = {0};
    int pointer_num = backtrace(buffer, BACKTRACE_SIZE);
    char **string_buffer = backtrace_symbols(buffer, pointer_num);
    if (string_buffer == NULL)
    {
        printf("backtrace_symbols error");
        exit(-1);
    }

    printf("print backtrace begin\n");
    for (int i = 0; i < pointer_num; i++)
    {
        printf("%s\n", string_buffer[i]);
    }
    printf("print backtrace end\n");

    free(string_buffer); //需要手动释放空间

    return;
}

void my_assert()
{
    print_backtrace();
}

void Func2()
{
    my_assert();
}

void Func1()
{
    Func2();
}

int main(int argc, char *argv[])
{
    Func1();
    return 0;
}

执行结果如下,注意在编译时带 -rdynamic 参数:

$ ./bin/test_util 
print backtrace begin
./bin/test_util(_Z15print_backtracev+0x45) [0x7f7af6000b0f]
./bin/test_util(_Z9my_assertv+0x9) [0x7f7af6000be5]
./bin/test_util(_Z5Func2v+0x9) [0x7f7af6000bf1]
./bin/test_util(_Z5Func1v+0x9) [0x7f7af6000bfd]
./bin/test_util(main+0x14) [0x7f7af6000c14]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7f7af5821b97]
./bin/test_util(_start+0x2a) [0x7f7af60009ea]
print backtrace end

在上面说过,我们不能对其使用 -On优化,否则打印出的结果是不确定的。

从结果中可以看出函数的调用堆栈信息,但是不够直观,因为其中还有一些地址信息相关的,我们需要对其结果进行过滤得到一个格式良好的堆栈信息。

封装backtrace系列函数

通过上面的示例可以看出 backtrace 函数的运行结果了,但是如果我们想要一个完整清晰的格式,还需要自己对其进行封装,这里参照 sylar 框架中的封装方法,简单的对其封装如下:

static std::string demangle(const char *str)
{
    size_t size = 0;
    int status = 0;
    std::string rt;
    rt.resize(256);
    if (1 == sscanf(str, "%*[^(]%*[^_]%255[^)+]", &rt[0]))
    {
        char *v = abi::__cxa_demangle(&rt[0], nullptr, &size, &status);
        if (v)
        {
            std::string result(v);
            free(v);
            return result;
        }
    }
    if (1 == sscanf(str, "%255s", &rt[0]))
    {
        return rt;
    }
    return str;
}

/**
 * @brief 获取当前的调用栈
 * @param[out] bt 保存调用栈
 * @param[in] size 最多返回层数
 * @param[in] skip 跳过栈顶的层数
 */
void Backtrace(std::vector<std::string> &bt, int size = 64, int skip = 1)
{
    void **array = (void **)malloc((sizeof(void *) * size));
    size_t s = ::backtrace(array, size);
    char **strings = backtrace_symbols(array, s);
    if (strings == NULL)
    {
        std::cout << "backtrace_synbols error." << std::endl;
        return;
    }
    for (size_t i = skip; i < s; ++i)
    {
        bt.push_back(demangle(strings[i]));
    }
    free(strings);
    free(array);
}

/**
 * @brief 获取当前栈信息的字符串
 * @param[in] size 栈的最大层数
 * @param[in] skip 跳过栈顶的层数
 * @param[in] prefix 栈信息前输出的内容
 */
std::string BacktraceToString(int size = 64, int skip = 2, const std::string &prifix = "")
{
    std::vector<std::string> bt;
    Backtrace(bt, size, skip);
    std::stringstream ss;
    for (size_t i = 0; i < bt.size(); ++i)
    {
        ss << prifix << bt[i] << std::endl;
    }
    return ss.str();
}

测试程序如下:

#include <iostream>
#include <assert.h>
#include <stdio.h>
#include <execinfo.h>
#include <unistd.h>
#include <stdlib.h>
#include <vector>
#include <execinfo.h>
#include <cxxabi.h>
#include <sstream>

//...

void my_assert()
{
    std::cout << BacktraceToString(10, 0, " --- ") << std::endl;
}

void Func2()
{
    my_assert();
}

void Func1()
{
    Func2();
}

int main(int argc, char *argv[])
{
    Func1();
    return 0;
}

运行结果如下:

$ ./bin/test_util 
 --- Backtrace(std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&, int, int)
 --- BacktraceToString(int, int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
 --- my_assert()
 --- Func2()
 --- Func1()
 --- ./bin/test_util(main+0x14)
 --- /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7)
 --- ./bin/test_util(_start+0x2a)

可以看到,经过封装后的函数打印出的堆栈信息非常清晰明了,我们可以为其指定调用栈的层数和跳过栈顶的层数,同时还可以指定其栈信息前输出的内容。

在 demangle 函数中,我们调用了一个 abi::__cxa_demangle 的函数,虽然 C++ 中有一个 typeid 操作可以获取一个类型的名称,但是在 gcc 中并不能得到想要的结果,如下程序:

typedef int (*pFun)(int, int);
std::cout << typeid(pFun).name() << std::endl;

msvc 编译器中打印出来的信息非常清晰,但是我们上面已经见识过 gcc 默认打印出来的栈函数信息,gcc 打出来将是如下:

PFiiiE

如果我们想要在 gcc 中打印出类似于 msvc 的信息比较详细的效果,就需要使用 abi::__cxa_demangle ,我们使用这个函数打印一个上面的程序:

char *name = abi::__cxa_demangle(typeid(pFun).name(), nullptr, nullptr, nullptr);
std::cout << name << std::endl;
free(name);

效果将会是:

int (*)(int, int)

同时我们再进行字符串的处理,就能清晰明了的函数调用堆栈信息了。

封装断言宏

上面已经将 backtrace 函数进行封装,也能得到清晰明了的函数调用堆栈信息。现在可以进行自己的函数的断言宏的封装。

#if defined __GNUC__ || defined __llvm__
#   define LIKELY(x)       __builtin_expect(!!(x), 1)
#   define UNLIKELY(x)     __builtin_expect(!!(x), 0)
#else
#   define LIKELY(x)      (x)
#   define UNLIKELY(x)    (x)
#endif

#define MY_ASSERT(x) \
    if (UNLIKELY(!(x))) \
    { \
        std::cout << "ASSERTION:" #x \
            << "\nbacktrace:\n" \
            << BacktraceToString(100, 2, " --- "); \
    }

上面定义了 LIKELY 和 UNLIKELY 宏定义,使用到了 __builtin_expect 分支预测优化,在另一篇文章中有详细说明,点击查看

测试代码直接将上面的函数 my_assert 修改一下:

void my_assert()
{
    int a = 10;
    MY_ASSERT(a == 100)
}

在代码中,如果想要使用断言,就可以换成自己封装的 MY_ASSERT 宏,这个宏打印出来的格式如下:

$ ./bin/test
ASSERTION:a == 100
backtrace:
 --- my_assert()
 --- Func2()
 --- Func1()
 --- ./bin/test_log(main+0x14)
 --- /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7)
 --- ./bin/test_log(_start+0x2a)

相比最开始的的 assert 函数是不是清晰明了太多了呢。

参考
如何在C++中获得完整的类型名称
https://github.com/sylar-yin/sylar

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
回溯算法是一种通过穷举所有可能的解来求解问题的算法。它通常用于解组合优化问题,其中需要在给约束条件下找到最优解。01背包问题是一个经典的组合优化问题,它要求在给定背包容量和一组物品的重量和价值的情况下,选择一些物品放入背包中,使得背包中物品的总价值最大,同时不能超过背包的容量。 回溯算法解决01背包问题的基本思想是通过递归的方式遍历所有可能的解空间,并在搜索过程中进行剪枝,以提高搜索效率。具体步骤如下: 1. 定义一个递归函数backtrack,该函数接受当前背包容量、当前物品索引和当前背包中物品的总价值作为参数。 2. 在递归函数中,首先判断当前物品索引是否超过物品总数或者当前背包容量是否小于等于0,如果是,则返回当前背包中物品的总价值。 3. 如果不满足上述条件,则有两种情况: - 将当前物品放入背包中,更新背包容量和物品总价值,并递归调用backtrack函数。 - 不将当前物品放入背包中,直接递归调用backtrack函数。 4. 在递归调用后,比较两种情况的结果,返回较大的总价值作为当前背包中物品的最大总价值。 下面是一个使用回溯算法解决01背包问题的Python示例代码: ```python def backtrack(capacity, weights, values, index, total_value): if index >= len(weights) or capacity <= 0: return total_value # 将当前物品放入背包中 if capacity >= weights[index]: total_value1 = backtrack(capacity - weights[index], weights, values, index + 1, total_value + values[index]) else: total_value1 = 0 # 不将当前物品放入背包中 total_value2 = backtrack(capacity, weights, values, index + 1, total_value) return max(total_value1, total_value2) # 示例数据 capacity = 10 weights = [2, 3, 4, 5] values = [3, 4, 5, 6] max_value = backtrack(capacity, weights, values, 0, 0) print("Max value: ", max_value) ``` 这段代码中,我们定义了一个backtrack函数来求解01背包问题。在示例数据中,背包容量为10,物品的重量和价值分别为[2, 3, 4, 5]和[3, 4, 5, 6]。运行代码后,将输出最大总价值。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

code_peak

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值