c语言 backtrace

c语言 backtracec 语言 backtrace 的详细理解

c语言 backtrace

1:函数简介
通常,应用程序使用外部调试程序(比如gdb)进行调试,但有时出于统计、分析等目的,可在程序自身内使用函数backtrace记录应用程序的堆栈轨迹。
应用程序通常使用专用调试器来调试程序,但在有些情况下,调试器可能无法使用。而且,无论怎样,当程序出现问题时,向开发人员提供尽可能多的现场信息终究是有利的




因此,C库中提供了backtrace系列函数,利用这些函数可以记录在错误出现之前应用程序的运行轨迹,帮助开发人员更快地定位问题。
注:backtrace(回溯)指的是线程中当前活跃的函数调用。

int backtrace(void buffer, int size);

当函数成功返回时,返回值是buffer中实际的地址数量,这个值小于等于size;如果返回值=size,那么buffer中的返回地址可能不是全部。

2.函数backtrace_symbols的声明位于头文件execinfo.h,函数原型是

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

函数backtrace_symbols将给定buffer中的返回地址转换为符号化的地址字符串。参数size指定buffer中的地址数量。每个地址的符号表示包括:函数名称(如果能够解析)、函数内的偏移量(十六进制)以及实际的返回地址(十六进制)。该函数的结果是字符串指针数组的地址,并且由backtrace_symbols用malloc分配存储空间,使用者负责用free释放内存(数组成员无需也不能用free释放)

注:只有使用ELF格式的二进制程序和库才能获得函数名称,并且可能还需要向连接器传输特殊选项(比如GNU ld,要传递-rdynamic)

当函数成功完成时,返回值是字符串数组的地址;否则,返回NULL

3.函数backtrace_symbols_fd原型是:

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

与前面不同的是,其不会返回字符串数组,而是将字符串写入fd所代表的文件,一行一个,还有一个不同的是,它不使用malloc,因此在malloc可能会失败的某些场合,可以使用这个函数来代替。

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

2 :捕获系统异常信号输出调用栈
当程序出现异常时通常伴随着会收到一个由内核发过来的异常信号,如当对内存出现非法访问时将收到段错误信号SIGSEGV,然后才退出。利用这一点,当我们在收到异常信号后将程序的调用栈进行输出,它通常是利用signal()函数,关于系统信号的总结可以参考另外一篇文章:
https://zhouyuming.blog.csdn.net/article/details/




3:从backtrace信息分析定位问题

/* * add.c */ #include  
      #include  
      #include  
      int add1(int num) { 
    int ret = 0x00; int *pTemp = NULL; *pTemp = 0x01; /* 这将导致一个段错误,致使程序崩溃退出 */ ret = num + *pTemp; return ret; } int add(int num) { 
    int ret = 0x00; ret = add1(num); return ret; } 
/* * dump.c */ #include  
      #include  
      #include  
      #include  
     /* for signal */  #include  
     /* for backtrace() */  #define BACKTRACE_SIZE 16  void dump(void) { 
    int j, nptrs; void *buffer[BACKTRACE_SIZE]; char **strings; nptrs = backtrace(buffer, BACKTRACE_SIZE); printf("backtrace() returned %d addresses\n", nptrs); strings = backtrace_symbols(buffer, nptrs); if (strings == NULL) { 
    perror("backtrace_symbols"); exit(EXIT_FAILURE); } for (j = 0; j < nptrs; j++) printf(" [%02d] %s\n", j, strings[j]); free(strings); } void signal_handler(int signo) { 
    #if 0  char buff[64] = { 
   0x00}; sprintf(buff,"cat /proc/%d/maps", getpid()); system((const char*) buff); #endif  printf("\n=========>>>catch signal %d <<<=========\n", signo); printf("Dump stack start...\n"); dump(); printf("Dump stack end...\n"); signal(signo, SIG_DFL); /* 恢复信号默认处理 */ raise(signo); /* 重新发送信号 */ } 
/* * backtrace.c */ #include  
      #include  
      #include  
      #include  
     /* for signal */  #include  
     /* for backtrace() */  extern void dump(void); extern void signal_handler(int signo); extern int add(int num); int main(int argc, char *argv[]) { 
    int sum = 0x00; signal(SIGSEGV, signal_handler); /* 为SIGSEGV信号安装新的处理函数 */ sum = add(sum); printf(" sum = %d \n", sum); return 0x00; } 

3.2 静态链接情况下的错误信息分析定位

我们首先将用最基本的编译方式将他们编译成一个可执行文件并执行,如下:

