如何阅读一份源代码?
CONTENTS
先跑起来调试手段使用顺手的工具情景分析利用好测试用例厘清核心数据结构之间的关系整体和细节多问自己几个问题写自己的代码阅读笔记总结
我在本文的基础上做了一些补充和改进,见《如何阅读一份源代码?(2020年版)》
阅读源代码的能力算是程序员的一种底层基础能力之一,这个能力之所以重要,原因在于:
- 不可避免的需要阅读或者接手他人的项目。比如调研一个开源项目,比如接手一个其他人的项目。
- 阅读优秀的项目源码是学习他人优秀经验的重要途径之一,这一点我自己深有体会。
然而,读代码比写代码还是更难一些,原因在于“写代码是在表达自己,读代码是在理解别人”。因为面对的项目多,项目的作者有各自的风格,理解起来需要花费不少的精力。
我从业这些年泛读、精读过的项目源码不算少了,陆陆续续的也写了一些代码分析的文章,本文中就简单总结一下我的方法。
先跑起来
开始阅读一份项目源码的第一步,是先让这个项目能够通过你自己编译通过并且顺利跑起来。这一点尤其重要。
有的项目比较复杂,依赖的组件多,搭建起一个调试环境并不容易,所以并不见得是所有项目都能顺利的跑起来。如果能自己编译跑起来,那么后面讲到的情景分析、加上调试代码、调试等等才有展开的基础。
就我的经验而言,一个项目代码,是否能顺利的搭建调试环境,效率大不一样。
跑起来之后,又要尽量的精简自己的环境,减少调试过程中的干扰信息。比如,Nginx使用多进程的方式处理请求,为了调试跟踪Nginx的行为,我经常把worker数量设置为1个,这样调试的时候就知道待跟踪的是哪个进程了。
总而言之,跑起来之后的调试效率能提升很多,而在跑起来的前提之下又要尽量精简环境。
调试手段
调试手段,大体分为以下两种:
- 加调试语句。为了做到这一点,你需要先了解项目如何加调试日志,可能需要修改项目的日志级别支持输出一些在调试级别的日志,等等。
- 断点调试。并不是所有项目代码,跑起来之后都自带调试信息能够断点调试的。所以在自己的调试环境里需要先确定这一点。比如一些C相关的项目,基本都是”./configure & make”来编译,但是makefile中的编译flags使用了O2之类的优化选项,此时需要自己先手动修改成”-O0 -g”,即编译生成的二进制中不优化且带上调试信息。
总之,在能够搭建自己的调试环境之后,还需要想办法确定一下如何加上调试日志以及断点调试。
使用顺手的工具
好的工具会让你事半功倍,这一点应该很多人都同意。
我阅读Go代码的时候,喜欢使用IDEA,这个IDE工具可以完美的做到以下几点:
- 符号的定位、跳转、查找符号被引用的地方。
- 左边能够展开一个源码文件中的所有符号。
反之,很多人推崇的VSCode,我几次尝试使用用来阅读Go和C类代码,都觉得不够顺手,查找符号能力不行、也没有地方可以看到一个文件中出现的符号。
C\C++类的代码,在尝试各种工具之后,还是使用Vim+Ctags+Cscope来写C、C++代码。
情景分析
假如有了前面的基础,已经能够让项目顺利在自己的调试环境跑起来了,那么就可以对项目代码进行情景分析了。
所谓的“情景分析”,我的理解就是自己构造一些情景,然后通过加断点、调试语句等分析在这些场景下的行为。
以我自己为例,在写《Lua设计与实现》时,讲解到Lua虚拟机指令的解释和执行过程中,需要针对每个指令做分析,此时用的就是情景分析的方法。我会模拟出来使用该指令的Lua脚本代码,然后在程序里断点调试这些场景下的行为。
我惯用的做法,是在某个重要的入口函数上面加上断点,然后构造触发场景的调试代码,当代码在断点处停下,通过查看堆栈、变量值等等来观察代码的行为。
例如,Lua解释器代码中中,生成Opcode最终都会调用函数luaK_code,那么我就在这个函数上面加上断点,然后构造我想要调试的场景,只要在断点处中断,我通过函数堆栈就能看到完整的调用流程:
(lldb) bt
* thread #1: tid = 0xb1dd2, 0x00000001000071b0 lua`luaK_code, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00000001000071b0 lua`luaK_code
frame #1: 0x000000010000753e lua`discharge2reg + 238
frame #2: 0x000000010000588f lua`exp2reg + 31
frame #3: 0x000000010000f15b lua`statement + 3131
frame #4: 0x000000010000e0b6 lua`luaY_parser + 182
frame #5: 0x0000000100009de9 lua`f_parser + 89
frame #6: 0x0000000100008ba5 lua`luaD_rawrunprotected + 85
frame #7: 0x0000000100009bf4 lua`luaD_pcall + 68
frame #8: 0x0000000100009d65 lua`luaD_protectedparser + 69
frame #9: 0x00000001000047e1 lua`lua_load + 65
frame #10: 0x0000000100018071 lua`luaL_loadfile + 433
frame #11: 0x0000000100000eb9 lua`pmain + 1545
frame #12: 0x00000001000090cd lua`luaD_precall + 589
frame #13: 0x00000001000098c1 lua`luaD_call + 81
frame #14: 0x0000000100008ba5 lua`luaD_rawrunprotected + 85
frame #15: 0x0000000100009bf4 lua`luaD_pcall + 68
frame #16: 0x00000001000046fb lua`lua_cpcall + 43
frame #17: 0x00000001000007af lua`main + 63
frame #18: 0x00007fff6468708d libdyld.dylib`start + 1
情景分析的好处在于:不会在一个项目中大海捞针似的查找,而是能够把问题缩小到一个范围内展开来理解。
“情景分析”这一概念不是我想出来的名词,比如有这么几本分析代码的书籍,如:《Linux内核源代码情景分析》,《Windows内核情景分析》。
利用好测试用例
好的项目都会自带不少用例,这类型的例子有:etcd、google出品的几个开源项目。
如果测试用例写的很仔细,那么很值得好好去研究一下。原因在于:测试用例往往是针对某个单一的场景,独自构造出一些数据来对程序的流程进行验证。所以,其实跟前面的“情景分析”一样,都是让你从大的项目转而关注具体某个场景的手段之一。
厘清核心数据结构之间的关系
虽然说“程序设计=算法+数据结构”,然后我实际中的体会,数据结构更加重要。
因为结构定义了一个程序的架构,结构定下来了才有具体的实现。
Linus说: “烂程序员关心的是代码。好程序员关心的是数据结构和它们之间的关系。”
因此,在阅读一份代码时,厘清核心的数据结构之间的关系尤其重要。这个时候,需要使用一些工具来画一下这些结构之间的关系,我的源码分析类博客中有很多这样的例子,比如《Leveldb代码阅读笔记》、《Etcd存储的实现》等等。
需要说明的是,情景分析、厘清核心数据结构这两步并没有严格的顺序关系,不见得是先做某事再做某事,而是交互进行的。
比如,你如果现在刚接手某个项目,需要简单的了解一下项目,可以先阅读代码了解都有哪些核心数据结构。理解了之后,如果不清楚某些情景下的流程,可以使用情景分析法。总而言之,交替进行直到解答你的疑问为止。
整体和细节
阅读代码的过程中,需要在整体和细节之间做权衡。
比如,有时候你需要大体了解一个整体的框架、轮廓、流程之后,才能再针对具体的细节深入进去。这个时候,不宜针对具体的函数实现、算法等深入分析。而细节的分析,又不能缺少,否则一些东西的理解又流于表面。
所以,如何把握整体和细节是一个需要累积阅读代码经验才能把握好的。我的建议是:过程中还是以整体为首,在不理解整体的前提之前,不要太过深入某个细节。把某个函数、数据结构当成一个黑盒,知道它们的输入、输出就好,只要不影响整体的理解就暂且放下接着往前看。
多问自己几个问题
学习的过程中离不开交互。
如果阅读代码只是输入(Input),那么还需要有输出(Output)。只有简单的输入好比喂东西给你吃,而只有更好的消化才能变为自己的营养,而输出就是更好消化知识的重要手段。
其实这个思想很常见,比如学生上课(Input)了需要做练习作业(Output),比如学了算法(Input)需要自己编码练习(Output),等等。简而言之,输出是学习过程中的一种及时反馈,质量越高学习效率越高。
输出的手段有很多,在阅读代码时,比较建议的是自己能够多问自己一些问题,比如:
- 为什么选择这个数据结构来描述这个问题?类似的场景下,其他项目是怎么设计的?都有哪些数据结构做这样的事情?
- 如果由我来设计这样的项目,我会怎么做?
等等等等。越是主动积极的思考,就越有更好的输出,输出质量与学习质量成正比关系。
写自己的代码阅读笔记
我从开始写博客,就是写不少各种项目的代码解读类文章,网名“codedump”也源于想把“code内部的实现原理dump出来”之意。
前面提到学习质量与输出质量成正比关系,这是我自己的深刻体会。也因为如此,所以才要坚持阅读源码之后写自己的分析类笔记。
写这类笔记,有以下几个需要注意的地方。
虽然是笔记,但是要想象着在向一个不太熟悉这个项目的人讲解原理,或者想象一下是几个月甚至几年后的自己回头来看这个文章。在这种情况下,会尽量的把语言组织好,循循善诱的解释。
尽量避免大段的贴代码。我认为在这类文章中,大段贴上代码有点自欺欺人:就是看上去自己懂了,其实并不见得。如果真要解释某段代码,可以使用伪代码或者缩减代码的方式。记住:不要自欺欺人,要真的懂了。如果真的想在代码上加上自己的注释,我有一个建议是fork出来一份该项目某个版本的代码,提交到自己的github上,上面随时可以加上自己的注释并且保存提交。比如我自己注释的etcd 3.1.10代码:etcd-3.1.10-codedump,类似的我阅读的其他项目都会在github上fork出一个带上codedump后缀的项目。
多画图,一图胜千言,使用图形展示代码流程、数据结构之间的关系。我最近才发现画图能力也是很重要的能力,自己在从头学习如何使用图像来表达自己的想法。
写作是很重要的基础能力,我一个朋友最近教育我,大体的意思是说:如果你在某方面的能力很强,如果再加上写作好、英语好,那么将极大放大你在这方面的能力。而类似写作、英语这样的底层基础能力,不是一撮而就的,需要长时间保持练习才可以。而写博客,对于技术人员而言,就是一种很好的锻炼写作的手段。
总结
以上是我简单总结的一些阅读源码时候的手段和注意方法,大体而言有那么几点吧:
- 只有更好的输出才能更好的消化知识,所谓的搭建调试环境、情景分析、多问自己问题、写代码阅读笔记等都是围绕输出来展开的。总而言之,不能像一条死鱼一样指望着光靠看代码就能完全理解它的原理,需要想办法跟它互动起来。
- 写作是人的基础硬实力之一,不仅锻炼自己表达能力,还能帮助整理自己的思路。对程序员而言锻炼写作能力的手段之一就是写博客,越早开始锻炼越好。
最后,如同任何可以习得的技能一般,阅读代码这种能力也需要长时间、大量的反复练习,下一次就从自己感兴趣的项目开始锻炼自己的这种技能吧。
Author codedump
LastMod 2019-03-24
License 本作品采用知识共享署名 4.0 国际许可协议进行许可。 转载时请注明原文链接,图片使用OmniGraffle进行绘制。