利用堆栈回顾、addr2line和Graphviz生成运行时函数调用图
利用堆栈回溯、addr2line和Graphviz生成运行时函数调用图
1. 堆栈回溯
这一行表示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):
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,文件内容如:
2. 生成函数调用图
调用脚本callgraph.py arm-eabi-addr2line ./out/....../lib/libwebcore.so backtrace.out callgraph.png
脚本处理流程如下:
转换dot文件生成函数调用图:
现在的软件源代码动则千万行,初学者常常感到迷惘,如果能自动生成关键函数的调用关系图,则思路可以清晰许多。如下面这幅图展示了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
这一行表示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在内存中的映射地址范围。
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