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

原文地址:https://www.cnblogs.com/clblacksmith/p/8378434.html

时间: 2024-10-06 00:38:52

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

在ASP程序中打印Excel报表的新方法

目前,B/S模式(浏览器/服务器模式)成为企业网上首选的计算模式.由于B/S模式的特殊性,在C/S下相对较易实现的Excel报表打印功能在B/S下却成为一个难点.本人通过研究写了一个基于ASP程序的打印Excel报表的程序.本程序的特点是无须任何组件. Print.asp ------------------------------------------------ <html><title>打印Excel报表</title> <% '控制脚本语言 respon

Wayland中的跨进程过程调用浅析

原文地址:http://blog.csdn.net/jinzhuojun/article/details/40264449 Wayland协议主要提供了Client端应用与Server端Compositor的通信机制,Weston是Server端Compositor的一个参考实现.Wayland协议中最基础的是提供了一种面向对象的跨进程过程调用的功能,在作用上类似于Android中的Binder.与Binder不同的是,在Wayland中Client和Server底层通过domain socke

Wayland (三) Wayland中的跨进程过程调用浅析 [FW]

原文地址:http://blog.csdn.net/jinzhuojun/article/details/40264449 Wayland协议主要提供了Client端应用与Server端Compositor的通信机制,Weston是Server端Compositor的一个參考实现.Wayland协议中最基础的是提供了一种面向对象的跨进程过程调用的功能,在作用上类似于Android中的Binder.与Binder不同的是,在Wayland中Client和Server底层通过domain socke

JNI基础 c程序中打印数据到控制台

(1)声明头文件,拷贝下面的内容到c文件中 #include <android/log.h> #define LOG_TAG "clog" #define LOGI(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,  __VA_ARGS__) #define LOGD(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,  __VA_ARGS__) (2)在Android.

VC++ 崩溃处理以及打印调用堆栈

title: VC++ 崩溃处理以及打印调用堆栈 tags: [VC++, 结构化异常处理, 崩溃日志记录] date: 2018-08-28 20:59:54 categories: windows 高级编程 keywords: VC++, 结构化异常处理SEH, 崩溃日志记录 --- 我们在程序发布后总会面临崩溃的情况,这个时候一般很难重现或者很难定位到程序崩溃的位置,之前有方法在程序崩溃的时候记录dump文件然后通过windbg来分析.那种方法对开发人员的要求较高,它需要程序员理解内存.寄

Python程序中的进程操作-开启多进程(multiprocess.process)

目录 一.multiprocess模块 二.multiprocess.process模块 三.process模块介绍 3.1 方法介绍 3.2 属性介绍 3.3 在windows中使用process模块的注意事项 四.使用process模块创建进程 4.1 在Python中启动的第一个子进程 4.2 join方法 4.3 查看主进程和子进程的进程号 4.4 多个进程同时运行 4.5 多个进程同时运行,再谈join方法(1) 4.6 多个进程同时运行,再谈join方法(2) 4.7 通过继承Pro

110 python程序中的进程操作-开启多进程

之前我们已经了解了很多进程相关的理论知识,了解进程是什么应该不再困难了,刚刚我们已经了解了,运行中的程序就是一个进程.所有的进程都是通过它的父进程来创建的.因此,运行起来的python程序也是一个进程,那么我们也可以在程序中再创建进程.多个进程可以实现并发效果,也就是说,当我们的程序中存在多个进程的时候,在某些时候,就会让程序的执行速度变快.以我们之前所学的知识,并不能实现创建进程这个功能,所以我们就需要借助python中强大的模块. 一.multiprocess模块 仔细说来,multipro

java程序中调用dos shell命令 -- 此处以调用doc命令为例

/**  *  Java调用windows的DOS命令  */ public class RunDocInJava{     public static void main(String[] args) {         InputStream ins = null;         String[] cmd = new String[] { "cmd.exe", "/c", "ipconfig" };  // 命令行         try 

linux c程序中获取shell脚本输出的实现方法

linux c程序中获取shell脚本输出的实现方法 1. 前言Unix界有一句名言:“一行shell脚本胜过万行C程序”,虽然这句话有些夸张,但不可否认的是,借助脚本确实能够极大的简化一些编程工作.比如实现一个ping程序来测试网络的连通性,实现ping函数需要写上200~300行代码,为什么不能直接调用系统的ping命令呢?通常在程序中通过 system函数来调用shell命令.但是,system函数仅返回命令是否执行成功,而我们可能需要获得shell命令在控制台上输出的结果.例如,执行外部