用户态使用 glibc/backtrace 追踪函数调用堆栈定位段错误

2023-05-16


title: 用户态使用 glibc/backtrace 追踪函数调用堆栈定位段错误
date:2018-11-17 15:22
author: gatieme
tags: linux
categories:
- debug
thumbnail:
blogexcerpt: 一般用户态程序出现段错误, 而我们想要察看函数运行时堆栈, 常用的方法是使用GDB(bt命令)之类的外部调试器,但是有些时候为了分析程序的BUG,(主要针对长时间运行程序的分析),在程序出错时打印出函数的调用堆栈是非常有用的. C 库中提供了一些堆栈 backtrace 的函数用于跟踪函数的堆栈信息, 我们也可以通过注册异常处理函数来实现函数异常时自动打印调用栈.


CSDNGitHubHexo
用户态使用 glibc/backtrace 追踪函数调用堆栈定位段错误AderXCoding/language/c/backtraceKernelShow(gatieme.github.io)

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可, 转载请注明出处, 谢谢合作

因本人技术水平和知识面有限, 内容如有纰漏或者需要修正的地方, 欢迎大家指正, 也欢迎大家提供一些其他好的调试工具以供收录, 鄙人在此谢谢啦


一般察看函数运行时堆栈的方法是使用 GDB(bt命令) 之类的外部调试器, 但是, 有些时候为了分析程序的 BUG,(主要针对长时间运行程序的分析),在程序出错时打印出函数的调用堆栈是非常有用的.

#1 glibc 获取堆栈信息的接口

glibc 头文件 execinfo.h 中声明了三个函数用于获取当前线程的函数调用堆栈.

#1.1

#include <execinfo.h>

/* Store up to SIZE return address of the current program state in
   ARRAY and return the exact number of values stored.  */
int backtrace(void **array, int size);

/* Return names of functions from the backtrace list in ARRAY in a newly
   malloc()ed memory block.  */
char **backtrace_symbols(void *const *array, int size);

/* This function is similar to backtrace_symbols() but it writes the
   result immediately to a file.  */
void backtrace_symbols_fd(void *const *array, int size, int fd);

使用它们的时候有一下几点需要我们注意的地方:

  • backtrace的实现依赖于栈指针(fp寄存器), 在gcc编译过程中任何非零的优化等级(-On参数)或加入了栈指针优化参数 -fomit-frame-pointer 后多将不能正确得到程序栈信息;

  • backtrace_symbols的实现需要符号名称的支持, 在gcc编译过程中需要加入 -rdynamic 参数

  • 内联函数没有栈帧, 它在编译过程中被展开在调用的位置;

  • 尾调用优化(Tail-call Optimization)将复用当前函数栈, 而不再生成新的函数栈, 这将导致栈信息不能正确被获取。

##1.2 backtrace

int backtrace(void **buffer,int size)

该函数用于获取当前线程的调用堆栈,

参数:
获取的信息将会被存放在 buffer 中,它是一个指针列表.
参数 size 用来指定 buffer 中可以保存多少个 void* 元素.

函数返回值:
实际获取的指针个数, 最大不超过 size大小.

buffer 中的指针实际是从堆栈中获取的返回地址, 每一个堆栈框架有一个返回地址

注意:某些编译器的优化选项对获取正确的调用堆栈有干扰,另外内联函数没有堆栈框架;删除框架指针也会导致无法正确解析堆栈内容

##1.3 backtrace_symbols

char ** backtrace_symbols (void *const *buffer, int size)

backtrace_symbols 将从 backtrace 函数获取的信息转化为一个字符串数组.

参数:
buffer 应该是从 backtrace 函数获取的指针数组
size 是该数组中的元素个数(backtrace 的返回值)

函数返回值:
一个指向字符串数组的指针, 它的大小同 buffer 相同.
每个字符串包含了一个相对于buffer中对应元素的可打印信息.
它包括函数名,函数的偏移地址,和实际的返回地址

现在, 只有使用ELF二进制格式的程序才能获取函数名称和偏移地址. 在其他系统,只有16进制的返回地址能被获取.
另外,你可能需要传递相应的符号给链接器,以能支持函数名功能

(比如,在使用GNU ld链接器的系统中,你需要传递(-rdynamic), -rdynamic可用来通知链接器将所有符号添加到动态符号表中,如果你的链接器支持-rdynamic的话,建议将其加上!)

该函数的返回值是通过malloc函数申请的空间,因此调用者必须使用free函数来释放指针.

注意 : 如果不能为字符串获取足够的空间函数的返回值将会为NULL

##1.4 backtrace_symbols_fd

