程序中打印当前进程的调用堆栈(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 of
object */
const ElfW(Phdr) *dlpi_phdr; /* Pointer to array of
ELF program headers
for 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.
程序中打印当前进程的调用堆栈(backtrace)的更多相关文章
- 在C/C++程序中打印当前函数调用栈
前几天帮同事跟踪的一个程序莫名退出,没有core dump(当然ulimit是打开的)的问题.我们知道,正常情况下,如果程序因为某种异常条件退出的话,应该会产生core dump,而如果程序正常退出的 ...
- Tom_No_02 Servlet向流中打印内容,之后在调用finsihResponse,调用上是先发送了body,后发送Header的解释
上次在培训班学上网课的时候就发现了这个问题,一直没有解决,昨天又碰到了,2-3小时也未能发现点端倪,今早又仔细缕了下,让我看了他的秘密 1.Servlet向流中打印内容,之后在调用finsihResp ...
- Wayland中的跨进程过程调用浅析
原文地址:http://blog.csdn.net/jinzhuojun/article/details/40264449 Wayland协议主要提供了Client端应用与Server端Composi ...
- [转]C# 关闭嵌在程序中的word进程而不关闭用户通过word手动打开的word进程
命名空间 :System.Diagnostics 以前在word的时候,经常碰到word进程产生一大堆,怕关错了,把用户自己打开的word也关闭,一直搞忽悠,今天上网花了10块钱,下了个文件,给我了一 ...
- 定位java程序中占用cpu最高的线程堆栈信息
找出占用cpu最高的线程堆栈信息 在java编码中,有时会因为粗心导致cpu占用较高的情况,为了避免影响程序的正常运行,需要找到问题并解决.这里模拟一个cpu占用较高的场景,并尝试定位到代码行. 示例 ...
- 巧用Grafana和Arthas自动抓取K8S中异常Java进程的线程堆栈
前言 近期发现业务高峰期时刻会出现CPU繁忙导致的timeout异常,通过监控来看是因为Node上面的一些Pod突发抢占了大量CPU导致的. 问: 没有限制CPU吗?是不是限制的CPU使用值就可以解决 ...
- 如何在linux用户空间程序中打印时间戳?
1. 使用clock_gettime接口即可 2. clock_gettime的使用方法: 2.1 定义一个结构体 struct timespec ts; 2.2 调用clock_gettime获取当 ...
- VC++ 崩溃处理以及打印调用堆栈
title: VC++ 崩溃处理以及打印调用堆栈 tags: [VC++, 结构化异常处理, 崩溃日志记录] date: 2018-08-28 20:59:54 categories: windows ...
- 在Python程序中的进程操作,multiprocess.Process模块
在python程序中的进程操作 之前我们已经了解了很多进程相关的理论知识,了解进程是什么应该不再困难了,刚刚我们已经了解了,运行中的程序就是一个进程.所有的进程都是通过它的父进程来创建的.因此,运行起 ...
随机推荐
- Python调试打印错误信息
try: ..... except Exception, e: print 'repr(e):\t', repr(e)
- Spring Cloud Netflix Zuul 重试会自动跳过经常超时的服务实例的简单说明和分析
在使用E版本的Spring Cloud Netflix Zuul内置的Ribbon重试功能时,发现Ribbon有一个非常有用的特性: 如果某个服务的某个实例经常需要重试,Ribbon则会在自己维护的一 ...
- Apache Spark as a Compiler: Joining a Billion Rows per Second on a Laptop(中英双语)
文章标题 Apache Spark as a Compiler: Joining a Billion Rows per Second on a Laptop Deep dive into the ne ...
- Django-jet自定义菜单
Django-jet自定义菜单:并且可设置权限.https://jet.readthedocs.io/en/latest/config_file.html#custom-menu
- [转]JS判断字符串是否为json数据
原文地址:https://blog.csdn.net/qq_26400953/article/details/77411520 这周碰到了很多问题,尽量把遇到的问题都记录下来. JS判断字符串是否为j ...
- 【算法】八皇后问题 Python实现
[八皇后问题] 问题: 国际象棋棋盘是8 * 8的方格,每个方格里放一个棋子.皇后这种棋子可以攻击同一行或者同一列或者斜线(左上左下右上右下四个方向)上的棋子.在一个棋盘上如果要放八个皇后,使得她们互 ...
- Laravel-mix 中文文档
镜像地址 : https://segmentfault.com/a/1190000015049847原文地址: Laravel Mix Docs 概览 基本示例 larave-mix 是位于w ...
- 聊天机器人(chatbot)终极指南:自然语言处理(NLP)和深度机器学习(Deep Machine Learning)
在过去的几个月中,我一直在收集自然语言处理(NLP)以及如何将NLP和深度学习(Deep Learning)应用到聊天机器人(Chatbots)方面的最好的资料. 时不时地我会发现一个出色的资源,因此 ...
- SpringCloud(一)浅谈SpringCloud
前言 现在微服务实在是太火了,所以我们必不可少的是要学习一下SpringCloud了,服务化的核心就是将传统的一站式应用 根据业务拆分成一个一个的服务,而微服务在这个基础上要更彻底地去耦合(不再共享D ...
- 2单一职责原则SRP
一.什么是单一职责原则 单一职责原则(Single Responsibility Principle ): 就一个类而言,应该仅有一个引起它变化的 原因. 二.多功能的山寨手机 山寨手机的功能: 拍照 ...