为了方便调式程序,产品中需要在程序崩溃或遇到问题时打印出当前的调用堆栈。由于是基于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.