LZ最近正在挠破头的啃ChromiumOS,因为之前没有过分析开源系统的经验,所以一筹莫展。
而这篇文章的出现,正好给了LZ以思路。非常感谢原作者!
ref from:: http://hi.baidu.com/lyb1900/blog/item/06b3d31e520b03e71bd576f1.html
--------------------------------------------------------------------------------------------------------
最近一段时间时间一直在做新产品的预研和代码分析方面的工作。整个过程是及其富有挑战性和探索性的,回顾这段时间的工作和学习,对代码分析方法有一些总结和心得,隧记录下来以备遗忘。这篇文档并非最终文档,在之后的工作和学习过程中会随时补充。
开源时代的来临给程序员们带来了丰盛的大餐,网络上存在着大量的优秀产品和代码可供我们品味。但是真正当你要公关一个具体的开源软件时,会面临代码量大、文档少、没有技术支持等多方面的问题。如何能更有效、更加快速的了解一个产品的特性、实现细节?我想这是每个程序员都在关心和思考的问题。这里记录的仅仅是我在研究和学习代码过程中的体会和心得,如果需要了解更多的有关代码阅读方法方面的朋友,请阅读Diomidis Spinellis的Code Reading。
一、收集信息
代码阅读的第一步是收集可以收集的所有信息,这些信息包括如下内容:
项目用户文档
项目设计文档
项目FAQ
项目测试用列
这些信息可以从项目的主页、Wiki、Google、邮件列表、论坛,以及相关的论文和书籍中获得,并且将收集的相关信息统一管理起来。这里我推荐三个不错的知识管理软件:myBase、Moin和Google Notebook:
myBase: 这个工具可以用于收集任何数字信息,包括网页、文件、多媒体信息等,并且可以方便的按照树形结构将其分门别类
Moin:这是一个Wiki工具,主页提供一个desktop版本,可以方便的copy到U盘携带,它最大的特点是每篇文章都是单独存放的可以方便的使用版本管理工具管理起来
Google Notebook:可以方便的为每个知识点打Tag,方便检索。貌似现在Notebook已经停止开发了,团队重心放到Google Document了。
二、制定分析策略
由于开源软件的代码量往往是惊人的,很多时候又缺少相关的设计文档和资料,并且对于个人来说精力也是有限的。因此,在代码分析之前制定一套分析策略是比较重要的。分析代码之前首先要明确我们分析的最终目的是什么?
如果我们分析代码的主要目的是了解该产品的特性,则我们的分析目标应该放在如下几个方面:
产品适用的领域有那些?
相关的产品和实现有那些?
产品的优点和缺点有那些?
通过对产品的横向和纵向比较,我们大致的能定位产品,以及确定其是否可以应用到现有的产品框架中。
如果我们分析代码的主要目的是学习新的框架,则重点应当放在如下几个方面:
产品采用了什么核心技术?
产品的逻辑框架是什么?
产品框架分成了那些模块?模块之间的关系是什么?
如果我们分析代码的主要目的是研究某个模块或者功能的具体实现细节,则应当将研究目标放在下面几个方面:
实现采用了什么算法?算法的思想和原理是什么?
实现采用了什么数据结构描述?
实现过程中使用了什么实现技巧?
不管分析的目的是以上三个方面的哪个方面,在分析过程中都应将重点放在拟定的分析目标的基础上,不断的向自己提出新问题。自己在分析具体代码时,曾由于目标不够明确最终迷失到庞大的代码中了。结果不但花费了很多时间,最终的结果也并非预期想要的。
三、源码分析
再收集和阅读相关资料,并且确定了分析策略后即可开始研读代码。代码的分析主要分成几个个部分:构建可运行环境、构建测试环境、源码的静态分析和源码的动态分析。静态分析过程重点在于把我整个代码的逻辑结构和关系,而动态代码分析主要是关注的数据流和实现方法。
3.1 构建源码的可运行环境
当源码从网络上down下来后,首先需要确保手头的代码是确实可执行的,因此首先应当构建一个完整的可执行系统。这样,今后由于修改造成的编译或者运行时错误,则可以确定是由于我们自己的修改导致的,而非代码本身的问题。构建可执行系统还有一个好处是通过阅读项目构建代码(Makefile、Scons或者Shell),可以了了解产品的大体框架结构,并且可以了解系统目前支持那些特性、依赖那些库和文件。一般来说,开源软件都会提供README、 INSTALL文件或者在其主页上提供构建的需求文件以及构建方法。参照文档,我们可以构建出至少debug和release两个不同的版本。但是文档中往往只会提及一些基本的构建指令、编译规则中有可能隐含了很多产品特性,这些特性可以通过阅读Makefile文件或者使用config --help来获得。通过不同的编译特性组合我们可以构建出不同特性的测试版本。
文档化构建过程是一个很好的习惯,这样不但可以以防遗忘,并且可以为之后的分析提供一定的信息。这里我通常使用Ooffice中的Excel工具将编译选项和其构建的文件关系用表的形式记录下来。
在构建成功后,应当使用一个比较熟悉的版本管理工具(SVN,GIT,CVS)将代码管理起来,因为之后我们的每次修改,都可能引发代码编译和运行时的问题,通过版本管理工具可以很快速的回滚。
3.2 构建测试环境
一般开源软件都会提供测试方法和代码,这些信息可以从项目主页或者代码树中获得。获得测试代码和方法后应当将这些测试代码在以构建的产品中运行一次,这样可以了解产品目前实现的状态以及发现一些缺陷。当然,并不是所有产品都会提供有效的测试代码,这种情况下只能自己动手写一些简单的测试用列,让产品RUN 起来。最后,应当将这个测试过程应当记录下来,并且编写成测试脚本以供之后代码动态分析和代码修改时候使用。
3.3 代码的静态分析
至此,我们应当大体了解了产品的逻辑框架结构,并且有了一定的用户体验。知道产品完成了那些功能,尚未支持哪些功能,有哪些特点。接着,我们应当更加深入的从代码的静态结构上深入了解产品的结构。
3.3.1 代码统计
这里主要做一些代码阅读工作量方面的统计,主要了解如下几个方面的内容:
有多少代码文件?
采用一种语言还是多种语言?语言的分布情况?
代码行数有多少?
3.3.2 构建模块关系
一般来说,开源软件的功能模块都是按照目录层次结构划分的,并且目录名称或者文件名称都会很明确的表示该模块或者文件的功能是什么。因此通过了解文件和目录的组织关系可以很直观的了解代码的组成结构。不过也有特列,如果文件名称或者目录名称不能很明确的标明功能,则需要通过分析代码中包和类关系来确定组成关系了。
在工作中,我专门为模块分析工作编写了一些脚本,通过这些脚本可以很方便的将目录、文件之间的关系以图表的方式直观的表现出来。
3.3.3 构建UML图
如果代码过于复杂和庞大,应当考虑用一些逆向工程工具从源码动态生成UML图(rose、jude),这样可以更加直观的反应代码的组成方式。这个过程重点应当放在弄清包之间的关系,以及对象之间的关系。
3.3.4 构建源码阅读环境
阅读代码之前需要使用一些代码阅读工具将代码整理和建立索引,这样可以更方便定位代码。这里我比较推荐Global、Doxygen、Source Navigator。这三个工具应该是Linux下用过的比较好的源码索引工具了。
Global是GNU开发的开源软件,可以构建成以HTML为基础的源码索引页面。通过和apache的配合,可以很方便地查找、定位函数和类。
Source Navigator是目前在Linux下最好的图形界面的源码浏览工具,之前由RED HAT开发和维护,但是中间停滞了5年,最近由德国一个组织接手继续开发。应该说这是Linux下唯一一个能和Source Insight比高下的产品了。
Doxygen,该软件不但可以建立代码索引而且可以从代码的注释中获得信息,生成帮助信息,并且可以绘制函数掉用关系图和类关系图。
3.3.5 了解关键的数据结构和算法
这个活动主要是以阅读代码为主,通过阅读代码的头文件可以对关键的数据结构和涉及的算法有一个初步的认识。了解这些数据结构的大致功能,数据结构之间的关系。最后,可以通过对数据结构代码的了解绘制出数据结构的大体的关系图。
3.3.6 产出物
通过以上的分析活动,可以得到如下的一些产出物。
1)了解代码阅读的工作量
2)了解代码的组成结构(模块关系、包关系、类关系)
3)了解核心的数据结构
4)绘制出代码数据统计表、模块和包关系图、类关系图、数据结构逻辑关系图
4. 源码的动态分析
源码的动态分析过程,主要的目的在于了解系统运行过程中,关键数据结构的操作,关键功能的函数调用关系,数据的组织和流向等方面。这个分析过程分成几个部分:运行时环境分析、函数调用分析、运行时数据分析。
4.1 运行时环境分析
这个步骤需要了解下面几个方面的问题:
运行时需要的环境变量是什么?
参数是什么?
运行时以来的库有那些?
运行时访问的资源有那些(文件,网络,内存分配等)?
这个过程可以通过读取/proc/xxx下面相关的文件、使用ldd、objdump等工具获得。
4.2 运行时函数调用分析
之前,大建的测试环境在这里就有用途了,通过运行不同的测试用列,并且使用gdb、callgrind、gprof、codeviz等工具可以很方便的获取函数的调用序列。将其捕获的数据经过脚本处理,去除一些不必要的噪声信息,可以绘制出一个非常直观的函数运行时调用序列图。这个过程对我们理解特定的产品特性的实现过程是相当有帮助的。
4.3 运行时数据分析
运行时数据分析过程,重点主要在关键数据结构的创建、操作和销毁过程,通过gdb或者logger工具,将不同执行过程中的数据内容以可读的方式输出出来。通过测试用列的配合,可以最终绘制出关键数据结构的运行时关系图。这里并没有一套比较统一的方法可以完成数据采集工具,我使用的比较多的方式是 gdb,通过gdb以及自定义的宏,可以动态的抓取和输出指定数据结构内容。当然这个方法对那些时间敏感的调用过程并不有效,因此这种情况下多半采用 logger的方式将数据输出。在很多开源软件中,开发者往往为代码的测试提供了一些宏或者数据遍历方法,可以使用这些功能接口,很便捷的输出需要的信息。
4.5 产出物
这个工程活动的最后大致有几个方面的产出物:
a)不同用列情况下的函数调用信息
b)数据结构关系图
c)数据流图
d)算法实现的原理文档
5. 添加和修改功能
了解一个系统的最终目的还是为了使用,因此为了检验对软件的理解是正确的,我们应当在现有的基础上去修改代码以验证我们的理解是没有问题的。但是整个过程需要建立在良好的测试环境基础上。这个活动的主要步骤有:
a)了解现有代码的实现规则(按照代码的统一规则去增添和修改代码)
b)确定修改或者添加功能的目标(实现或者修改什么功能,达到的效果是什么样的?)
c)实现代码
d)编写测试代码验证实现过程是正确的
四、总结
通过以上的4个工程过程,我们可以对一个产品有一个较为客观的认识,并且最后应当以文档的方式表达出来。如果为软件增添了新功能,还需要为新增功能增加接口文档和设计文档。
下图是总结后的思维导图: