程序中打印当前进程的调用堆栈(backtrace)

2024-03-10 23:58

本文主要是介绍程序中打印当前进程的调用堆栈(backtrace),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

为了方便调式程序,产品中需要在程序崩溃或遇到问题时打印出当前的调用堆栈。由于是基于Linux的ARM嵌入式系统,没有足够的空间来存放coredump文件。

实现方法,首先用__builtin_frame_address()函数获取堆栈的当前帧的地址(faddr), ×faddr(栈帧的第一个单元存放的数据)即当前函数的返回地址,及调用函数中的指令地址。×(faddr-1)是调用函数的栈帧的地址,即栈帧中保存了调用函数的栈帧的地址。由此可知,同一线程的所有栈帧组成了一个链表。遍历此链表,就可以打印出所有的调用堆栈。

栈帧中存放的是地址和局部变量的值。为了方便调试,还需要把地址转换为对应的函数名和对应的源文件。这要利用到符号表。程序中的全局函数和动态库中导出(exported)的函数,程序加载时会把对应的符号表(.dynsym)加载到内存中,这时利用库函数dladdr()即可方便的获取对应函数名及其所在的库文件。但是dladdr()只能获取exported的全局函数名,如果函数被隐藏(hidden)或者是static的,则dladdr无能为力。为了显示这些static 的函数,我们经历了一个漫长的求索过程。然而仍没有完美的解决。故在此备忘一下。

1. 首先是非常蒙蔽,不知道为啥有的能显示,有的不能。于是一阵狂搜,度娘使尽了吃奶的力气,没有给出任何有用的结果。没有办法,只有再啃啃代码,甚至想去读dladdr的源码了。好吧,有点过了,停下来喝杯咖啡,望望窗外一望无际的大海,心胸顿时开阔,平静许多。既然有的能显示,有的不能显示,说明没有方向错误。那么为啥有的不能呢?很显然是dladdr欺骗了我们,他没有给出我们想要的东西。嗯,这看上去是句废话。然,只有确定了这一点,才有底气去找dladdr的麻烦。看了n遍dladdr的man,都没有发现原因。度娘也没用。谷哥吧,幸好有国外代理。还是谷哥强壮,一出手就找到一个非常类似的问题,而且有高手明确指出dladdr只能获取exported的函数。dladdr doesnt return the function name

https://stackoverflow.com/questions/11731229/dladdr-doesnt-return-the-function-name

2. 终于找到了病因,如何治疗呢?或者是个绝症,无药可救?大多数患了癌症的人都会垂死挣扎的,比如动手术,化疗,放疗,开始锻炼,打太极等等。我们也不例外,找了很多偏方,一一试过,都无效。比如,-Wl,--export-dynamic, -rdynamic, -fvisibility, --version_script等等。唉,这些一知半解的江湖郎中真是误人不浅啊。更搞笑的是有的还信誓旦旦"验证过了“。所谓久病成医,自己一边试错,一边补习基本功。了解了elf文件的格式和动态库的连接加载过程后,基本清楚是怎么回事儿了。static 或隐藏了的函数,连接阶段会把其设置为LOCAL属性,是不会被加载到内存中的。运行时用dladdr去查,自然找不到。如果去掉static, 其属性会变为GLOBAL, 会被加载,dladdr自然也能查到了。于是找到堆栈上的某个static函数,去掉static,编译一试,果然如此。

3. 这看似又前进了一步,其实不然,仍然停留在原因分析阶段,只不过是跟深入了些罢了。老板根本不关心这些深层次的原因是什么,只关心问题到底解决了没有。当然这也是有理论根据的,所谓的”结果导向“嘛。OK,既然内存中没有,那文件中有没有呢?答案是肯定的,也是不肯定的。编译时如果带了-g选项,即增加了调试信息,那么生成的elf文件中包含所有的符号信息。当然,这时文件很大。为了给elf文件瘦身,实际发布版本时,一般都会用strip删除多余的信息。这样做的好处是,既可以保留调试信息方便调试,又可以减少elf文件的大小,减少空间占用。所谓的”鱼和熊掌可以得兼”吧。需要的信息就在这里(对应的.so文件中),如何把它拿出来呢?

4. 这涉及DWAF信息格式,二进制文件操作的相关库,比如:libbfd,libelf和libdwarf等等。由于我们对这些库一窍不通,如果要重头去研究学习的话,感觉是一个漫长的过程。这个问题已经delay很久了,既然让老板看到了曙光,他就迫切想很快看到黎明。再者,搞软件开发不是切忌“重新发明轮子”的么?得想办法利用已有的工具或库函数。前期探索过程中,通过计算偏移量并利用addr2line工具成功 地解析出了程序中地址对应的符号。一个可能的解决方案自然而然地浮现在眼前。首先找到库文件在进程中的映射地址(lib_start)和存放在栈中的返回地址(faddr_n),(faddr_n - lib_start)就是该指令在动态库文件中的地址,然后用addr2line就可以解析出函数名和所在的文件及行号。fadd_n已经有了,关键就是获取lib_start。