gcc -g -rdynamic backtrace.c add.c dump.c -o backtrace ./backtrace =========>>>catch signal 11 <<<========= Dump stack start... backtrace() returned 8 addresses [00] ./backtrace(dump+0x1f) [0x400a9b] [01] ./backtrace(signal_handler+0x31) [0x400b63] [02] /lib/x86_64-linux-gnu/libc.so.6(+0x36150) [0x7f86afc7e150] [03] ./backtrace(add1+0x1a) [0x400a3e] [04] ./backtrace(add+0x1c) [0x400a71] [05] ./backtrace(main+0x2f) [0x400a03] [06] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed) [0x7f86afc6976d] [07] ./backtrace() [0x] Dump stack end... 段错误 (核心已转储) 

由此可见在调用完函数add1后就开始调用段错误信号处理函数了,所以问题是出在函数add1中。这似乎还不够,更准确的位置应该是在地址0x400a3e处,但这到底是哪一行呢,我们使用addr2line命令来得到,执行如下:

addr2line -e backtrace 0x400a3e /home/share/work/backtrace/add.c:13 

3.3 动态链接情况下的错误信息分析定位

然而我们通常调试的程序往往没有这么简单,通常会加载用到各种各样的动态链接库。如果错误是发生在动态链接库中那么处理将变得困难一些。下面我们将上述程序中的add.c编译成动态链接库libadd.so,然后再编译执行backtrace看会得到什么结果呢。

/* 编译生成libadd.so */ gcc -g -rdynamic add.c -fPIC -shared -o libadd.so /* 编译生成backtrace可执行文件 */ gcc -g -rdynamic backtrace.c dump.c -L. -ladd -Wl,-rpath=. -o backtrace 

其中参数 -L. -ladd为编译时链接当前目录的libadd.so;参数-Wl,-rpath=.为指定程序执行时动态链接库搜索路径为当前目录,否则会出现执行找不到libadd.so的错误。然后执行backtrace程序结果如下:

./backtrace =========>>>catch signal 11 <<<========= Dump stack start... backtrace() returned 8 addresses [00] ./backtrace(dump+0x1f) [0x400a53] [01] ./backtrace(signal_handler+0x31) [0x400b1b] [02] /lib/x86_64-linux-gnu/libc.so.6(+0x36150) [0x7f] [03] ./libadd.so(add1+0x1a) [0x7f85839fa5c6] [04] ./libadd.so(add+0x1c) [0x7f85839fa5f9] [05] ./backtrace(main+0x2f) [0x400a13] [06] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed) [0x7fd76d] [07] ./backtrace() [0x] Dump stack end... 段错误 (核心已转储) 

此时我们再用前面的方法将得不到任何信息,如下:

addr2line -e libadd.so 0x7f85839fa5c6 ??:0 
char buff[64] = { 
   0x00}; sprintf(buff,"cat /proc/%d/maps", getpid()); system((const char*) buff); 

然后编译执行得到如下结果(打印比较多这里摘取关键部分):

.................................................... 7f0962fb3000-7f0962fb4000 r-xp 00000000 08:01  /home/share/work/backtrace/libadd.so 7f0962fb4000-7f09631b3000 ---p 00001000 08:01  /home/share/work/backtrace/libadd.so 7f09631b3000-7f09631b4000 r--p 00000000 08:01  /home/share/work/backtrace/libadd.so 7f09631b4000-7f09631b5000 rw-p 00001000 08:01  /home/share/work/backtrace/libadd.so ..................................................... =========>>>catch signal 11 <<<========= Dump stack start... backtrace() returned 8 addresses [00] ./backtrace(dump+0x1f) [0x400b7f] [01] ./backtrace(signal_handler+0x83) [0x400c99] [02] /lib/x86_64-linux-gnu/libc.so.6(+0x36150) [0x7f0962c2b150] [03] ./libadd.so(add1+0x1a) [0x7f0962fb35c6] [04] ./libadd.so(add+0x1c) [0x7f0962fb35f9] [05] ./backtrace(main+0x2f) [0x400b53] [06] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed) [0x7f0962c1676d] [07] ./backtrace() [0x400a69] Dump stack end... 段错误 (核心已转储) 

Maps信息第一项表示的为地址范围如第一条记录中的7f0962fb3000-7f0962fb4000,第二项r-xp分别表示只读、可执行、私有的,由此可知这里存放的为libadd.so的.text段即代码段,后面的栈信息0x7f0962fb35c6也正好是落在了这个区间。所有我们正确的地址应为0x7f0962fb35c6 – 7f0962fb3000 = 0x5c6,将这个地址利用addr2line命令得到如下结果:

addr2line -e libadd.so 0x5c6 /home/share/work/backtrace/add.c:13 

可见也得到了正确的出错行号。

gcc -g -rdynamic add.c -fPIC -shared -o libadd.so -Wl,-Map,add.map 