void backtrace_symbols_fd (void *const *buffer, int size, int fd)

backtrace_symbols_fdbacktrace_symbols 函数具有相同的功能, 不同的是它不会给调用者返回字符串数组,而是将结果写入文件描述符为fd 的文件中, 每个函数对应一行.它不需要调用malloc函数,因此适用于有可能调用该函数会失败的情况

#2 示例

##2.1 简单用例(glibc 提供)

下面是 glibc 中的实例:

// http://www.gnu.org/software/libc/manual/html_node/Backtraces.html
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>

/* Obtain a backtrace and print it to @code{stdout}. */
void print_trace (void)
{
	void    * array[10];
	size_t  size;
	char    ** strings;
	size_t  i;

	size = backtrace(array, 10);
	strings = backtrace_symbols (array, size);
	if (NULL == strings)
	{
		perror("backtrace_symbols");
		exit(EXIT_FAILURE);
	}

	printf ("Obtained %zd stack frames.\n", size);

	for (i = 0; i < size; i++)
		printf ("%s\n", strings[i]);

	free (strings);
	strings = NULL;
}

/* A dummy function to make the backtrace more interesting. */
void dummy_function (void)
{
	print_trace();
}

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

输出如下:

gcc -c example.c -o example.o    -rdynamic -g
gcc example.o -o example  -rdynamic -g

#./example

Obtained 5 stack frames.
./example(print_trace+0x19) [0x400916]
./example(dummy_function+0x9) [0x4009bb]
./example(main+0x14) [0x4009d1]
/lib64/libc.so.6(__libc_start_main+0xf5) [0x7fb5e7f49445]
./example() [0x400839]

example

##2.2 简单使用(man手册)

//http://man7.org/linux/man-pages/man3/backtrace.3.html
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

	void
myfunc3(void)
{
	int j, nptrs;
#define SIZE 100
	void *buffer[100];
	char **strings;

	nptrs = backtrace(buffer, SIZE);
	printf("backtrace() returned %d addresses\n", nptrs);

	/* The call backtrace_symbols_fd(buffer, nptrs,
	 * STDOUT_FILENO)
	 *               would produce similar output to the
	 *               following: */

	strings = backtrace_symbols(buffer, nptrs);
	if (strings == NULL) {
		perror("backtrace_symbols");
		exit(EXIT_FAILURE);
	}

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

	free(strings);
}

static void   /* "static" means don't export the symbol... */
myfunc2(void)
{
	myfunc3();
}

void
myfunc(int ncalls)
{
	if (ncalls > 1)
		myfunc(ncalls - 1);
	else
		myfunc2();
}

编译运行程序

gcc -c prog.c -o prog.o    -rdynamic -g
gcc prog.o -o prog  -rdynamic -g


#./prog 3

backtrace() returned 8 addresses
./prog(myfunc3+0x1f) [0x4009cc]
./prog() [0x400a61]
./prog(myfunc+0x25) [0x400a88]
./prog(myfunc+0x1e) [0x400a81]
./prog(myfunc+0x1e) [0x400a81]
./prog(main+0x59) [0x400ae3]
/lib64/libc.so.6(__libc_start_main+0xf5) [0x7f1d1b1f1445]
./prog() [0x4008e9]

prog

##2.3 段错误时自动触发 call trace

我们还可以利用这 backtrace 来定位段错误位置.

通常情况系, 程序发生段错误时系统会发送 SIGSEGV 信号给程序, 缺省处理是退出函数.

我们可以使用 signal(SIGSEGV, &your_function); 函数来接管 SIGSEGV 信号的处理,
程序在发生段错误后, 自动调用我们准备好的函数, 从而在那个函数里来获取当前函数调用栈.

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <execinfo.h>
#include <signal.h>

/* Obtain a backtrace and print it to stdout. */
#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
void dump_stack(void)
{
	void *array[30] = { 0 };
	size_t size = backtrace(array, ARRAY_SIZE(array));
	char **strings = backtrace_symbols(array, size);
	size_t i;

	if (strings == NULL)
	{
		perror("backtrace_symbols.");
		exit(EXIT_FAILURE);
	}

	printf("Obtained %zd stack frames.\n", size);

	for (i = 0; i < size; i++)
		printf("%s\n", strings[i]);

	free(strings);
	strings = NULL;

	exit(EXIT_SUCCESS);
}

void sighandler_dump_stack(int sig)
{
	psignal(sig, "handler");
	dump_stack();
	signal(sig, SIG_DFL);
	raise(sig);
}

void func_c()
{
	*((volatile int *)0x0) = 0x9999; /* ERROR */
}

void func_b()
{
	func_c();
}