5. 我们又读了一遍dladdr的man,发现其返回结构体(Dl_info)中有一个dli_fbase成员,应该就是动态库的加载地址。Dl_info的完成注释如下:

typedef struct {const char *dli_fname;  /* Pathname of shared object that contains address */void       *dli_fbase;  /* Base address at which shared object is loaded */const char *dli_sname;  /* Name of symbol whose definition overlaps addr */void       *dli_saddr;  /* Exact address of symbol named in dli_sname */} Dl_info;
于是按照这个思路开始了新一轮的尝试。结果让我们大跌眼镜。所有调用返回的dli_fbase都是一样的!我们又陷入了沮丧和沉思之中。显然这文档和我们的环境是对应不上的。仔细研究了下返回的结果,发现它其实同一个库(libc.so)的基地址。libc.so也是我们堆栈的第一个库。也不能说完全没用,利用它至少可以解出第一个符号__clone()。但是,没有完全解决我们的问题。
儿子经常考我一个问题:”世界上哪个城市的交通最发达?"答案是罗马,因为条条道路通罗马。既然dladdr这条路走不通,我们换一条与之最接近的路——dladdr1()试试?因为dladdr1可以返回更多的信息,其中最感兴趣的是struct link_map *, 因为拿到它就可以遍历出进程中加载的全部动态库。

6. 说干就干,第一次就打出了很多库,包括很多堆栈上没有的库。奇怪的是,有一个库居然没有。这离成功已经很近了,稍微思索了一下,就找到了问题的关键,link_map是一个双向链表,我们拿到指针后没有先移动到表头,而是直接重获取链表的位置(就是libc.so的位置)开始一直打印到表尾的,有的库可能还在libc.so之前。于是稍作修改,先找到表头,然后开始遍历,就打印出了所有的库。到达最终的黎明之前还有一块小小的绊脚石。按之前的理解,link_map ×就应该是我们要找的基地址。然而实验表明这是不对的。幸好我们多留了个心眼,打出了link_map中的l_addr,用这个值一试,解出了全部符号。即,link_map中的l_addr才是.so文件在内存中的基地址。这里同样列出link_map的注释,因为l_addr的注释同样是让我们没看懂的。

struct link_map {ElfW(Addr) l_addr;  /* Difference between the  address in the ELF file and  the address in memory */char      *l_name;  /* Absolute pathname where object was found */ElfW(Dyn) *l_ld;    /* Dynamic section of the shared object */struct link_map *l_next, *l_prev;   /* Chain of loaded objects *//* Plus additional fields private to the  implementation */};

到此终于算完成了整个打印backtrace的漫长之旅。不过并不完美,因为不能在发布版本中直接打出堆栈,还得拿回trace用对应的没有strip的文件重新解析一次。如果发布的版本多了,就不能保证找到对应的文件。

有一个疑问,是否存在某个编译选项,可以把static的函数exported为global的?这样的话dladdr就可以直接拿到所有的符号了。

另外:

在遍历所有的动态库时,我们还尝试了dl_iterate_phdr(),根据man,它应该也可以打印出所有库的。然而,试验结果是它只会打出程序本身,相当于啥也没打出。附dl_iterate_phdr()的文档:

int dl_iterate_phdr(int (*callback) (struct dl_phdr_info *info,size_t size, void *data),void *data);

Description

The dl_iterate_phdr() function allows an application to inquire at run time to find out which shared objects it has loaded.

The dl_iterate_phdr() function walks through the list of an application's shared objects and calls the function callback once for each object, until either all shared objects have been processed or callbackreturns a nonzero value.

Each call to callback receives three arguments: info, which is a pointer to a structure containing information about the shared object; size, which is the size of the structure pointed to by info; and data, which is a copy of whatever value was passed by the calling program as the second argument (also named data) in the call to dl_iterate_phdr().

The info argument is a structure of the following type:

struct dl_phdr_info {ElfW(Addr)        dlpi_addr;  /* Base address of object */const char       *dlpi_name;  /* (Null-terminated) name ofobject */const ElfW(Phdr) *dlpi_phdr;  /* Pointer to array ofELF program headersfor this object */ElfW(Half)        dlpi_phnum; /* # of items in dlpi_phdr */
};
(The  ElfW() macro definition turns its argument into the name of an ELF data type suitable for the hardware architecture. For example, on a 32-bit platform, ElfW(Addr) yields the data type name Elf32_Addr. Further information on these types can be found in the  <elf.h> and  <link.h> header files.)

The dlpi_addr field indicates the base address of the shared object (i.e., the difference between the virtual memory address of the shared object and the offset of that object in the file from which it was loaded). The dlpi_name field is a null-terminated string giving the pathname from which the shared object was loaded.

