程序中打印当前进程的调用堆栈(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

相关文章

在C#中调用Python代码的两种实现方式

《在C#中调用Python代码的两种实现方式》:本文主要介绍在C#中调用Python代码的两种实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C#调用python代码的方式1. 使用 Python.NET2. 使用外部进程调用 Python 脚本总结C#调

mysql如何查看当前连接数

《mysql如何查看当前连接数》:本文主要介绍mysql如何查看当前连接数问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录mysql查看当前连接数查看mysql数据库允许最大连接数总结mysql查看当前连接数查看当前连接数SHOW STATUS LIKE

SpringCloud之LoadBalancer负载均衡服务调用过程

《SpringCloud之LoadBalancer负载均衡服务调用过程》:本文主要介绍SpringCloud之LoadBalancer负载均衡服务调用过程,具有很好的参考价值,希望对大家有所帮助,... 目录前言一、LoadBalancer是什么?二、使用步骤1、启动consul2、客户端加入依赖3、以服务

Linux中的进程间通信之匿名管道解读

《Linux中的进程间通信之匿名管道解读》:本文主要介绍Linux中的进程间通信之匿名管道解读,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、基本概念二、管道1、温故知新2、实现方式3、匿名管道(一)管道中的四种情况(二)管道的特性总结一、基本概念我们知道多

Vue 调用摄像头扫描条码功能实现代码

《Vue调用摄像头扫描条码功能实现代码》本文介绍了如何使用Vue.js和jsQR库来实现调用摄像头并扫描条码的功能,通过安装依赖、获取摄像头视频流、解析条码等步骤,实现了从开始扫描到停止扫描的完整流... 目录实现步骤:代码实现1. 安装依赖2. vue 页面代码功能说明注意事项以下是一个基于 Vue.js

Linux进程终止的N种方式详解

《Linux进程终止的N种方式详解》进程终止是操作系统中,进程的一个重要阶段,他标志着进程生命周期的结束,下面小编为大家整理了一些常见的Linux进程终止方式,大家可以根据需求选择... 目录前言一、进程终止的概念二、进程终止的场景三、进程终止的实现3.1 程序退出码3.2 运行完毕结果正常3.3 运行完毕

如何用java对接微信小程序下单后的发货接口

《如何用java对接微信小程序下单后的发货接口》:本文主要介绍在微信小程序后台实现发货通知的步骤,包括获取Access_token、使用RestTemplate调用发货接口、处理AccessTok... 目录配置参数 调用代码获取Access_token调用发货的接口类注意点总结配置参数 首先需要获取Ac

讯飞webapi语音识别接口调用示例代码(python)

《讯飞webapi语音识别接口调用示例代码(python)》:本文主要介绍如何使用Python3调用讯飞WebAPI语音识别接口,重点解决了在处理语音识别结果时判断是否为最后一帧的问题,通过运行代... 目录前言一、环境二、引入库三、代码实例四、运行结果五、总结前言基于python3 讯飞webAPI语音

Windows命令之tasklist命令用法详解(Windows查看进程)

《Windows命令之tasklist命令用法详解(Windows查看进程)》tasklist命令显示本地计算机或远程计算机上当前正在运行的进程列表,命令结合筛选器一起使用,可以按照我们的需求进行过滤... 目录命令帮助1、基本使用2、执行原理2.1、tasklist命令无法使用3、筛选器3.1、根据PID

Android如何获取当前CPU频率和占用率

《Android如何获取当前CPU频率和占用率》最近在优化App的性能,需要获取当前CPU视频频率和占用率,所以本文小编就来和大家总结一下如何在Android中获取当前CPU频率和占用率吧... 最近在优化 App 的性能,需要获取当前 CPU视频频率和占用率,通过查询资料,大致思路如下:目前没有标准的