利用堆栈回溯、addr2line和Graphviz生成运行时函数调用图

来源:互联网 发布:linux安装lnmp环境 编辑:程序博客网 时间:2024/06/05 03:01

现在的软件源代码动则千万行,初学者常常感到迷惘,如果能自动生成关键函数的调用关系图,则思路可以清晰许多。如下面这幅图展示了WebKit网页渲染的部分函数执行过程,比单纯地看代码直观多了。


代码下载点这里,包括三个文件backtrace.h、backtrace.c和callgraph.py。

1. 堆栈回溯

比如要分析libwebcore.so里面的函数调用,首先要知道这个库文件在内存中的映射位置。程序启动时调用backtrace_init('libwebcore.so', 10, 65535)(第二个参数表示最大回溯层数,第三个参数表示最大栈帧大小)。函数读取“/proc/self/maps”得到:

[plain] view plaincopyprint?
  1. 48c00000-49751000 r-xp 00000000 1f:00 607 /system/lib/libwebcore.so  
48c00000-49751000 r-xp 00000000 1f:00 607 /system/lib/libwebcore.so

这一行表示libwebcore.so被映射到内存中48c00000-49751000的位置。为了用addr2line从内存地址得到函数名,对动态库要减去起始位置得到偏移量,对可执行文件不需要减去起始位置。

一般来说,函数栈帧的范围保存在基址寄存器(X86为BP寄存器,ARM为FP寄存器)和栈指针寄存器SP。在调用一个函数时,当前函数的基地址会被保存到栈上。为了让编译器生成标准的堆栈结构,GCC编译X86程序时需加上-fno-omit-frame-pointer参数,编译ARM程序要加上-fno-omit-frame-pointer -mapcs两个参数。

在关键函数开始处调用backtrace()函数实现堆栈回溯(以下代码只测试了ARM和64位X86,没有测试X84):
[cpp] view plaincopyprint?
  1. void backtrace()  
  2. {  
  3.   if (addr_end <= addr_start)  
  4.     return;  
  5.   void *bp = 0, *ip = 0, *sp = 0, *prev_bp = 0, *prev_ip = 0;  
  6.   
  7.   #if CPU_ARCH == CPU_ARCH_X86  
  8.   __asm__("mov %%ebp, %0;" : "=r"(bp));  
  9.   __asm__("mov %%esp, %0;" : "=r"(sp));  
  10.   #elif CPU_ARCH == CPU_ARCH_X86_64  
  11.   __asm__("movq %%rbp, %0;" : "=r"(bp));  
  12.   __asm__("movq %%rsp, %0;" : "=r"(sp));  
  13.   #elif CPU_ARCH == CPU_ARCH_ARM  
  14.   __asm__("mov %0, fp" : "=r"(bp));  
  15.   __asm__("mov %0, sp" : "=r"(sp));  
  16.   __asm__("mov %0, lr" : "=r"(ip));  
  17.   #else  
  18.   return;  
  19.   #endif  
  20.   int i = 0;  
  21.   while (bp >= sp) {  
  22.     #if CPU_ARCH == CPU_ARCH_X86 || CPU_ARCH == CPU_ARCH_X86_64  
  23.     prev_bp = *((void**)bp);  
  24.     prev_ip = *((void**)bp + 1);  
  25.     #else  
  26.     prev_bp = *((void**)bp - 3);  
  27.     prev_ip = *((void**)bp - 1);  
  28.     #endif  
  29.     if (prev_ip >= addr_start && prev_ip < addr_end  
  30.         && ip >= addr_start && ip < addr_end) {  
  31.       call_table_set((unsigned long)prev_ip - addr_start, (unsigned long)ip - addr_start);  
  32.     }  
  33.     if (abs(bp - prev_bp) > max_frame_size) //函数栈帧太大就认为出错  
  34.       break;  
  35.     i ++;  
  36.     if (i > max_frame_depth)  
  37.       break;  
  38.     bp = prev_bp;  
  39.     ip = prev_ip;  
  40.   }  
  41. }  
