提升你的逆向工程能力
理查德·费曼,我们这个时代最伟大的科学家和物理学家之一,曾说过一句名言:「凡是我不能重新创造的,我就没有真正理解。」他的意思是:从一张白纸出发,只凭脑子里已有的知识,他就能把任何理论结果重新推导一遍。费曼认为,这种能力才是真正理解一件事的标志。
我自己也坚信必须掌握底层原理。牢固掌握既有问题的现成解法,是想出新解法的绝对前提。在你所处的领域里,凡是已经被解决过的问题,你都得知道该怎么解。
但这里有个难题——这类知识去哪里找?在如今这个节奏飞快的世界里,技术的作者们几乎没有时间去写文章阐述底层原理。那怎么办?我主张:做逆向工程。
大家知道我逆向分析过 Angular。但 Angular 并不是我唯一深入研究过的框架。我还研究过 Vue.js、Webpack、jQuery 以及许多其他 Web 库和框架。现在我正在啃 React。我认为自己积累的心得已经足够拿出来分享,帮你入门逆向工程。
对我来说,逆向工程的过程是发现新事物的魔法。那是一种为新发现而兴奋、像黑客一样思考的状态:永远保持好奇。希望这份指南能帮你的头脑进入同样的状态。
我把心得拆成了两篇文章。本篇讲我在做逆向工程时所依循的方法与原则。第二篇通过实际逆向分析 React 的一小部分,展示这些原则的具体应用,同时演示几种能加快逆向进度的有趣调试技巧。
不过首先,我们先说说你为什么要去做逆向工程。
为什么
我们得承认:逆向工程是件苦差事。它耗时,而且通常需要相当扎实的知识储备。那何必自找麻烦?
多数人认为,逆向工程的首要目的是加深对某项技术的了解,好找一份更好的工作。而既然现代技术的生命周期相当短暂,花时间钻得太深似乎并不划算。
通读源码,你几乎必然会对这项技术获得极好的理解。但这只是逆向工程带来的诸多收益之一。
在阅读源码的过程中,你会熟悉解决常见问题的新设计模式,之后可以在工作中复用。这一点我亲身体会过很多次。比如逆向 Angular Router 时,我学会了如何懒加载组件和模块,这帮我搭起了一个基于插件的平台。
你会学到语言的许多新特性。逆向 Angular 时,我了解了 JavaScript 中的单态性。你会掌握底层平台的 API,因为大多数技术都大量依赖它。你会发现新的学习方向,并看到它们的实际用法,而不是只在书里读到、再去猜哪里能用得上。
而如果你决定把发现分享给社区,这会帮你建立公开形象。这是双赢:帮助别人的同时也在帮助自己。我的成功故事始于 Angular-In-Depth(AiD)——那时我决定开始把自己的发现写出来。此后 AiD 成长为最大的 Angular 专栏,并极大地帮助我开始在大会上演讲,以及在 ag-Grid 找到一份理想的工作。花时间深入学习一项技术,同时也是在展示你解决问题的能力、决心和好奇心。这些正是创新型公司在候选人身上看重的品质。
你还会变得善于阅读既有代码、上手陌生的代码库。罗伯特·C·马丁(人称 Uncle Bob)估计,阅读代码与编写代码所花时间之比远超 10 比 1。为了写新代码,我们一直在读旧代码。所有开发者接手现有项目时,都是从这里开始的。而通过练习逆向工程,你在这方面会占据优势。
如你所见,逆向工程会让你成为更好的工程师。
有助于逆向工程的知识储备
我们先来看看,要让逆向工程的过程更轻松、更快速,你需要掌握哪些东西。花时间去学习并精通它们。要掌握的内容不少,你现在多半还不具备这些知识。别担心。每天留出一两个小时刻意学习,并把成为这些领域的专家定为目标。你会做到的。
对底层平台的扎实掌握
成功做逆向工程的第一个前提,是了解底层平台的 API。如果说的是运行在 Web 上的框架和库,那么你需要牢固掌握以下几样:JavaScript、DOM API 和浏览器 API。
对 JavaScript 的扎实理解,不是指你知道什么是闭包、this 如何解析。扎实理解意味着你得掌握大部分进阶内容,比如属性描述符(Vue.js 在用)、Proxy 对象或位掩码(Angular 在用)。
说到 DOM 和浏览器 API,仅仅会创建并插入一个 DOM 节点、会异步执行一个回调是不够的。你需要知道:重新插入一个已存在的子节点会发生什么,浏览器如何处理未知元素。去了解现有的 HTTP 请求 API 以及它们的细节,比如 XHR 的失败回调究竟在什么时候被执行。
要精通 JavaScript,我推荐 Axel Rauschmayer 的书。要精通 DOM 和浏览器 API,我推荐 MDN web docs 和 Google developers web updates。而如果你能学会读规范,那当然是最理想的来源:JavaScript 看 EcmaScript,浏览器环境看 WHATWG。
调试工具
把你常用浏览器的开发者工具摸透,这一点至关重要。我常用的浏览器是 Chrome。在 Chrome 里,你应当知道:
- 在控制台输入
$0时它代表什么 - 如何使用条件断点
- 如何在异常抛出前暂停
- 如何跳过某段代码或跳出当前函数
- 如何在已加载的源码中查找特定文本,等等
关于 Chrome Dev Tools,最好的资源当然是 Google’s Tools for web developers。
常见设计模式与通用架构概念
技术有时会使用常见的设计模式,所以了解它们很有好处。比如 Webpack 依赖 async 库所实现的异步 JS 执行模式。在 ES modules 出现之前,所有框架和库都使用 UMD 打包格式来分发。随着你研究的框架和库越来越多,你会开始认出这些共通的模式——在代码中推进的速度也会更快。
特定技术自身的概念
了解某个框架或库所使用的概念同样有帮助。比如在逆向一个现代框架之前,你应该知道什么是组件。这些概念有时能从文档中获得,有时来自一篇深度文章或设计文档。在开始看代码之前,把能找到的都读一遍。在代码中遇到新概念时,随时去补读。我建议选择带有具体实现细节的材料,而不是面向大众、被大幅简化的版本。大会演讲通常不是了解具体细节的好途径;Github 上的设计文档和进阶文章要有用得多。
探索源码的方法
去读后续那篇文章,看看我是如何运用这些方法通读 React 源码的。我建议你把沿途发现的知识碎片记下来。之后你就能把它们拼在一起,看到更完整的图景。
先确定要聚焦技术的哪一部分
我被问得最多的问题是:从哪里开始?在这个代码库里该把 debugger 放在哪儿?这其实由你的目标决定。在动手逆向之前,你心里始终要清楚自己想弄懂技术的哪一部分。比如逆向 Angular 或 React 时,我首先想理解的是变更检测。那就是我需要聚焦的部分。凭着对现代变更检测流程的了解,我知道变更检测的核心是把组件实例上的变化同步到 DOM 节点。所以我需要找到这些框架把已创建 DOM 节点的引用存放在哪里。这就是目标。
像科学家一样思考
我认为科学方法——观察、基于观察提出假设,再通过实验加以检验——是获取知识最有效的方法。这也是我做逆向工程时所用的模型。基本步骤如下:
- 做出观察并形成假设。
- 基于假设做出预测。
- 检验这个预测。
拿到结果后,用它们提出新的假设和预测。反复迭代,直到你把聚焦的那部分弄明白为止。
用推断来形成假设、做出预测。推断就是结合观察与背景知识得出合乎逻辑的结论。比如你看到某人吃一道新菜时皱了眉,就可以推断他不喜欢;又比如有人摔门而出,你可以推断他因为什么事而不快。
除了给逆向工程提供一套有条理的方法之外,我还认为:假设与验证会在记忆中形成锚点,帮助你把学到的东西记得更久,并在需要时把它们取出来。
在调试、查看源码实现和阅读注释之间来回切换
逆向工程不只是读源码。事实上,我只有大约 20% 的时间用在源码里核对某个具体实现细节上,大约 70% 的时间花在调试示例应用。这正是我认为熟练掌握调试工具对提高逆向效率不可或缺的原因。剩下 10% 的时间,我用来读源码里的注释,或者读代码中所发现概念的说明。通常注释比你在网上能找到的任何东西都有用得多,所以千万别把它们当成无关紧要的内容而略过。
用调用栈还原应用的执行流程
在调试推进、在代码不同位置打断点的过程中,要养成查看调用栈的习惯。函数调用的先后顺序会让你对应用的执行流程心里有数。它往往还能帮你在源码中定位到承载相关功能的函数。
别因为假设错了就气馁
做好心理准备:你的大多数假设都会被证伪。这是正常且意料之中的过程。有时它意味着你该花更多时间去补背景知识;但更多时候,它只是说明这个框架或库所实现的模式是新颖的。
这不是说你不会感到沮丧。你会的。但请盯住目标,越过这些沮丧,继续往前。想错了,恰恰意味着你刚刚学到了新东西。
给自己一点时间消化所发现的东西
芭芭拉·奥克利在《学习之道》一书中讲到两种交替出现的心智状态——专注模式与发散模式。学习新事物时两者缺一不可。专注模式是用理性的、线性的、分析性的方式直接解决问题。发散模式让我们对苦苦纠缠的问题突然产生新的洞见,它与「大局观」相关联。当你放松注意力、任由思绪游走时,发散模式就出现了。所以别一坐在电脑前就是几个小时;要定期短暂休息,想一想自己发现了什么。我就是在公寓里来回走走。而且我做任何创造性工作时都这样,比如写作,不只是逆向工程。
从获取源码和搭建示例应用开始
要逆向一个框架或库,你需要它的源码和一个使用该技术的示例应用。如今大多数框架和库都托管在 Github 上,直接把仓库克隆下来就行。你要研究的总是某个特定版本,所以去仓库的「releases」标签页看看最新的发布版本。克隆完成后,用 git checkout tags/[version] 切换到这个版本。
第二步是搭建示例应用。永远尽量把配置做到最简。避开 webpack 之类的打包工具和现代框架提供的 CLI 工具——以我的经验,它们会让调试体验复杂得多。理想的配置是一个普通的 HTML 页面,代码从 CDN 加载,比如 unpkg.com。只要确认框架或库的版本与你在源码中检出的版本一致即可。
关于运气
和人生中的所有事一样,运气也有它的一份。当你反复走过同一段功能时,可能会撞见一条新注释,让某个概念豁然开朗;或者看到一个此前没注意过的、名字很有说明性的函数调用。这种事我遇到过很多次。所以我建议:对那段你看不懂的代码,用调试器多走几遍;或者过一阵再回来看。别只因为现在看不懂就彻底放下它。很有可能下一次再走一遍时,你会发现新的东西——因为你已经积累了新的认知。