现在的软件源代码动则千万行,初学者常常感到迷惘,如果能自动生成关键函数的调用关系图,则思路可以清晰许多。如下面这幅图展示了WebKit网页渲染的部分函数执行过程,比单纯地看代码直观多了。
1. 堆栈回溯
比如要分析libwebcore.so里面的函数调用,首先要知道这个库文件在内存中的映射位置。程序启动时调用backtrace_init('libwebcore.so', 10, 65535)(第二个参数表示最大回溯层数,第三个参数表示最大栈帧大小)。函数读取“/proc/self/maps”得到:
- 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):
- 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;
- }
- }
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在内存中的映射地址范围。
- 13b864 1fdee8
- 13b864 1fe000
- 13b864 1ea30c
- 169750 1be190
- 19f13c 66ba78
- 后面省略......
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
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
- 省略......
- }
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
dot -Tpng -Nshape=box -Nfontsize=10 callgraph.dot -o callgraph.png