Map文件中将包含关于libadd.so的丰富信息,我们搜索函数名add1就可以找到其在.text段的地址如下:

................................... .text 0x00000000000005ac 0x55 /tmp/ccCP0hNf.o 0x00000000000005ac add1 0x00000000000005dd add ................................... 

由此可知我们的add1的地址为0x5ac,然后加上偏移地址0x1a即0x5ac + 0x1a = 0x5c6,由前面可知这个地址是正确的。

#/bin/bash # assembler2c.sh #帮助函数 function help { 
    echo "" echo $1 echo "" echo "Usage: $0 libXXX.dbg FunctionName Offset" echo "" echo " libXXX.dbg -- The dbg file of the exception dymanic library." echo " FunctionName -- Exception function name in callstack." echo " Offset -- Exception instruction offset in exception function. It must be start with 0x." echo "" } #检查参数 if [ $# != 3 ]; then help "Error : Argu must be 3!" exit 1 fi #检查文件是否存在 if [ ! -f $1 ]; then help "File $1 not Found!" exit 1 fi #检查文件格式是否是ELF格式 type=`file $1 | grep ELF | wc -l` if [ $type == 0 ]; then help "$1 :file type is not ELF!" exit 1 fi #检查函数是否包含在dbg文件中 containflag=`objdump -t $1 | grep " $2$" | wc -l` if [ $containflag == 0 ]; then help "$1 :Function not in this dbg file!" exit 1 fi #获取函数基地址 baseaddr=`objdump -t $1 | grep " $2$" | grep .text | awk { 
    'print $1'}` #获取指令最终地址 destaddr=`printf "%x" $[0x$baseaddr+$3]` if [ $? != 0 ]; then help "please check argu!" exit 1 fi #打印输出 addr2line -e $1 0x$destaddr echo "" 

3、file *.gdb

4、l *(func_name+0xxxx) 0xxxx为接口func_name的偏移地址

objdump相关符号表查找:

objdump -t xxx.dbg | grep xxx

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/226228.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月17日 上午7:33
下一篇 2026年3月17日 上午7:34


相关推荐

  • ArcGIS API for JavaScript-弹出窗口简介

    ArcGIS API for JavaScript-弹出窗口简介弹出窗口通过显示信息以响应用户操作 提供了一种简便的方法来将交互性添加到 ArcGISAPIfor 应用程序 每个 view 都有一个与之关联的 popup 在大多数情况下 弹出窗口的内容允许用户从图层和图形访问数据属性 虽然弹出窗口通常与图形层或要素层一起使用 但是您也可以显示弹出窗口以响应查询或不涉及图形或要素的某些其他操作 例如 您可以在视图中显示用户单击位置的纬度 经度坐标 本示例将通过设置默认属性 例如 content title 和 location 并显示它而无需从 PopupTe

    2026年3月20日
    1
  • Windows 部署安装 OpenClaw 教程:零基础小白也能搞定的完整指南

    Windows 部署安装 OpenClaw 教程:零基础小白也能搞定的完整指南

    2026年3月13日
    2
  • socket通讯原理及例程(一看就懂)

    socket通讯原理及例程(一看就懂)对TCP/IP、UDP、Socket编程这些词你不会很陌生吧?随着网络技术的发展,这些词充斥着我们的耳朵。那么我想问:什么是TCP/IP、UDP?Socket在哪里呢?Socket是什么呢?你会使用它们吗?什么是TCP/IP、UDP?TCP/IP(TransmissionControlProtocol/InternetProtocol)即传输控制协议/网间协议…

    2022年7月14日
    16
  • 12款Py程序员必备PyCharm插件,亲测过~推荐

    12款Py程序员必备PyCharm插件,亲测过~推荐最近使用Python,烧脑的我,使用编程软件肯定少不了去安装一些非常好用的插件,目的为了代码高效和方便的开发。以下是我亲测过的一些插件,很实用!!!!下载位置在输入框种输入要安装的插件名称即可下载。插件合集1..ignore我们做的每个Git项目中都需要一个“.gitignore”文件,这个文件的作用就是告诉Git哪些文件不需要添加到版本管理中。2.CSVPlugin(必备推荐)它可以让CSV各个列之间区别明显,很清晰的显示各种颜色的高亮.3.CodeGlan..

    2022年6月24日
    33
  • linux pycharm激活码[免费获取][通俗易懂]

    (linux pycharm激活码)本文适用于JetBrains家族所有ide,包括IntelliJidea,phpstorm,webstorm,pycharm,datagrip等。IntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,下面是详细链接哦~https://javaforall.net/100143.html…

    2022年3月21日
    245
  • jQuery 文档操作 – remove() 方法

    jQuery 文档操作 – remove() 方法

    2021年10月17日
    74

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号