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

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

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

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

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

1. 堆栈回溯

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

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):

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,文件内容如:

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得到函数名:

arm-eabi-addr2line -f -C -e ./out/....../lib/libwebcore.so 13b864
WebCore::Timer<WebCore:PluginStream>::fired()
diy-fp.cc:0

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

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文件生成函数调用图:

dot -Tpng -Nshape=box -Nfontsize=10 callgraph.dot -o callgraph.png