转自:https://blog.csdn.net/gatieme/article/details/84189280

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

CSDN GitHub Hexo
用户态使用 glibc/backtrace 追踪函数调用堆栈定位段错误 AderXCoding/language/c/backtrace KernelShow(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)
1
该函数用于获取当前线程的调用堆栈,

参数:
获取的信息将会被存放在 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_fd 与 backtrace_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]

##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]

##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

对应错误的行号.

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

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

参考代码:

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 ), 不得用于商业目的.

基于本文修改后的作品务必以相同的许可发布. 如有任何疑问,请与我联系.
---------------------
作者:JeanCheng
来源:CSDN
原文:https://blog.csdn.net/gatieme/article/details/84189280
版权声明:本文为博主原创文章,转载请附上博文链接!

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

  1. 嵌入式 linux下利用backtrace追踪函数调用堆栈以及定位段错误

    嵌入式 linux下利用backtrace追踪函数调用堆栈以及定位段错误 2015-05-27 14:19 184人阅读 评论(0) 收藏 举报  分类: 嵌入式(928)  一般察看函数运行时堆栈的 ...

  2. linux下利用backtrace追踪函数调用堆栈以及定位段错误

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

  3. Linux下利用backtrace追踪函数调用堆栈以及定位段错误[转]

    来源:Linux社区  作者:astrotycoon 一般察看函数运行时堆栈的方法是使用GDB(bt命令)之类的外部调试器,但是,有些时候为了分析程序的BUG,(主要针对长时间运行程序的分析),在程序 ...

  4. Linux下利用backtrace追踪函数调用堆栈以及定位段错误【转】

    转自:https://www.linuxidc.com/Linux/2012-11/73470p2.htm 通常情况系,程序发生段错误时系统会发送SIGSEGV信号给程序,缺省处理是退出函数.我们可以 ...

  5. 【转】Android下面打印进程函数调用堆栈(dump backtrace)的方法

    1. 为什么要打印函数调用堆栈? 打印调用堆栈可以直接把问题发生时的函数调用关系打出来,非常有利于理解函数调用关系.比如函数A可能被B/C/D调用,如果只看代码,B/C/D谁调用A都有可能,如果打印出 ...

  6. linux 用户态 内核态

    http://blog.chinaunix.net/uid-1829236-id-3182279.html 究竟什么是用户态,什么是内核态,这两个基本概念以前一直理解得不是很清楚,根本原因个人觉得是在 ...

  7. 41.Linux应用调试-修改内核来打印用户态的oops

    1.在之前第36章里,我们学习了通过驱动的oops定位错误代码行 第36章的oops代码如下所示: Unable to handle kernel paging request at //无法处理内核 ...

  8. 内核中dump_stack()的实现,并在用户态模拟dump_stack()【转】

    转自:https://blog.csdn.net/jasonchen_gbd/article/details/44066815?utm_source=blogxgwz8 版权声明:本文为博主原创文章, ...

  9. linux用户态和内核态通信之netlink机制【转】

    本文转载自:http://blog.csdn.net/zcabcd123/article/details/8272360 这是一篇学习笔记,主要是对<Linux 系统内核空间与用户空间通信的实现 ...

随机推荐

  1. 基于python调用libvirt API

    基于python调用libvirt API 1.程序代码 #!/usr/bin/python import libvirt import sys def createConnection(): con ...

  2. flask 安装及基础学习(url_for反转,静态文件引入)

    pip3 install flask pycharm 创建项目 默认的代码解释说明(及开启debug模式) #encoding:utf-8 from flask import Flask #从flas ...

  3. springboot下整合各种配置文件

    本博是在springboot下整合其他中间件,比如,mq,redis,durid,日志...等等  以后遇到再更.springboot真是太便捷了,让我们赶紧涌入到springboot的怀抱吧. ap ...

  4. python脚本难点

    本文主要记录在录制过程中,遇到一些需要特殊处理的脚本.如果有总结不好的地方,希望多多指点. 一.输入框listview选择: 如图:   类似这种情况,选择其中一项的脚本如下: m = driver. ...

  5. 【不懂】spring bean生命周期

    完整的生命周期(牢记): 1.spring容器准备 2.实例化bean 3.注入依赖关系 4.初始化bean 5.使用bean 6.销毁bean Bean的完整生命週期可以認為是從容器建立初始化Bea ...

  6. 01-Unity深入浅出(一)

    一. 温故而知新 在开始学习Unity框架之前,有必要温习一下 [依赖倒置原则]和[手写IOC], 因为我们框架代码的构建都是基于[依赖倒置原则]的,而Unity框架的核心思想就是IOC和DI,所以有 ...

  7. ava.io.InputStream & java.io.FileInputStream

    java.io.InputStream & java.io.FileInputStream java.io.InputStream,这个抽象类是表示字节输入流的超类,这个抽象类的共性的方法有: ...

  8. 在java1.8下使用jetty报错java.lang.CharSequence cannot be resolved

    环境: JDK: 1.8Jetty: jetty6,jetty7(在eclipse中使用run-jetty-run插件) 在JSP页面中使用StringBuilder或者StringBuffer,示例 ...

  9. 百度编辑器ueditor 光标位置的坐标

    项目需求: 输入某个字符时,弹出一个弹框 弹框位置跟随光标处 经查找和亲测,下面记录一下代码: // 下面计算坐标 let domUtils = UE.dom.domUtils let bk_star ...

  10. Android SVN上传项目

    方式一: 1 工具栏 VCS ——import into Version Control - Share Project (SubVersion)(注意不要用import into SubVersio ...