backtrace函数与assert断言宏封装

2023-11-17

这篇文章是在阅读 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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

backtrace函数与assert断言宏封装 的相关文章

  • C# 网络编程之Tcp实现客户端和服务器聊天

    最近使用Socket网络套接字编程中 在同步与异步通讯中客户端与服务器总是无法响应 但在学习Tcp协议编程中完成了通讯聊天功能 下面简单讲讲我最近学到的及Tcp聊天的源代码及详细注释 Tcp协议是一个传输层的协议 在Tcp协议编程中它通常使
  • OpenGL超级宝典 纹理(一)

    文章目录 纹理 创建并且初始化纹理 更新纹理数据 从着色器中读取数据 采样器类型 控制纹理数据的读取方式 创建采样器对象和绑定到纹理单元 纹理过滤 设置过滤器 加载纹理 完整代码展示 shader vertex shader fragmen
  • mysql 授权管理和设置

    1 给指定数据库增加所有权限 所有库即 GRANT ALL PRIVILEGES ON TO 用户名 IDENTIFIED BY 密码 WITH GRANT OPTION 设置完之后更新权限表 FLUSH PRIVILEGES 2 给用户增
  • C++11模板元编程-std::enable_if示例详解

    文章目录 1 限制模板函数的参数类型 2 模板类型偏特化 传送门 gt gt AutoSAR实战系列300讲 糖果Autosar 总目录 C 11中引入了std enable if函数 函数原型如下 template lt bool B c
  • AI+数据安全,探索数据安全防护新手段

    随着 4G 正式商用 带宽将不再是数据传输的瓶颈 人类社会真正意义的进入了以手持终端 各类传感器为代表的移动互联网 万物互联 人工智能时代 我们将不再受限于地理位置 可尽情享受着手机购物 电子支付 媒体社交 个性化推送 VR等各种便捷和个性
  • 计算机图形学十五:基于物理的渲染(蒙特卡洛路径追踪)

    蒙特卡洛路径追踪 摘要 1 蒙特卡洛积分 Monte Carlo Integration 2 蒙特卡洛路径追踪 Monte Carlo Path Tracing Reference 本篇文章同步发表于知乎专栏 https zhuanlan
  • PHP与JSON的一些常用操作

    PHP把数据写入JSON文件 PHP读取JSON数据
  • C++ 抽象类

    抽象类 接口 接口描述了类的行为和功能 而无需完成类的特定实现 C 接口时通过抽象类实现的 设计抽象类的目的 是为了给其他类提供一个可以继承的适当的基类 抽象类本类不能被用于实例化对象 只能作为接口使用 注意 如果试图实例化一个抽象类的对象
  • 对象的初始化和清理

    对象的初始化和清理 构造函数和析构函数 对象的初始化和清理也是两个非常重要的安全问题 一个对象或者变量没有初始状态 对其使用后果是未知 同样的使用完一个对象或变量 没有及时清理 也会造成一定的安全问题 c 利用了构造函数和析构函数解决上述问
  • visual studio2019创建解决方案,并在一个解决方案中包含多个项目

    系列文章目录 文章目录 系列文章目录 前言 一 使用步骤 前言 之前一直使用visual studio2019一直都是一个解决方案 下面包含一个工程 这次写一个网络同步的模块 具体使用boost的asio模块 我们需要建立一个解决方案 一个
  • 使用slickedit调试开源代码

    slickedit linux下的神器啊 阅读代码堪比 source insight 调试代码堪比 visual studio nginx优秀的web服务器 因为其具有多进程 后台进程的特点 因此本文选择以此为例讲解slickedit如何对
  • Java中的排序算法

    冒泡排序 核心思想 冒泡排序 核心思想 冒泡排序 Bubble Sort 又被称为气泡排序或泡沫排序 它是一种较简单的排序算法 它会遍历若干次要排序的数列 每次遍历时 它都会从前往后依次的比较相邻两个数的大小 如果前者比后者大 则交换它们的
  • LeetCode题解——394. 字符串解码

    题目相关 题目链接 LeetCode中国 https leetcode cn com problems decode string 注意需要登录 题目描述 给定一个经过编码的字符串 返回它解码后的字符串 编码规则为 k encoded st
  • 昨晚做梦面试官问我三色标记算法

    本文已收录至GitHub 推荐阅读 Java随想录 微信公众号 Java随想录 原创不易 注重版权 转载请注明原作者和原文链接 文章目录 三色标记算法 增量更新 原始快照 某天 爪哇星球上 一个普通的房间 正在举行一场秘密的面试 面试官 我
  • Sql server 存储过程加密

    本方法可用于加密SQL存储过程 函数或者触发器 使用 WITH ENCRYPTION 选项 WITH ENCRYPTION 子句对用户隐藏存储过程的文本 例子 IF OBJECT ID N Pro Encrypt Test IS NOT N
  • PySide6-控件教程-005-QLabel标签控件-内边距、缩放、伙伴关系

    QLabel 标签控件 本文摘录自我的开源教程 PySide6 代码式教程 QLabel CSDN 平台仅做镜像 答疑 纠错请至 GitHub 提交 issue 内边距 QLabel还可以调整内边距 启用内容缩放 以更细致地调节显示效果 s
  • 与游戏世界交互作业

    一 编写一个简单的鼠标打飞碟 Hit UFO 游戏 游戏内容要求 游戏有 n 个 round 每个 round 都包括10 次 trial 每个 trial 的飞碟的色彩 大小 发射位置 速度 角度 同时出现的个数都可能不同 它们由该 ro
  • 如何将Python项目部署到新电脑上运行?

    如何将Python项目部署到新电脑上运行 在工作中 可能需要在新服务器上部署项目代码 例如新增服务器 把测试环境的代码部署到生产环境等 在生活中 也会遇到换新电脑 需要将自己在旧电脑上写的 项目 代码拷贝到新电脑上运行 本文将这个过程中的关