void backtrace(){  if (addr_end <= addr_start)    return;  void *bp = 0, *ip = 0, *sp = 0, *prev_bp = 0, *prev_ip = 0;  #if CPU_ARCH == CPU_ARCH_X86  __asm__("mov %%ebp, %0;" : "=r"(bp));  __asm__("mov %%esp, %0;" : "=r"(sp));  #elif CPU_ARCH == CPU_ARCH_X86_64  __asm__("movq %%rbp, %0;" : "=r"(bp));  __asm__("movq %%rsp, %0;" : "=r"(sp));  #elif CPU_ARCH == CPU_ARCH_ARM  __asm__("mov %0, fp" : "=r"(bp));  __asm__("mov %0, sp" : "=r"(sp));  __asm__("mov %0, lr" : "=r"(ip));  #else  return;  #endif  int i = 0;  while (bp >= sp) {    #if CPU_ARCH == CPU_ARCH_X86 || CPU_ARCH == CPU_ARCH_X86_64    prev_bp = *((void**)bp);    prev_ip = *((void**)bp + 1);    #else    prev_bp = *((void**)bp - 3);    prev_ip = *((void**)bp - 1);    #endif    if (prev_ip >= addr_start && prev_ip < addr_end        && ip >= addr_start && ip < addr_end) {      call_table_set((unsigned long)prev_ip - addr_start, (unsigned long)ip - addr_start);    }    if (abs(bp - prev_bp) > max_frame_size) //函数栈帧太大就认为出错      break;    i ++;    if (i > max_frame_depth)      break;    bp = prev_bp;    ip = prev_ip;  }}

call_table_set((unsigned long)prev_ip - addr_start, (unsigned long)ip - addr_start) 是把这个函数调用(prev_ip调用ip)保存到一个哈希表,addr_start和addr_end是libwebcore.so在内存中的映射地址范围。

在程序退出时调用backtrace_dump('backtrace.out')把这个哈希表的内容保存到文件backtrace.out,文件内容如:
[html] view plaincopyprint?
  1. 13b864 1fdee8  
  2. 13b864 1fe000  
  3. 13b864 1ea30c  
  4. 169750 1be190  
  5. 19f13c 66ba78  
  6. 后面省略......  
13b864 1fdee813b864 1fe00013b864 1ea30c169750 1be19019f13c 66ba78后面省略......

2. 生成函数调用图
调用脚本callgraph.py arm-eabi-addr2line ./out/....../lib/libwebcore.so backtrace.out callgraph.png
脚本处理流程如下:

对backtrace.out文件的每一个偏移量调用addr2line得到函数名:

[plain] view plaincopyprint?
  1. arm-eabi-addr2line -f -C -e ./out/....../lib/libwebcore.so 13b864  
  2. WebCore::Timer<WebCore:PluginStream>::fired()  
  3. diy-fp.cc:0  
arm-eabi-addr2line -f -C -e ./out/....../lib/libwebcore.so 13b864WebCore::Timer<WebCore:PluginStream>::fired()diy-fp.cc:0

根据函数名和其调用关系生成dot脚本文件:

[plain] view plaincopyprint?
  1. digraph G {  
  2. node0 [ label="android::RecordContent" ];  
  3. node1 [ label="GraphicsLayerAndroid::repaint" ];  
  4. node2 [ label="RenderLayer::paint" ];  
  5. node3 [ label="android::CreateFrame" ];  
  6. node4 [ label="PicturePile::updatePicturesIfNeeded" ];  
  7. 省略......  
  8. node0 -> node30  
  9. node26 -> node39  
  10. node22 -> node7  
  11. node8 -> node8  
  12. node14 -> node4  
  13. 省略......  
  14. }  
digraph G {node0 [ label="android::RecordContent" ];node1 [ label="GraphicsLayerAndroid::repaint" ];node2 [ label="RenderLayer::paint" ];node3 [ label="android::CreateFrame" ];node4 [ label="PicturePile::updatePicturesIfNeeded" ];省略......node0 -> node30node26 -> node39node22 -> node7node8 -> node8node14 -> node4省略......}

转换dot文件生成函数调用图:
[html] view plaincopyprint?
  1. dot -Tpng -Nshape=box -Nfontsize=10 callgraph.dot -o callgraph.png  
dot -Tpng -Nshape=box -Nfontsize=10 callgraph.dot -o callgraph.png


原创粉丝点击