void func_a()
{
	func_b();
}


int main(int argc, const char *argv[])
{
	if (signal(SIGSEGV, sighandler_dump_stack) == SIG_ERR)
		perror("can't catch SIGSEGV");

	func_a();

	return 0;
}

编译该程序

cc -c handler.c -o handler.o    -rdynamic
cc handler.o -o handler  -rdynamic

接着运行.

#./handler

handler: Segmentation fault
Obtained 9 stack frames.
./handler(dump_stack+0x39) [0x400aa6]
./handler(sighandler_dump_stack+0x1f) [0x400b6c]
/lib64/libc.so.6(+0x362f0) [0x7f0bc00f72f0]
./handler(func_c+0x9) [0x400b90]
./handler(func_b+0xe) [0x400ba6]
./handler(func_a+0xe) [0x400bb6]
./handler(main+0x38) [0x400bf0]
/lib64/libc.so.6(__libc_start_main+0xf5) [0x7f0bc00e3445]
./handler() [0x4009a9]

程度异常后自动打印调用栈

可以看出, 真正出异常的函数位置在 ./handler(func_c+0x9) [0x400b90].

我们可以看下这个位置位于哪里:

  • 使用 addr2line
addr2line -C -f -e  ./handler 0x400b90

addr2line 将行号显示出来

对应错误的行号.

bugline

  • 使用 objdump

使用 objdump 将函数的指令信息 dump 出来.
其中 -D 参数表示显示所有汇编代码, -S 表示将对应的源码也显示出来
最后用 grep 显示地址 0x400b90 处前后 6 行的信息

objdump -DS ./handler | grep -6  "400b90"

objdump 将异常 dump 出来

参考代码:

a user-space simulated dump_stack(), based on mips.

kernel perf source dump_stack

#3 更低层次的函数

只有使用 glibc 2.1 或更新版本, 可以使用 backtrace() 函数, 参看 <execinfo.h>, 并且不同架构和系统中可能有不同的支持.

因此 GCC 提供了两个内置函数用来在运行时取得函数调用栈中的返回地址和框架地址

void *__builtin_return_address(int level);

得到当前函数层次为 level 的返回地址, 即此函数被别的函数调用, 然后此函数执行完毕后, 返回, 所谓返回地址就是调用的时候的地址(其实是调用位置的下一条指令的地址).

void* __builtin_frame_address (unsigned int level);

得到当前函数的栈帧的地址.

#include <memory.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <ucontext.h>
#include <dlfcn.h>
#include <execinfo.h>

void showBacktrace()
{
        void * ret = __builtin_return_address(1);
        printf("ret address [%p]\n", ret);
        void * caller = __builtin_frame_address(0);
        printf("call address [%p]\n", caller);
#ifdef __cplusplus
        Dl_info dlinfo;

        void *ip = ret;
        if(!dladdr(ip, &dlinfo)) {
                perror("addr not found\n");
                return;
        }

        const char *symname = dlinfo.dli_sname;
        int f = 0;
        fprintf(stderr, "% 2d: %p %s+%u (%s)\n",
                        ++f,
                        ip,
                        symname, 0,
// (unsigned)(ip - dlinfo.dli_saddr),

                        dlinfo.dli_fname);
#endif
}

int MyFunc_A()
{
        showBacktrace();
        return 10;
}

int MyFunc_B()
{
        return MyFunc_A();
}

int main()
{
        MyFunc_B();
        return 0;
}

运行结果

#4 参考资料

Stack backtrace 的实现

backtrace.c:Code Content

一个glibc中abort不能backtrace的问题

在Linux中如何利用backtrace信息解决问题

