本篇内容是根据2021年5月份#227 Analyzing static analysis[1]音频录制内容的整理与翻译
来自以色列理工学院的 Matan Peled[2] 与 Natalie[3] 和 Mat[4] 一起讨论他在PhD阶段研究的元编程和静态分析器方面内容。Go 的表现如何?如果 Matan 来构建一个 Go,它会是什么样子?
过程中为符合中文惯用表达有适当删改, 版权归原作者所有.
Natalie Pistunovich: 各位早上、下午、晚上好,欢迎收听这一期关于静态检查器的播客。欢迎Mat,我这一期的联合主持人。你好吗?
Mat Ryer: 你好,Natalie,我也很好,谢谢。你怎么样?
Natalie Pistunovich: 很好,很好!我很喜欢我们背景里的那些植物。今天的嘉宾是Matan,欢迎你!
Matan Peled: 你好,谢谢你。
Natalie Pistunovich: 你最近怎么样?
Matan Peled: 我还不错,虽然有点晚了... 今天已经很长了,但我还是感觉不错。
Natalie Pistunovich: 你是从哪里加入我们的?
Matan Peled: 我是从xx加入的。你可以从那边的窗户看到海,如果外面不是那么黑的话。
Mat Ryer: 哇哦。
Natalie Pistunovich: 从以色列远道而来... 我看到你背景里没有植物,反而有鸭子,估计你是用它们来做"鸭子调试"的,一如既往。而且你有两只,一只是白色的,一只是红色的,给那些只听音频没看视频的人解释一下... 红色的那只会说“直接强推提交吧”,而白色的则说“再运行一次测试吧”。
Mat Ryer: [笑]
Matan Peled: 我还有几只,其中一只是熊猫鸭。
Mat Ryer: 哦对,熊猫鸭。它看起来有点像个小恶魔。
Natalie Pistunovich: 而蓝色的那只则是为蓝队准备的,也就是代码还不错,但没太多测试,但也不至于强推。
Matan Peled: 是啊,它似乎能平衡一下红色的那只。
Mat Ryer: 我们最近在阿姆斯特丹有一个团队外出活动,给每个人的纪念品是一只小橡胶鸭子,每个人的鸭子都是量身定制的。
Natalie Pistunovich: 哇哦。
Mat Ryer: 是啊,这是个非常贴心的礼物... 而且在你提到的"橡胶鸭调试"中非常有用,Natalie。
Matan Peled: 听起来很有心意。
Mat Ryer: 没错,它确实能让程序员变得更好。我不知道为什么---
大学里会教这个吗?他们教"橡胶鸭调试"吗?
Matan Peled: 我在辅导学生的时候会教,有时候我帮他们做作业,我基本上让他们把问题解释给我听,而他们在解释的过程中就明白了问题所在。然后我告诉他们,“下次你可以试着对橡胶鸭解释。”
Mat Ryer: [笑] 然后就停在那里,不再解释了。
Natalie Pistunovich: 就在你的接待时间,“这是只橡胶鸭,跟它说话。”
Mat Ryer: 好主意。你可以把它放在办公室里。
Natalie Pistunovich: 我学到的一个关于橡胶鸭的奇怪知识是,如果你真的把它放在有水的地方,比如浴缸里,你要确保它没有那个可以发声的洞,因为这会导致它发霉。
Mat Ryer: 哦,真的吗?为什么?因为水会进去吗?
Natalie Pistunovich: 对。而且通风不好。所以如果你打算把它放在潮湿的地方,确保它不会发出声音。
Mat Ryer: 所以,如果你是个程序员,事情进展不顺,你尝试"橡胶鸭调试",而且你哭得很厉害,记得把眼泪离鸭子远一点... 否则它会发霉。
Natalie Pistunovich: 这是第十步。
Mat Ryer: 对。
Natalie Pistunovich: 我能想到还有几件事你应该先做... 但这可以放在扩展列表里。
Mat Ryer: 对,好点子。纸巾,吹风机... 有很多方法---
Natalie Pistunovich: [笑]
Matan Peled: 散步也是一种好方法。
Mat Ryer: 哦,对,那确实是个好技巧。如果你卡住了,出去散散步,或者想想别的事情。多少次你听到人们说,在洗澡时,甚至是在睡梦中,“我解决了这个问题。” 休息一下很重要,而这往往是反直觉的,因为你觉得必须一直工作才能解决问题,对吗?
Natalie Pistunovich: 是的。
Matan Peled: 是的。通常你会感到有压力... 而这种压力会阻碍你清晰地思考。散步有时很难,因为你需要强迫自己停止思考问题,专注于冥想、散步或大自然... 但这样你的大脑就有机会处理你正在做的事情,可能会产生一些想法。
Natalie Pistunovich: 我在想如何把这个话题和我们今天的主题联系起来,但目前还没找到一个好的方式... 所以我们就不做过渡了,Matan,你是以色列理工学院的博士候选人,你研究的是元编程和静态分析。你曾在各种有趣的公司工作过,现在又回到了学术界... 跟我们说说,你学了什么,之后做了什么,为什么在进入行业后又回到学术界继续学习?
Matan Peled: 我本科学的是计算机工程,因为我有一种(可以说是)浪漫的想法,我应该从物理到软件都了解计算机的工作原理...
Mat Ryer: 哇哦。
Matan Peled: 这非常酷,我学到了很多,我非常喜欢我的学位,但这也让它比其他课程更难,因为我是程序员,在开始学习之前我就已经做软件工程了,所以我对这方面很擅长。而加入一些完全新鲜且不同的东西,比如电气工程、所有的电路和物理学之类的,让它变得更加艰难。但我仍然非常喜欢,也觉得自己学到了很多。完成学位后,我在朋友们创办的初创公司工作了一段时间,然后我决定需要在大公司体验一下生活,于是去了谷歌,做了一段时间的搜索功能... 但最终,我觉得做一个“代码猴子”很有趣,我也喜欢,但我也想体验一下研究,看看学术界的生活是什么样的,研究生学位里都在做些什么...
所以我回去读了硕士,完成后现在在读博士。我很喜欢这种生活。我不确定是否会一直做下去,但目前来说,这很有趣... 我可以在自己的“沙盒”里玩,建造沙堡,尝试自己的点子,这是我喜欢学术的地方。
Natalie Pistunovich: 你提到了“代码猴子”,我现在不得不开个玩笑,因为我还没从之前不太顺畅的过渡中脱身出来... 你试过做“代码地鼠(gopher)”吗?
Mat Ryer: [笑]
Natalie Pistunovich: 有趣。那么你硕士的研究课题是什么?
Matan Peled: 我的硕士研究是设计一种编程语言。我研究的领域是编程语言设计。我设计的编程语言是为幻灯片制作的,我的目标是添加动画,基本上是设计一种语言来描述屏幕上的动画和运动。
我很喜欢这个项目,做起来很有趣,但说实话,它的主要目的是让我获得硕士学位,没能成为特别实用的系统。它有一些非常酷的想法... 比如,我的想法是可以通过基本部分构建一个大的动画,如果我想在动画中间做出修改,任何用过PowerPoint做动画的人都知道,制作动画没问题,你可以添加动画,但如果你想在中间插入一些东西,那么基本上你得从头开始,把所有东西重新同步。而我想做的是,能够告诉系统“在中间加点东西,然后根据这个变化重新计算一切。”
所以我有几个有趣的想法,比如把它做成一个弹簧物理系统,你可以在中间加一个弹簧,所有的东西都根据你做的改变作出反应。而且它确实起作用了,效果不错,但它并不是一个生产级系统,让我们这么说吧。
Natalie Pistunovich: 因为它没有测试。
Matan Peled: 其实它有测试。只不过使用它的人必须跟我当时的思维完全一致... 还要是我写它时的特定思维。
Mat Ryer: 那对你来说有多重要?你提到你喜欢在沙盒里玩,建沙堡... 那这些东西和实际应用之间的联系有多紧密?
Matan Peled: 嗯,这是个好问题。我的想法是,我希望我的研究能够回归到一些实际和有用的东西上... 但如果没有也没关系。我也愿意经常去探索未知,去发现一些东西... 如果最终这些东西没有影响力,或者没有带来经济回报,那也可以。理想情况下,我梦想着能创造出一些每个人都知道、使用且有用的重要东西... 但如果不是这样,我也能接受。基本上就是敢于冒险,不知道结果会怎样。
Mat Ryer: 这有点像创业的态度... 能够接受失败,并且拥有失败的自由,这其实很重要。这几乎给了你额外的许可去做一些其他人可能没有机会去做的事情。
Matan Peled: 是的。在某种意义上,我认为创业文化是关于小而灵活的,能够把自己推向一个大公司不能轻易进入的利基市场,因为他们没有足够的灵活性去这样思考,或者为那件事分配资源... 但另一方面,我认为创业公司总是想着最小可行产品(MVP),想着做出能用的东西和产品,而在学术界,你要做的是写论文。你需要有实验、结果、数据,然后告诉其他学者。通常,为了让别人知道你在做什么,你并不需要建一个完整的产品。你不需要有用户,也不需要做任何这些事情。你只需要做出你的特定实验,并把它写得足够清晰有趣,让别人觉得有意思,那就够了。
Natalie Pistunovich: 这听起来很像在博客上写有趣的项目。
Matan Peled: 这是升级版的博客写作,是加了很多形式化的博客写作。这也是为什么现在有科学家写博客,这是一个趋势。学者们通常喜欢写作;这是他们日常生活中的一部分,他们通常也擅长写作。所以他们写博客。比如说,学术推特也是一个现象。
Mat Ryer: 哦,我猜那一定很有趣。真的好吗?
Matan Peled: 是的,如果你对这类东西感兴趣的话。
Natalie Pistunovich: 你有在推特上写静态分析工具的内容吗?或者静态分析的相关内容。等等,也许我们可以先从解释什么是静态分析开始...
Matan Peled: 静态分析,基本上我们想做的是找出代码的一些特性。一个特性可以是最基本的,比如“这个程序有错误吗?”或者“这个程序能成功吗?”我们想要在不实际运行程序的情况下找到这些答案,因为运行程序可能会有副作用;它可能会做一些我们现在不想做的事情。它可能需要很长时间... 我们现在就是不想运行它。程序可能还没有完成。我们不能运行它,但我们仍然想知道一些关于它的信息。
所以静态分析可以是从“这个函数在哪里被调用”,到“这个程序的风格是否正确?”这种问题。对于Go语言来说,风格问题不算太大,因为我们有go fmt
,它也算是一种静态分析工具。但其他语言有像linter这样的工具,它会告诉你“这里的缩进不对”,这也是常见的问题。
但静态分析也可以是重构的一部分,比如你想重命名一个方法,你希望IDE能找到所有对这个方法的调用,并使用静态分析来完成这个任务。
Natalie Pistunovich: 是的,右键单击重构。
Matan Peled: 没错。
Mat Ryer: 静态分析在严格类型的语言中是不是更容易做?相比那些动态语言,它们有很多运行时的元素... 是这样吗?
Matan Peled: 哦,是的,肯定的,因为类型检查也是一种静态分析。基本上,通过提供类型,你给了工具更多的信息,它可以利用这些信息。而如果它有更多的信息,它就可以做更多的事情。
计算机科学的一个基本真理是静态分析是不可能的。你有停机问题,这是Alan Turing早在很久以前就证明的,基本上他说你不能写一个程序来判断另一个程序是否会终止。这个证明很酷,因为他基本上是说“如果我有一个程序能够做到这一点,并且它能把自己放进去,那就会导致逻辑上的矛盾,所以我们不可能有这样的程序。”
由此也引出了Rice定理,它说明你不能证明任何有趣的(非平凡的)程序特性。所以你有一个强有力的理论基础,说明你无法做到这一点,但与此同时,你有一个丰富的科学领域,我们每天都在做这个。它并不是问题,只是发现现实中的那些有趣的程序,比如软件工程师们写的程序---
它们足够简单,我们可以分析它们。但这个定理的意义在于我们不能百分之百确定。我们必须做出某种让步。我们必须接受某些程序在某些情况下是无法分析的。所以对于每种语言,如果你做了足够疯狂的事情---
比如通过名字引用方法而不是直接调用它,叫什么来着?
(译者注: 莱斯定理(Rice's theorem)是可计算性理论中的一条定理,由亨利·戈登·莱斯于1953年提出。定理指出,递归可枚举语言的所有非平凡(nontrival)性质都是不可判定的。“非平凡”是指,仅被部分递归可枚举语言具有的特性)
Mat Ryer: 反射。
Matan Peled: 对,反射。如果你用足够多的反射操作,或者在C语言中做足够多的指针技巧,你总能让它混乱到无法工作。但这没关系,因为对于90%的程序来说,它是有效的,而这通常已经足够好了。我们在谈论的是静态分析,而不是验证。
Natalie Pistunovich: 那这个领域和你的研究有什么关系呢?或者说你的研究是什么?
Matan Peled: 我在做博士研究时想做的是用静态分析进行元编程。说到元编程,我的意思是代码生成代码,或者代码修改代码,基本上就是重构。重构通常意味着你修改代码,然后在修改后的版本上继续工作,但你也可以有一个编译步骤,它改变代码,而你从未在修改后的代码上工作。所以这就是我所说的元编程---
所有这些让代码修改代码的东西,模板,甚至可能包括泛型,诸如此类的东西。
我认为的是,让这些东西变得有意识,使用静态分析信息可以让它们更强大、更高效。举个例子,我的其中一个初期例子是实现响应式编程。假设我有一个类,这个类有一个字段,并且有一个getter,我想要的是每次这个字段改变时它都发送一个事件给我。但这个类并不是这样写的;编写它的人只是写了一个getter,你需要调用它。而我想做的是找到程序中所有可能改变这个字段的地方,每当它发生变化时,我希望它发送一个事件,这样我就可以知道什么时候发生了变化,从而让它变得响应式。
所以如果你能进行静态分析并基于此修改程序,那么你就可以轻松实现这一点。而这基本上就是我的目标---
我希望能够启用这样的功能,并希望以一种(可以说是)声明式的方式,使用基本的构建块来构建更复杂的行为。
Mat Ryer: 听起来很有趣。我见过的一些静态分析的例子---
因为你提到了很多... 我之前甚至没有考虑过像格式化工具也是其中之一。但当然了,这很有道理。go fmt
工具的一个缺点是如果程序不正确,它就无法工作。程序必须是---
Matan Peled: 结构良好的。
Mat Ryer: 没错,谢谢。它必须是结构良好的。所以任何能在程序不完全正确的情况下进行静态分析的工具,我都觉得非常了不起... 因为它经常依赖与编译时分析程序的包来进行静态分析。对吗?
Matan Peled: 是的。处理那些部分正确的,或者甚至是部分完成的代码---
它们不是不正确的,只是缺少了一些东西,而你只想处理那些好的部分,是很难的。
我目前正在进行的另一个项目与伪代码有关。我们想做的是比较伪代码与实际代码,看看它们是否匹配。这有点类似的想法,因为伪代码显然没有完美的语法。
Mat Ryer: 我偶尔会用GitHub Copilot,它实际上做得相当不错... 代码可能是错误的。事实上,你可以通过写注释,或者仅仅通过你使用的函数名和变量名来给它上下文。所以这确实感觉很神奇。我猜这有点不同,因为我想这是机器学习在处理这些工作,对吧?
Matan Peled: 是的。所以ML for PL,或者你想怎么称呼把机器学习应用于代码的领域---
这很有趣,因为一方面,代码... 很多用于这里的技术来自于自然语言处理(NLP),这显然是合乎逻辑的,因为这是文本,那也是文本,你不会用图像处理的技术;那完全不相关。
Mat Ryer: 对。
Matan Peled: 但另一方面,代码是非常结构化的,它非常有层次性,它有各种属性... 为了编译,它必须在各方面非常严格。所以放弃所有这些信息、上下文是愚蠢的。你确实想要利用它,而(让我们称之为)非机器学习的方法来处理代码的方式叫做形式化方法,它基本上是从逻辑和数学相关领域中借用想法,并将它们应用于代码。这也是所有类型检查等概念的理论基础。
我并不完全了解Copilot是如何工作的。我读过他们的白皮书,非常有趣... 一方面,机器学习的一个特点是它们并不做任何特定的事情,它们不会说“哦,看,这里有一个类型。” 它们希望机器学习能够自己学会这些...
Mat Ryer: 是的。
Matan Peled: ...但另一方面,我认为他们确实非常努力地确保算法能够访问类型信息之类的东西。
Mat Ryer: 是的,有趣的是,它有时能正确猜出一些非常令人惊讶的东西,但也会犯一些连简单的静态分析工具都不会犯的错误。它依然会犯这些错误,我相信他们会继续改进。就像在它之后进行另一个额外的检查,看看这些代码是否有效一样。
有时候确实会让人沮丧,因为它会猜测方法的参数,但这些参数是错误的。它们看起来像是它以前见过的东西,但却不是该方法的参数... 所以仅仅一个简单的检查就能发现这个问题... 我想他们会做到这一点。但这确实很有趣,从它犯的那些错误中,你能看到它到底在做什么的线索。但我不得不说,整体上它还是很惊人的。
Matan Peled: 是的,而它从哪里学到的代码,以及它为什么会得出这样的答案,也是很有趣的,对吧?
Mat Ryer: 是的。
Natalie Pistunovich: 是的,实际上正是这个我想提到的,所有这些AI生成代码的内容... Copilot是基于Codex[5]的。这是它背后的引擎。从幕后看,Copilot的插件会收集一些上下文,这些上下文是不为人知的。这有点像它的“秘密酱料”。这些上下文会与一些额外的指令一起被发送到Codex引擎,这可能就是你读到的那篇文章,Matan。有时候你可以看到,因为它收集了错误的上下文,它提供了一些过去的东西,但这些并不适用于你的代码... 就像你说的,Mat,关于那个函数签名,它本可以很容易地被捕捉到... 这确实很有道理,下一步这种工具的发展方向应该是创建静态甚至动态检查器。
但毫无疑问,不需要一个完整的工作程序就能够运行这种测试,是下一步发展的一个巨大进展,所以这确实非常有趣。
Mat Ryer: 对了,当我第一次听说Copilot时,不知道为什么,我把它读成了“Copy Lot”,像是“复制很多”... 我以为它像Camelot一样... 所以我完全误解了它是“Co-Pilot(副驾驶)”。我在预览版里用了很久,还以为它叫“Copy Lot”。[笑声] 感觉挺不错的。
Matan Peled: 在我读硕士期间,我曾在一家实习的公司工作过一个暑假,他们曾叫做xx。后来他们与Tabnine合并,或者说被Tabnine收购,最终成为了Tabnine。他们正在开发一个与Copilot非常相似的工具。他们的想法基本相同,但我认为他们的算法在幕后比Copilot要少一些盲点。也就是说,你不能像Copilot的宣传那样,例如写一些函数的文档,工具就能帮你自动完成整个函数;但它可以做一些类似的事情,比如你开始一个数据库连接,它就能完成所有的模板代码,类似这种基于它见过的其他例子。而且它确实使用了更多的类型信息、名称等内容。
Mat Ryer: 这真是太聪明了。我见过的一个静态分析的例子让我很惊讶,甚至让我对它非常感兴趣。这个例子是,如果程序中的某个变量在某一时刻被命名为密码(password),然后在程序的另一处被输出日志,那么静态分析会发出警告,说“看,这个变量,无论它现在叫什么,它曾经是一个密码,现在被打印出来了。”这种分析让我觉得非常有趣,因为它非常有用。
Matan Peled: 是的。如果你还记得,Perl语言有一个理念,你需要对输入进行清理和处理。他们有一个观点是,如果你不小心使用输入,它可能会影响程序,比如SQL注入等问题。
这种分析叫做污染分析(taint analysis),我认为近年来这种分析变得越来越重要,因为它可以防止密码泄露。还有一个相关但不同的想法是,开发者有时会把密钥直接放进他们的GitHub仓库。这也是静态分析可以找到的问题,提示你应该将密钥放在环境变量的特定位置。
Mat Ryer: 是的,我觉得如果能够在早期发现这些问题就太好了。我们之前提到,程序不需要完成就能进行分析。这正是你需要这种分析的时候,因为在那个时候你可能会做出一些设计决定,而这些决定可能会伴随你很长时间。所以这确实令人兴奋。你能分享一些其他类似的有趣用例或功能吗?
Matan Peled: 基本上,你可以在早期发现错误。其他编程语言,比如Rust,在语言本身就内置了非常严格的检查和静态分析机制。Rust可以确保指针不会在超出作用域后还被使用,因为语言本身会跟踪这些。
其他你可以做的事情包括,如果你在进行多线程编程,使用了互斥锁或其他锁,你可以用静态分析来确保每个锁都正确地加锁和解锁,不会出现解锁前没有加锁的情况。如果你在使用像C或C++这样需要手动分配内存的语言,你可以确保每次内存分配后都正确释放。此外,每次打开的文件也需要正确关闭,这些都可以通过静态分析来进行检查。
有些语言中这些事情是必须要检查的,所以使用静态分析是有意义的,但在其他语言中这些问题可能根本不存在,因为语言本身会负责释放资源。但对于那些需要手动管理资源的语言,静态分析就非常有意义了。当然,内存和文件并不是唯一的资源。如果你通过某种协议与服务器通信,你也可以用静态分析来确保你正确完成了协议的流程。
Natalie Pistunovich: 你对Go语言中的静态检查工具有什么看法?
Matan Peled: 我对Go语言不是特别了解... 从技术上讲,我是一个专业的Go开发者,因为有人曾经付钱让我写过Go代码...
Mat Ryer: 哦...
Natalie Pistunovich: 这就是标准定义了。
Mat Ryer: 对,你是我们中的一员了!
Natalie Pistunovich: [笑]
Matan Peled: 但我不擅长Go。我找到过一个叫Staticcheck的Go静态检查工具,它看起来非常全面。它有很多的代码风格检查选项,可以告诉你Go程序中可能存在的问题。
我们之前谈论过各种静态检查工具,并讨论了不同的层次。Staticcheck大部分做的是代码风格检查,它会寻找某些危险的模式、可能错误的代码,或者那些可能不是你想要实现的东西,然后发出警告。这非常有用。
它似乎也有一些更深入的静态分析功能,因为它可以跟踪各种错误的上下文,分析一些类似的情况... 所以它看起来是一个很棒的工具。
Mat Ryer: 是的,实际上有很多这样的工具,有些是通用的,有些是非常特定的... 比如有一个叫Errorcheck的工具,它会确保你没有忽略任何错误处理... 这是非常重要的一点。
还有Go Meta Linter[6] (译者注: 2019年后就不再更新了),它基本上运行所有的代码风格检查工具,并进行静态检查;正如你所说,大部分都是代码风格检查... 我们会在节目笔记中放一些相关的链接给感兴趣的听众。这些工具已经很好地集成到IDE中了,所以你可能已经在使用它们了。
Matan Peled: 我觉得有趣的是... 我想做静态分析研究,想自己开发一个静态分析工具。代码风格检查工具非常棒,每个人都应该使用它们。事实上,大家可能已经在使用了,因为它们已经集成在IDE中了... 但每个项目都有自己的特殊需求,比如如何使用某个库,如何使用某个API。而我希望每个人都能定义自己的规则集,或者使用某种语言来定义自己的静态分析规则,从而在编写代码时对可能出错的地方发出警告。
对于小项目或者脚本,你可能不需要这种工具。但如果你需要与多个人合作,或者是一个公司项目或开源项目,那么这些工具就显得非常有意义了。
Natalie Pistunovich: 在你见过的任何语言中,有哪些静态检查工具让你印象深刻?
Matan Peled: 哦...
Natalie Pistunovich: 有哪些功能让你特别喜欢?
Matan Peled: 嗯...
Natalie Pistunovich: 如果你要构建一个静态检查工具,它会有什么功能?
Matan Peled: 静态分析中有一件比较难的事情---
它不是一个工具本身,而是实现某种功能的方式,叫做指向分析[7](points-to analysis)。即使在没有指针的语言中,你通常也有引用,这意味着一个东西引用了另一个东西,而这个东西可能会在程序执行过程中发生变化。跟踪某个内存对象的别名是非常困难的,无论是在编写代码时保持程序的心智模型,还是在调试时搞清楚“这个现在指向哪里”,甚至在进行静态分析时,这些都很难做到,因为此时你还没有运行程序。
Mat Ryer: 嗯。
Matan Peled: 如果你知道当前这个变量指向哪里,它是从哪里分配的,它的动态类型是什么,它实际上是什么东西,那么你可以让其他静态分析更强大,因为现在工具知道了更多的信息。它可以知道“哦,这是一个指针,我现在知道它是从哪里来的。”
所以我觉得指向分析是非常酷的静态分析,它真的很难做,因为程序员可以随心所欲地编写代码,而你需要在这种混乱中找到一些约束... 但这就是我想做得更好的地方。
Mat Ryer: 是的,这个问题在运行时解决起来可能更容易。
Matan Peled: 当然。在运行时你就能直接知道它是什么了,不需要检查。在运行时你会遇到其他问题。比如假设你跟踪了程序,现在你有一个巨大的跟踪文件,记录了所有的操作流程,你仍然需要整理这个跟踪信息,找到正确的路径... 因为通常在调试时,你看到的是“好吧,我这里有一个值,它是怎么到这儿的?” 你发现错误的地方通常不是错误发生的地方。你真正想知道的是这个值所经历的所有操作,直到它变成这个明显错误的状态。而这很难做到。不过有一个工具叫rr,它基本上是一个反向调试器[8]...
Mat Ryer: 它能把bug引入你的程序吗?什么是反向调试器?
Natalie Pistunovich: 听起来像模糊测试。
Matan Peled: 正向调试器是指你每次执行“下一步”操作,它会向前执行一条指令,对吧?
Mat Ryer: 哦,对,确实。
Matan Peled: 而反向调试器允许你后退,回到之前的执行状态。
Mat Ryer: 哦,时光旅行的调试器。
Matan Peled: 没错。
Mat Ryer: 这听起来太棒了。
Matan Peled: 是的,确实非常神奇。这是一项非常了不起的工程。
Mat Ryer: 但是它是每时每刻都保存状态的快照,还是有更智能的方式?因为有些操作会丢失信息,对吧?它是如何倒退到过去的?这是“时光旅行”吗?
Matan Peled: 基本上,它就是这样做的。它会在每个操作点保存操作。不过,你得维护很多记录才能做到这一点... 因为显然,你不能在每个机器操作码后都保存状态,那样很快就会导致爆炸式的增长。而程序还会做其他事情,比如输出到屏幕、写入套接字等。所以你必须非常聪明地保存这些信息。
所以它的基本思路是保存快照,但并不是在每个操作点后保存,只是在输入或输出前保存。它认为剩下的部分可以通过这些快照来计算出来。
Mat Ryer: 明白了。这样听起来确实很酷。我在想Go语言是否也有这样的工具。我从来没听说过Go有这种工具,但也许有。
Matan Peled: 也许它可以在Go上运行,因为... 我不确定,我们可以查一下。
Natalie Pistunovich: 大家都开始搜索了。
Mat Ryer: 是的,我觉得有可能。我找到了一个叫“使用Mozilla rr调试不稳定的Go测试”的文章。
Matan Peled: 是的,因为它在汇编级别工作。它关心的是机器操作码。如果代码编译成机器操作码,那么它就可以工作。
Mat Ryer: 哦,这太棒了。
Matan Peled: 是的,虽然它很难使用,而且你得用的是类似GDB的调试器界面,这并不是最用户友好的界面... 但它确实能完成它承诺的工作,这很酷。
Mat Ryer: 这倒是一个好机会,某人可以开发一个工具,或者把它集成到IDE中,因为它目前只是一个文本界面。
Matan Peled: 是的,没错。我相信JetBrains或者其他公司正在做这个事情。
Mat Ryer: 其他IDE也是可以的...
Matan Peled: 这是事实。
Mat Ryer: 我只是为了法律原因必须这么说。其实我不需要,但我还是说了。我猜他们确实在做。
Natalie Pistunovich: 此外,如果有人在寻找一个有趣的演讲题目,用于即将到来的会议,我觉得这个话题我从来没听过。
Mat Ryer: 是的,我也非常想听一场关于这些内容的演讲。如果Matan不打算做,那就应该找别人来做。
Matan Peled: 是的。那么我们来聊聊动态分析吧。基本上,静态分析是在你运行程序之前帮助你,它可以帮助你发现bug,帮助你回答关于程序的问题,比如程序是如何工作的,或者回答一些查询的问题,查找程序中的内容... 如果你有足够多的代码,光是搜索它就已经是一项任务了。
Mat Ryer: 对。
Matan Peled: 但动态分析依然是一项艰巨的任务。你可以使用编译时用到的相同信息,但现在你有了所有实时的数值,所以你可以不用进行符号执行来推测数值可能是什么,因为你实际上知道了它们是什么。但你仍然需要跟踪它们。
在某些情况下,你只需到处打印日志,然后你就搞清楚了,你看着程序的输出,然后就满意了,问题解决了。但有时候打印日志是不够的。如果你在做无服务器编程,比如使用Amazon Lambda,你写的只是单个函数,然后将它们连接起来,没有地方可以打印日志,你不知道它们什么时候运行,也不知道它们会如何运行,但如果你把它们正确地组合起来,或许你可以得到一个跟踪记录,然后你可以用这个跟踪记录来发现“哦,我从数据库中得到了一个错误的值,然后它经过了11个不同的lambda函数,最后到达这里,这就是我的错误来源。” 把这些都串联起来绝非易事,你基本上需要构建一个工具来做到这一点。
Natalie Pistunovich: 或者听上一期的节目,Mat在里面讲到了插桩(instrumenting)。
Mat Ryer: 是的。
Matan Peled: 是的,插桩就像动态分析,对吧?你是在观察发生了什么。
Mat Ryer: 是的,这就是你在大规模运行时需要的东西。当然,这和单独的代码或单个程序的情况不同。但是的... 所以简单的打印输出---
这也算是动态分析,对吧?
Matan Peled: 是的,这是一种非常原始的动态分析... 并且没有借助工具来增强它,可以这么说。而我们作为程序员所做的事情就是开发工具来让我们的工作变得更轻松,对吧?
Mat Ryer: 是的,完全正确。这确实很有趣。我们甚至尝试用结构化日志(structured logging)来做到这一点。现在我们把结构化数据放进日志输出中,以便将来能更好地使用这些信息。
Matan Peled: 是的,而结构化日志几乎就像一个跟踪记录。
Natalie Pistunovich: 这确实是一个有趣的视角。与其说“我只是在打印东西”,你可以说“我现在是在做动态调试”。
Mat Ryer: 是的。“我正在进行动态分析。” 你会说Hello World是一个动态分析程序吗?它不就是这样吗?这可能是最简单的例子...
Natalie Pistunovich: 它给了你什么信息呢?
Mat Ryer: 它告诉你“你好”。
Matan Peled: 我猜是的... 它只是打印---
它告诉你什么时候进入了函数,什么时候离开了函数,这就像是一个跟踪记录,对吧?
Mat Ryer: 是的,这确实是一个小信号,不是吗?
Matan Peled: 是的。
Mat Ryer: 是的,没错。
Natalie Pistunovich: 你可以加上时间戳,那样就像一个正式的日志了。
Mat Ryer: 是的。如果你使用Go的日志功能… 如果你使用 log.Println
或类似的函数,Go会自动给你加上时间戳。你可以在工作中使用Go,因为Go的一个好处是它是开源语言。你可以使用Go工具链本身用来理解Go的所有包。
现在Go语言实际上是用Go写的。说到元编程(meta programming)... Go以前是用C语言写的,现在是用Go写的。我迫不及待地想看到某一天这一最初的信息被遗忘,后代只知道Go是用Go写的,但没人知道它是怎么做到的... 我非常喜欢这个想法。
Natalie Pistunovich: 是的,我们可以用rr回到过去...
Mat Ryer: 是的。 [笑]
Matan Peled: 等等... 你一提到这个,我必须跟你们讲一件我觉得最酷的事之一... 那就是一个叫“信任信任的反思”(Reflections on Trusting Trust)[9] 的演讲,我想是Brian Kernighan的演讲...
Natalie Pistunovich: 演讲链接也会放在节目笔记里...
Matan Peled: 抱歉,是Ken Thompson。这是Ken Thompson的演讲。你知道的,编译器开发者通常喜欢用他们编译的语言来编写编译器。这就叫做自托管语言(self-hosting language),这基本上是一个编程语言的里程碑,因为它意味着该语言足够复杂,能够为自己编写编译器。而编写编译器是计算机科学中一个经典的复杂性问题。
“信任信任的反思”背后的想法是,C编译器是用C语言编写的,并且它能够编译自己。所以,如果你在编译器中植入了一个后门,例如每次它试图编译一个日志程序时,它都会加入一个后门,允许输入一个未知的用户名和密码,那么这个编译器就会在代码中插入一个后门,而这个后门并不会出现在日志程序的源代码中。这样做还不够,因为编译器的源代码会显示它在做这些事情,对吧?所以你不能这样做。
你可以做的是,在编译器中再添加另一个后门,当它编译自己时,它就会为自己加入一个后门,这个后门不仅会添加前面提到的日志程序后门,还会为自己添加一个后门。这样你就会有一个基本上无法被检测到的后门,除非有人特别喜欢阅读编译后的汇编语言;而且不是手写带注释的汇编语言,而是纯粹的编译器输出... 这个后门不会出现在任何源代码中,它只会出现在二进制文件中... 你不能通过重新编译编译器来去除它,因为它会不断地加入这个后门。
Mat Ryer: 哇,太棒了。这真是让人毛骨悚然。
Matan Peled: 是的。
Mat Ryer: 这就像《黑镜》里的剧情,真的,或者类似的东西。
Matan Peled: 对。
Natalie Pistunovich: 肯定有某种病毒或黑客软件在使用这种技术... 这看起来已经存在很久了。
Matan Peled: 是的。
Mat Ryer: 他是在1984年写的...
Matan Peled: 是的,这个想法已经存在很久了,以至于已经有了“反信任信任”的措施,基本上你需要用多个编译器来生成一个经过验证的输出,而且还有很多其他的想法来对抗这一点... 不过如果你对这些自我引用的想法感兴趣,或者对一些类似的东西感兴趣...
Mat Ryer: 我确实感兴趣。
Matan Peled: 那么我强烈推荐你读一本叫《哥德尔、艾舍尔、巴赫》(Gödel, Escher, Bach)的书。
Natalie Pistunovich: 绝对推荐,同意。
Mat Ryer: 是的。
Natalie Pistunovich: 这确实是一本非常有趣的书。
Mat Ryer: 我非常喜欢那本书。完全同意。它非常疯狂,但非常好。
Natalie Pistunovich: 如果我们三个人都同意...
Mat Ryer: 是的,没错。这听起来不像是个...
Natalie Pistunovich: [笑]
Mat Ryer: 如果我们三个人都同意...
Natalie Pistunovich: 这并不像是一个不受欢迎的观点。
Mat Ryer: 对!
(节目音乐)
Natalie Pistunovich: 你不得不承认,这次的音乐很顺滑。
Mat Ryer: 太棒了。这是迄今为止最棒的一次。
Natalie Pistunovich: 这也算是一个受欢迎的观点。好了,Matan,我们在准备这集节目时,要求你提出一个不受欢迎的观点,它可以与Go语言、编程或任何其他话题相关或无关。所以我们准备好了... 你的不受欢迎的观点是什么?
Matan Peled: 我的不受欢迎的观点是,虽然我们刚才大肆赞扬了静态分析及其所能做的一切,但我的不受欢迎的观点是,静态分析实际上并没有那么有效。它在某种程度上是有用的,能解决一些简单的问题,但当你试图让它变得更复杂时,它就会崩溃,最后你只能收拾残局。如果你试图用形式化方法或者机器学习来解决它,无论你怎么做,最终你还是得绞尽脑汁,用意志力去解决问题,而没有任何工具能帮你。
Mat Ryer: 这是你那种学术性的完美追求导致的结果吗?因为你找不到完美的答案?
Matan Peled: 这当然有一部分原因,还有就是我作为研究生反复尝试做一些事情,失败了,然后说“或许这根本行不通。” 是的,所以我的不受欢迎的观点是,软件工程师基本上是有工作保障的,电脑不会取代他们。
Mat Ryer: 哦,这倒是... 但我想这可能会非常受欢迎。我们可以在推特上测试一下。
Matan Peled: 但在我的学术圈子里,这并不受欢迎。
Mat Ryer: 这对你来说确实不太好。完全不利于你。[笑] 你打算辞职去做点别的事情,还是继续坚持?
Matan Peled: 不,我认为我会尽力推动它,看看它能走多远,但要记住,它可能不会无限延伸。
Mat Ryer: 哦,这太棒了。
Natalie Pistunovich: 我觉得我们都将逐渐成为提示工程师(prompt engineers),这可能是下一层的抽象,但也不一定... 我们基本上是在指导AI为我们做事情,其中包括编程... 从技术上讲,这也是一种自然语言,但并不完全是我们每天使用的英语。而且每个人的英语还不太一样... 我们彼此可以理解,但电脑对我们的理解稍有不同。所以这将是编程的下一层。而在某种程度上,我们会通过自动化让自己失业,但在另一方面,我们又会有新的工作。
Mat Ryer: 我们可以只写测试。我觉得即使是模糊测试,也可以使这项工作实现。
Matan Peled: 哦,你这么说,但这确实存在。我有一些朋友正在研究这个领域。它叫做合成(synthesis[10]),基本上你写下程序的规范,测试就是在指定程序应该做什么,然后进行合成... 不管是使用机器学习,还是通过某种非常具体的方式搜索所有可能的程序,它实际上有时可以找到程序... 你可以通过这种方式编程。
如果你想一想Excel,以及你可以用自动填充完成的所有事情,基本上就是这样。你写下你想要的输出,然后拖动它,剩下的它自己就会搞定。特别是随着他们加入的新功能。
Mat Ryer: 哦,是的,我见过。我前几天试了一下,我输入了1, 2, 3,然后它就不断重复1, 2, 3。我当时非常生气,简直气炸了。[笑声] 但你说得对---
如果它在代码中也能做到这一点... 如果它能理解像指向分析(points-to analysis)这样的东西,理解内存使用、性能等内容,它甚至可以优化代码。
Matan Peled: 是的。
Mat Ryer: 它可以给你一个早期版本,然后随着时间的推移不断改进。有时想到它能在大规模运行时,确实让人非常兴奋。
Matan Peled: 所以,我的不受欢迎的观点是,这种情况永远不会发生。它的最佳表现也不过是给你一个大致的方向,比如“你可能想看看这里,这里可能是个不错的地方。” 但它永远无法自己做到这些。它永远不会对程序了解得足够多,无法在没有人介入的情况下完成这些任务。
Natalie Pistunovich: 很有趣。
Mat Ryer: 是啊… 因为即使是我们编写的程序,也会有bug。我曾有一位经理说他不想代码里再有任何bug。
Matan Peled: 那很好办,只要别写代码了。没有代码就没有bug。简单。
Mat Ryer: 对,没错。
Natalie Pistunovich: 无代码。无代码才是未来。
Mat Ryer: 这话确实有一定道理,但认真来说... 是的,问题在于程序是否按照我们外部设定的标准去做我们想要的事情。所以实际上程序并不知道这些标准。但我想知道,你能否写一个测试...?
Matan Peled: 但是即便是测试也无法完全描述程序,对吧?我们都知道写测试是很难的。
Mat Ryer: 写好的测试确实很难,不是吗?
Matan Peled: 写出能很好地规范程序的测试更难。因为---
好吧,如果你让程序做乘法,它输出2,然后变成4... 那么,“好吧,我可以写一个程序,它总是输出4。” 这看起来是对的,对吧?
Mat Ryer: 嗯嗯。这就是为什么你需要不止一个测试用例。
Matan Peled: 正是如此。
Mat Ryer: 是啊。
Matan Peled: 它几乎就像对抗性的,它总是会找到一种方式,做出你不想让它做的事,而不是你想让它做的事。
Mat Ryer: 是的。而且你想要的东西也会随着时间的推移发生变化。这很有趣。使用静态分析的话---
你能检查测试是否自相矛盾吗?
Matan Peled: 哦,这是个有趣的问题。我想你可以,但这取决于你所说的“自相矛盾”是什么意思。你可以使用静态分析提取这些测试并进行比较,看看... 是的,你绝对可以做到这一点。你可以看看它们对它们正在测试的方法---
如果这些是单元测试,你测试的是一个方法---
它们所描述的是否一致。如果它能想到一个方法输出某个特定值。
所以静态分析---
假设你有一个返回整数的方法... 然后我们有各种各样的整数分析可以给出范围。这个输出在0到8之间。给它一个区间。这是区间分析。还有更复杂的整数分析,试图弄清楚这个值可能是什么... 是的,它绝对能找出某个地方是否有矛盾。
Mat Ryer: 是的,这确实很有趣。我猜这就是为什么像Rust这样的语言中纯函数更容易处理,相比Go来说。在Go中,方法和函数可能会有副作用。
Matan Peled: 静态分析其实并不在意副作用,因为它并不执行任何东西。如果你在读取输入或类似的东西,显然你不知道它是什么值,也不知道它可能是什么值... 但你只需要将其标记为“any”或“top”,然后继续。是的,它可以是任何值。最终就是这样。把它放进分析中。
Mat Ryer: 嗯,听起来很酷。
Natalie Pistunovich: 好的,大家,这真是非常有趣,尤其是在不受欢迎的观点之后,讨论变得更加有趣了。我已经在想我们下一期节目会谈些什么了。在那之前,非常感谢所有参与的人。祝大家接下来的一天愉快!
#227 Analyzing static analysis: https://changelog.com/gotime/227
[2]Matan Peled: https://github.com/chaosite
[3]Natalie: https://github.com/Pisush
[4]Mat: https://github.com/matryer
[5]Codex: https://openai.com/index/openai-codex/
[6]Go Meta Linter: https://github.com/alecthomas/gometalinter
[7]指向分析: https://anemone.top/pl-%E9%9D%99%E6%80%81%E7%A8%8B%E5%BA%8F%E5%88%86%E6%9E%90%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0%EF%BC%88%E6%8C%87%E9%92%88%E5%88%86%E6%9E%90%EF%BC%89/
[8]rr,它基本上是一个反向调试器: https://smartkeyerror.com/rr-debug
[9]“信任信任的反思”(Reflections on Trusting Trust): https://www.youtube.com/watch?v=SJ7lOus1FzQ
[10]synthesis: https://chhzh123.github.io/blogs/2020-02-01-compilation-and-synthesis/