1. 背景
现实中,我们需要学习已有的软件构造方法,或维护旧的软件,或添加新功能,亦或优化性能,
这时面对的代码都不是自己所写,那如何才能快速知道这个代码库干了什么,源文件如何组织,模块间如何交互?
2. 程序的运转
a)应用程序:一般是一个语法树结构,树的叶子结点或是简单的操作(+、-、=等),或是系统调用(read、write、sleep),
树根一般是main;如果有多线程/进程,那就会产生多颗树;在出现递归调用时,也会出现环路。
b)内核:内核是应用与硬件间的中间层,交互方法就是软中断和硬中断;
应用使用int指令触发系统调用,设备使用irq中断触发事件通知,内核使用in/out指令读写设备;
所以,内核一般有两部分组成:初始化流程(注册各种中断处理器),中断处理流程(各种中断响应例程);
最后,对于中断是自动切换上下文(寄存器状态),对于多任务执行则需要调度(哪些线程挂起,哪些线程运行)。
c)不论应用还是内核,一个执行流都是一颗树,树中的结点对应某个函数,这些函数都是可以读写全局变量和自己的局部变量。
3. 分析方法
所有程序都有启动流程,对于内核还有中断响应流程,数据是按需创建的,关键在于流程的把握。
流程包含了结点(函数)和结点间的关系(调用和被调),是一个有向图;
流入该结点的边说明了其用途,流出该结点的边说明了其实现方法。
这里介绍一种自动从源码生成流程图的工具:callgraph
该工具自动识别函数调用关系,并输出有向图,然后可以使用graphviz的dot命令生成可视化图表
保存格式可为pdf、png、svg等。
4. 示例分析
下面以linux-0.11的代码分析为例:
$ git clone https://github.com/gnprice/callgraph.git
下载后需要修改print_dot,以免无法显示:
~/projects/callgraph [master]$ git diff diff --git a/callgraph b/callgraph index e771dce..a79fcd8 100755 --- a/callgraph +++ b/callgraph @@ -67,12 +67,12 @@ def print_dot(refs, nonstatic): size = (scale[0] * 11 - 1, scale[1] * 8.5 - 1) # inches; letter, 1" margins print ‘strict digraph calls {‘ print ‘ rankdir=LR;‘ - print ‘ size="%f,%f";‘ % size - print ‘ ratio=fill;‘ - print ‘ rotate=90;‘ +# print ‘ size="%f,%f";‘ % size +# print ‘ ratio=fill;‘ +# print ‘ rotate=90;‘ print ‘ center=1;‘ - print ‘ margin=0;‘ - print ‘ page="8.5,11";‘ +# print ‘ margin=0;‘ +# print ‘ page="8.5,11";‘ funs = set(refs.keys()) for f, calls in sorted(refs.iteritems()): shape = ‘box‘ if f in nonstatic else ‘plaintext‘
$ curl -O https://www.kernel.org/pub/linux/kernel/Historic/old-versions/linux-0.11.tar.gz
$ tar -xf linux-0.11.tar.gz
$ cd linux-0.11/init
~/projects/linux-0.11/init
$ cat main.c | ~/projects/callgraph/callgraph | dot -Tpng > main.png
单文件init/main.c调用图如下:
整个kernel 子目录的调用图:
$ find kernel -type f -name "*.c" -o -name "*.h" | xargs cat | ~/projects/callgraph/callgraph | dot -Tpng > kernel.png