To understand the meaning of the dlpi_phdr and dlpi_phnum fields, we need to be aware that an ELF shared object consists of a number of segments, each of which has a corresponding program header describing the segment. The dlpi_phdr field is a pointer to an array of the program headers for this shared object. The dlpi_phnum field indicates the size of this array.

These program headers are structures of the following form:

typedef struct {Elf32_Word  p_type;    /* Segment type */Elf32_Off   p_offset;  /* Segment file offset */Elf32_Addr  p_vaddr;   /* Segment virtual address */Elf32_Addr  p_paddr;   /* Segment physical address */Elf32_Word  p_filesz;  /* Segment size in file */Elf32_Word  p_memsz;   /* Segment size in memory */Elf32_Word  p_flags;   /* Segment flags */Elf32_Word  p_align;   /* Segment alignment */
} Elf32_Phdr;
Note that we can calculate the location of a particular program header,  x, in virtual memory using the formula:
addr == info->dlpi_addr + info->dlpi_phdr[x].p_vaddr;

Return Value

The dl_iterate_phdr() function returns whatever value was returned by the last call to callback.

Versions

dl_iterate_phdr() has been supported in glibc since version 2.2.4.

转载于:https://www.cnblogs.com/clblacksmith/p/8378434.html

这篇关于程序中打印当前进程的调用堆栈(backtrace)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/795943

相关文章

SpringBoot服务获取Pod当前IP的两种方案

《SpringBoot服务获取Pod当前IP的两种方案》在Kubernetes集群中,SpringBoot服务获取Pod当前IP的方案主要有两种,通过环境变量注入或通过Java代码动态获取网络接口IP... 目录方案一:通过 Kubernetes Downward API 注入环境变量原理步骤方案二:通过

Windows的CMD窗口如何查看并杀死nginx进程

《Windows的CMD窗口如何查看并杀死nginx进程》:本文主要介绍Windows的CMD窗口如何查看并杀死nginx进程问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录Windows的CMD窗口查看并杀死nginx进程开启nginx查看nginx进程停止nginx服务

python编写朋克风格的天气查询程序

《python编写朋克风格的天气查询程序》这篇文章主要为大家详细介绍了一个基于Python的桌面应用程序,使用了tkinter库来创建图形用户界面并通过requests库调用Open-MeteoAPI... 目录工具介绍工具使用说明python脚本内容如何运行脚本工具介绍这个天气查询工具是一个基于 Pyt

Java调用C#动态库的三种方法详解

《Java调用C#动态库的三种方法详解》在这个多语言编程的时代,Java和C#就像两位才华横溢的舞者,各自在不同的舞台上展现着独特的魅力,然而,当它们携手合作时,又会碰撞出怎样绚丽的火花呢?今天,我们... 目录方法1:C++/CLI搭建桥梁——Java ↔ C# 的“翻译官”步骤1:创建C#类库(.NET

Ubuntu设置程序开机自启动的操作步骤

《Ubuntu设置程序开机自启动的操作步骤》在部署程序到边缘端时,我们总希望可以通电即启动我们写好的程序,本篇博客用以记录如何在ubuntu开机执行某条命令或者某个可执行程序,需要的朋友可以参考下... 目录1、概述2、图形界面设置3、设置为Systemd服务1、概述测试环境:Ubuntu22.04 带图

Java进程CPU使用率过高排查步骤详细讲解

《Java进程CPU使用率过高排查步骤详细讲解》:本文主要介绍Java进程CPU使用率过高排查的相关资料,针对Java进程CPU使用率高的问题,我们可以遵循以下步骤进行排查和优化,文中通过代码介绍... 目录前言一、初步定位问题1.1 确认进程状态1.2 确定Java进程ID1.3 快速生成线程堆栈二、分析

Python程序打包exe,单文件和多文件方式

《Python程序打包exe,单文件和多文件方式》:本文主要介绍Python程序打包exe,单文件和多文件方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录python 脚本打成exe文件安装Pyinstaller准备一个ico图标打包方式一(适用于文件较少的程

C/C++和OpenCV实现调用摄像头

《C/C++和OpenCV实现调用摄像头》本文主要介绍了C/C++和OpenCV实现调用摄像头,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录准备工作1. 打开摄像头2. 读取视频帧3. 显示视频帧4. 释放资源5. 获取和设置摄像头属性

Python程序的文件头部声明小结

《Python程序的文件头部声明小结》在Python文件的顶部声明编码通常是必须的,尤其是在处理非ASCII字符时,下面就来介绍一下两种头部文件声明,具有一定的参考价值,感兴趣的可以了解一下... 目录一、# coding=utf-8二、#!/usr/bin/env python三、运行Python程序四、

使用Python实现调用API获取图片存储到本地的方法

《使用Python实现调用API获取图片存储到本地的方法》开发一个自动化工具,用于从JSON数据源中提取图像ID,通过调用指定API获取未经压缩的原始图像文件,并确保下载结果与Postman等工具直接... 目录使用python实现调用API获取图片存储到本地1、项目概述2、核心功能3、环境准备4、代码实现