随机推荐

  • SSH版本信息可被获取漏洞解决方法CVE-1999-0634

    直接执行 cd etc touch ssh banner change echo Version is empty gt gt etc ssh banner change cd etc ssh cp sshd config sshd con
  • log4j漏洞复现

    第一步 下载marshalsec 源码进行编译 https github com mbechler marshalsec 下载后进行编译打包 mvn clean package DskipTests 得到jar文件 在这里插入图片描述 第二
  • Stable Diffusion 系列教程

    目录 1 提示词 基本的规则 2 提示词分类 2 1内容性提示词 2 2 画风艺术派提示词 2 3 画幅视角 2 4画质提示词 3 反向提示词 3 1 内容性反向提示词 3 2 画质性反向提示词 4 实例分析 5 权重 5 1 方法一 5
  • 无线传感网必知必会

    一 填空题 传感器网络三大基本要素 传感器 感知对象 用户 观测者 传感器节点的基本功能模块包括 数据采集模块 数据处理和控制模块 通信模块 供电模块 四个 其中 通信模块 能量消耗最大 传感器节点通信模块的工作模式有 发送 接收 空闲 睡
  • java七大排序——7_归并排序

    归并排序 将数组分为2块 再到每一小块再分为两块 直到最后一个元素为一块 然后进行有序数组合并 最终合并为一个有序数组 代码实现 public static void mergeSorts int array mergeSortsInter
  • 软件设计师--结构化开发

    结构化开发 耦合 真题 内聚 真题 设计原则 真题 系统文档 真题 数据流图 数据流图基本数据元素 外部实体 数据存储 加工 数据流 父图子图平衡 加工既要有输入数据流也要有输出数据流 数据守恒 真题 数据字典 真题 杂题精选 耦合 真题
  • [1051]python yagmail发邮件

    文章目录 安装 开通SMTP服务 常用邮箱host以及port yagmail 可以更简单的来实现自动发邮件功能 github项目地址 https github com kootenpv yagmail 安装 pip install yag
  • 备战金九银十: GitHub 上标星 46k+的《10 万字Java面试总结》,助你搞定面试官

    不论是校招还是社招都避免不了各种面试 笔试 如何去准备这些东西就显得格外重要 不论是笔试还是面试都是有章可循的 我这个有章可循 说的意思只是说应对技术面试是可以提前准备 运筹帷幄之后 决胜千里之外 不打毫无准备的仗 我觉得大家可以先从下面几
  • python tkinter 点击按钮选择文件,返回文件路径

    关于python tkinter 点击按钮选择文件 返回文件路径 这个方法我找了好几天 终于曲线救国实现了 首先分为两步 1 设计对话框选择文件 下面的代码搞了好几天 才发现全局变量的获取 必须放在root mainloop的最后 反正网上
  • MAC软件推荐(Java方向)

    MAC软件推荐 Tabby 终端控制工具 keka 解压工具 typora Markdown工具 QuickRedis Redis视图工具 UTM 虚拟机 Navicat Premium 数据库工具 Adobe Photoshop CC 2
  • Android-App的设计架构经验谈,终获offer

    前言 想要成为一名优秀的Android开发 你需要一份完备的知识体系 在这里 让我们一起成长为自己所想的那样 学算法真的很痛苦 虽然大数据现在很火 但找到适合自己定位的职业也未尝不是一种合理选择 投百度的经历非常坎坷 想写出来和大家分享一下
  • runtimeService 运行时服务组件

    在Activiti中 启动一个流程后 会创建一个流程实例 ProcessInstance继承Execution 两个都是接口 每个流程实例至少会有一个执行流 Execution 当流程实例没有流程分支时 一般情况下只会存在一个执行流 假设出
  • 计算机采用二进制每秒,计算机为什么采用二进制

    计算机为什么采用二进制 2018 09 12 电脑为什么要采用二进制计算 计算机中的一切计算都是用二进制进行的 平时我们用的十进制是逢十进一 二进制则是逢二进一 我们用的算盘事实上有两种用法 一种是十进制 一种是十六进制 算盘中代表 五 的
  • 嵌入式Linux下用C语言写后端接口——CGI实现

    文章目录 简介 实验环境 下载CGIC库源码 配置CGIC编译 测试CGI接口 编写一个简单的获取表单的CGI接口 测试login cgi CGIC接口API 简介 CGI Common Gateway Interface 公共网关接口 是
  • Python更改文件的编码格式

    Python更改文件的编码格式 import os from chardet universaldetector import UniversalDetector def change encode file change 2 type d
  • MySQL Flashback 闪回功能详解

    1 简介 mysqlbinlog flashback 闪回 用于快速恢复由于误操作丢失的数据 在DBA误操作时 可以把数据库恢复到以前某个时间点 或者说某个binlog的某个pos 比如忘了带where条件的update delete操作
  • FreeType简介及在vs2010的编译使用

    FreeType库是一个开源 高质量 可扩展 可定制 可移植的字体引擎 它提供统一的接口来访问多种字体格式文件 包括点阵字 TrueType OpenType Type1 CID CFF Windows FON FNT X11 PCF等 F
  • 2021.11.30 面试题

    1 请你介绍一下map的分类和常见的情况 java为数据结构中的映射定义了一个接口java util Map 它有四个实现类 分别是HashMap Hashtable LinkedHashMap 和TreeMap Map主要用于存储健值对
  • simulink仿真 adc 采样ePWM输出例程

    新建文件夹并用matlab打开 写入这两个模块 配置 ADC 配置ePWM 不使能B 关了就行 其他的默认即可 配置烧录 连线 示波器接pwma1 和地 adc chanl1接 3 3v或者 0 3 3 都行 转化是 x 3 3 2 12
  • backtrace函数与assert断言宏封装

    这篇文章是在阅读 sylar 框架时 对断言宏的封装所做的总结 在实际开发中 我们经常会遇到一种境况 如果程序执行的不是我们想要的正确结果 需要程序立即中断执行 我们希望得到其有效的错误信息 比如其出现错误的函数 文件 代码行号 和参数文本