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

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


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

1. 堆栈回溯

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

  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):
  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,文件内容如:
  1. 13b864 1fdee8  
  2. 13b864 1fe000  
  3. 13b864 1ea30c  
  4. 169750 1be190  
  5. 19f13c 66ba78  
  6. 后面省略......  
13b864 1fdee8
13b864 1fe000
13b864 1ea30c
169750 1be190
19f13c 66ba78
后面省略......

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

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

  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 13b864
WebCore::Timer<WebCore:PluginStream>::fired()
diy-fp.cc:0

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

  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 -> node30
node26 -> node39
node22 -> node7
node8 -> node8
node14 -> node4
省略......
}

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


  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值