内核中dump_stack()的实现,并在用户态模拟dump_stack()


  • 本作品/博文 ( AderStep-紫夜阑珊-青伶巷草 Copyright ©2013-2017 ), 由 成坚(gatieme) 创作.

  • 采用知识共享许可协议知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可. 欢迎转载、使用、重新发布, 但务必保留文章署名成坚gatieme ( 包含链接: http://blog.csdn.net/gatieme ), 不得用于商业目的.

  • 基于本文修改后的作品务必以相同的许可发布. 如有任何疑问,请与我联系.

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

用户态使用 glibc/backtrace 追踪函数调用堆栈定位段错误 的相关文章

  • 20210702剑指Offer03(数组中重复数字)

    找出数组中重复的数字 输入 xff1a 2 3 1 0 2 5 3 输出 xff1a 2 或 3 span class token keyword class span span class token class name Solutio
  • react异步数据如ajax请求应该放在哪个生命周期?

    对于同步的状态改变 xff0c 是可以放在componentWillMount xff0c 对于异步的 xff0c 最好好放在componentDidMount 但如果此时有若干细节需要处理 xff0c 比如你的组件需要渲染子组件 xff0
  • RabbitMQ exchange交换机机制

    目录 RabbitMQ 概念exchange交换机机制 什么是交换机binding xff1f Direct Exchange交换机Topic Exchange交换机Fanout Exchange交换机Header Exchange交换机R
  • 解决open-vm-tools无法复制粘贴文件问题

    在使用vmware kali linux时一直忍受着一个情况 xff1a open vm tools Error when getting information for file 34 tmp VMwareDnD 3jTONh xxx N
  • mipmap 和 drawable 的区别

    Android 在 API level 17 加入了 mipmap 技术 xff0c 对 bitmap 图片的渲染支持 mipmap 技术 xff0c 来提高渲染的速度和质量 mipmap 是一种很早就有的技术了 xff0c 翻译过来就是纹
  • LSTM与GRU

    LSTM 与 GRU 一 综述 LSTM 与 GRU是RNN的变种 xff0c 由于RNN存在梯度消失或梯度爆炸的问题 xff0c 所以RNN很难将信息从较早的时间步传送到后面的时间步 LSTM和GRU引入门 xff08 gate xff0
  • Pytorch 实战RNN

    一 简单实例 span class token comment coding utf8 span span class token keyword import span torch span class token keyword as
  • Pytorch : Dataset和DataLoader

    一 综述 Dataset 对数据进行抽象 xff0c 将数据包装为Dataset类 DataLoader 在 Dataset之上对数据进行进一步处理 xff0c 包括进行乱序处理 xff0c 获取一个batch size的数据等 二 Dat
  • 特征工程

    一 数据读取 1 1 读取CSV文件 1 1 1 原文件内容 1 1 2 读取csv span class token keyword import span csv csv file span class token operator 6
  • 代码命名规范

    代码命名规范 现在是2016年12月30日中午12点35分 xff0c 这是我第一次写博客 xff0c 用的是markdown编辑器 xff0c 还不太会用 今天就先简单的写一下 xff0c 看看写出来的效果是什么样的 xff01 xff0
  • Ubuntu18.04 离线安装nginx

    由于服务器位于内网环境且无法访问互联网 xff0c 需要离线安装nginx xff0c ubuntu18 04离线安装软件也并不复杂 xff0c 只是需要较大的耐心去搜集所需的包 xff0c 不过大家不用担心 xff0c 我已经为大家准备好
  • easyui combobox动态绑定数据

    1 jsp上的写法 lt input span class hljs keyword class span 61 span class hljs string 34 easyui combobox 34 span id 61 span cl
  • Echarts(二、柱状图(各参数详细描述))

    1 jsp页面 span class hljs tag lt span class hljs title body span gt span span class hljs tag lt span class hljs title div
  • js中级脚本算法

    1区间求值算法挑战 span class hljs function span class hljs keyword function span span class hljs title sumAll span span class hl
  • 常用easyUI -icon 图标

    1 样式 代码 xff1a lt DOCTYPE html gt lt html lang 61 34 en 34 gt lt head gt lt meta charset 61 34 UTF 8 34 gt lt title gt Ea
  • vue与后台交互数据(vue-resource)

    需要引入库 xff1a vue resource lt script src 61 34 https cdn jsdelivr net vue resource 1 0 3 vue resource min js 34 gt lt scri
  • Tensorflow——jupyter notebook调用某个库时,出现找不到这个库情况的解决方案

    1 激活tensorflow环境 终端下输入 xff1a source activate tensorflow 2 进入jupyter notebook 出现如下问题 xff1a 没有找到matplotlib库 3 解决方法 在tensor
  • 组合排序题目汇总(排列组合、卡特兰数和递归思想)

    组合排序题目汇总 排列组合矩阵走法A必须在B左边站队互不相邻站队分糖果球放入桶吃糖 卡特兰数括号匹配进出栈顺序 售票顺序二叉树不同的结构数高矮排列 递归思想信封装信 排列组合 矩阵走法 在6 9的方格中 xff0c 以左上角为起点 xff0
  • java.lang.IllegalStateException 错误原因及解决方法小记

    java lang IllegalStateException Cannot perform this operation because the connection pool has been closed at android dat
  • 架构设计三原则

    1 简单原则 xff1b 分析完业务场景的结构复杂性和逻辑复杂性后 xff0c 从业务场景和解决问题的角度出发 xff0c 选择和设计能够满足需求的方案 在简单架构和复杂架构都能满足要求的时候 xff0c 优先选择简单架构 xff0c 因为

随机推荐