2020-11-27 00:00:00
Nov 27, 2020用Sous Vide做羊排
疫情受困在家的半年多里,做了很多菜,其中这个羊排使我不得不记录一下。我做菜一般都是看完很多个教程之后凭感觉做的,做完经常自己也忘了怎么做的了,所以此教程并不能保证100%重现照片里的样子,估计样子能重现90%,味道能重现95%以上。
做这道烤羊排需要如下器材:
以及如下调料:
羊排就是Costco买的羊排,推荐买肥一点的,表面脂肪覆盖得比较均匀的。
下图就是我最满意的烤羊排了,可以看到内部有非常多的汁水,吃起来非常的嫩,铸铁锅的高温也让外部的脂肪焦得正好,有种烤羊肉串的焦香。
当然我还用类似方法做过很多别的东西,比如牛排,鸡翅,土豆泥之类的。
最后再夸一下Sous vide,真的很方便,在这里推荐一个workflow:早上起来腌一下肉,抽真空,丢到水箱里,估计十分钟解决。到了午饭时间,从水箱里取出来,拆出来放铸铁锅里煎一下,依然是十分钟解决,非常方便。如果把腌肉,抽真空这个活动放到前一天的晚上,那就更方便了,早上起床只要把肉放进去加热就行了。
搭配Sous vide买的抽真空机也很有用,平时还可以用来真空存储食物,减少食物腐败。
Anyway,这一套东西应该是我近几年买的最有用的东西之一,能够精确地控制温度,让quality control变得更容易,还可以带来一种自己可以更好地控制自己的生活的美好错觉 :)
2020-09-06 00:00:00
Sep 06, 2020我在Google的前三个月
距离上一篇文章已经过去半年了,这么久没有写东西,主要是因为上一篇文章消耗了我太多的精力与感情,主要是感觉没什么时间,倒不是说在Google很忙碌,更多的是之前在学校的时候属于自己的时间太少了,所以在毕业之后的这几个月里,我基本都在做一些之前没时间做的事,包括但不限于陪女朋友睡觉发呆做饭玩游戏等等等等。当然还有给自己两年前在瑞典交换时遇到的教授写信,很惭愧,来美国之后的两年里都没有联系过他。其实经常想到他,可是总是很忙碌,也觉得自己没有做成什么事,没什么值得骄傲的东西可以分享给他。他的回信来得很快,我觉得很开心,因为他说他也过得很好。只不过鉴于目前全球疫情的关系,我们一致认为下次我去欧洲或者他来美国的时候,至少也是2021年年底了。
IT Crowd里面有一集,Jen不在办公室,然后Roy和Moss在办公室疯玩,把他们平时想玩却不敢玩的东西全都玩了一遍。都玩完之后,他们就开始觉得无聊,又开始打电话给Jen,问她什么时候回来。我现在的状态就有点像Roy和Moss疯玩结束之后的状态,有点不知道该做什么,这周五一直到下周一都放假,四天的假期,没有什么期待,也没有什么动力工作。突然想到自己还可以写点东西回顾一下这几个月,那么就稍微写点吧。
我在今年六月初加入Google,主要是做一些程序分析的工作,简单来说就是通过分析程序的行为来识别Android平台上的各种恶意软件,然后及时下架这些恶意软件,从而保护用户的设备安全。程序分析算是一个很有用也很热门的研究主题,不过本科生阶段很少会接触这个,主要还是在研究生阶段提到的比较多,所以从事相关工作的人普遍跟学术界有千丝万缕的联系。我们组接近十个人,我应该是其中唯一一个没有拿到PhD学位的人,平时来的实习生也是各个大学里的博士生。所以我来到这个组,觉得自己很幸运,因为比起做业务的组,这里更像是一个做研究的组,基本可以看作是学校的延伸*。不同之处在于这里的资源很丰富,同事们也都很nice,不存在我在学校里遇到的那种push的环境,而且做得也是真正好的产品,不会像学术界一样,大部分时候是为了展示idea而草草做了一个勉强能用or几乎不能用的prototype。
刚进来的时候就是像发现新大陆一样的开心,Google的内网里有太多可看的东西,自制的toolchain,各种奇奇怪怪的badge,还有很nerd的游戏,经常看着看着就一天过去了。我觉得很有趣的一点是,Google内部几乎都是用自己开发的软件,俗称dogfood,然后Google的所有代码都放在同一个仓库里,对所有全职的软件工程师都是可见的,所以当发现某个软件出现了问题,或者文档写得不清楚学不会怎么用的时候,常常可以通过直接查看源代码的方式来解决问题。Google的代码都经过严格的code review,大部分代码读起来也比较轻松,所以有问题,往往自己看会儿代码就解决了。
*: 微软的CTO Kevin Scott也有差不多的观点,他在我们学校某个教授手下读了五年之后直接去了Google,并且没回来参加dissertation,也就是说,他没拿到PhD学位。在一个采访中,记者问他为什么这么做,他说他那时候其实都已经写好相当一部分dissertation了,虽然他不再那么热衷于拿到这个学位了,但是他还是想完成这个PhD。转折就在他去当时的Google工作了一会儿,他发现在Google工作太有趣了,而且那时候Google有很多project要做,他就在那里一边骗自己说自己会完成这个PhD的,一边又继续在Google工作。搞笑的是,在那时候居然有很多个像他一样的只差dissertation就能拿到PhD学位的人放弃了学位。那么,Google为什么能让他安心呆着呢?Kevin说是“You know, the funny thing about Google in the early 2000s, the first time that I was there was very academic in nature. So it was an easy transition to make from grad school to Google. You know, the environment was sort of engineered to be very, you know, familiar in that way.” 接近20年过去了,我认为我在我们组看到的Google,就跟Kevin描述的没什么两样。↩
说到code review,可以从我的第一个项目开始。我的第一个项目是改内部的程序分析框架,基本上就是给它加一些功能,专业点说就是让call graph更完整的同时,保留灵活性。由于我之前比较熟悉LLVM,内部的框架又设计得很清晰,所以我上手也很快,加了点新pass,改了点现有的pass,问题就解决了。这段时间里code review带给我的印象还是挺深的,在这之前我一直都觉得写代码是一件很私人的事,如果你让我自己在家闷头写代码,然后自己改改,改到满意了再发布出去,我觉得OK;但是如果让我每新写一点代码就要给别人看,这就让当时的我感到有点羞耻,特别是我才刚来不熟悉,很容易写出很傻的代码。我的前几次code review还是比较漫长的,被几个reviewer来回要求改了好几次。不过话说回来,正因为刚来不熟悉,所以code review对我的帮助是极大的,比如reviewer会告诉我这里某个容器已经在某个文件里定义过了,可以直接引用;又或者告诉我这里的代码应该保持跟某个地方的代码保持一致,等等。这样一来二去的,我不得不去读很多相关的代码,这个过程无疑让我更快且有针对性地熟悉了整个code base。在Google大家搞code review是很认真的,基本上除了正确性之外,大家还会提出比如性能,可读性方面的意见,以及新特性的应用,毕竟在Google的各位大多是紧跟技术潮流的 :)
说完了被人review,也可以说一下review别人的经历。前面说到我是组里唯一一个没有拿到PhD学位的人,再加上我又是新来的,那么很明显可以知道我在组里的等级是最低的。等级最低是否就说明没有代码可以review了呢?并不是。我的第二个项目就是和组里一个L5合作的,我的等级是L3,我基本是负责给他的项目加些代码。在我们合作之后,他每次提交代码也是主动发给我review的,我也每次都认真地看他提交的代码,这样一来其实也可以帮助我更快地熟悉他的项目。
在Google,大家还是会努力营造一个平等的氛围的。我一般懒得去看别人的等级,因为看了也没什么帮助,最多满足一下自己的好奇心,大家平等交流就好。再说了,我认为等级代表的,更多的是此人对公司所作出的贡献,而非此人的水平,所以在这里建议大家,不要迷信等级,相信自己。
既然说到了我的第二个项目,我就可以再扯点个人成长的东西。详细点说,我的第二个项目是我的manager带着我,跟另一个组交涉,改进我们组的一个产品,而这个产品是由我们组的一个L5负责的。其实我觉得我的manager完全可以让那个L5直接去跟另一个组交涉,因为那个产品的代码全是他写的,他再熟悉不过了,交流起来很方便。等他们都设计完了,然后直接告诉我该干嘛干嘛,让我去写代码就行了。不过我的manager并没有这么做,事实上,这个项目的design doc基本全是我写的,跟另一个组的交涉也基本是manager带着我参加的。所以我基本就是负责搞清楚我们这边的产品的所有细节,然后跟对方讨论设计细节。这对我这个新来的来说不算容易,但是我很开心能够参与设计,这让我觉得我是这个团队的一部分。
不过话说回来,对于我这种自视甚高的人来说,这种难度的project肯定是让我感到很难受的。事实上,有很多次我的内心状态都跟Anakin是一样的:
我认为我的能力完全qualify下一个等级,但是在Google快速升级并不是没有那么容易的事。想要升职,最好的办法当然是做一个有impact的项目,这个大家都知道,但是如果没有呢?就像现在刚进来的我一样。我的思路基本就是多要跟组里的人交流,问问有没有什么可以帮忙的,比如我就跟我自己的mentor要了好几个bug/feature request(我上面提到的“第一个项目”其实不算是我真正的第一个项目,只是我向mentor要来的feature request而已,但是因为比较复杂,所以我在这里把他算作我的第一个项目)。
但这很明显只是临时策略,让我来说明一下。升职是需要写一个report的,report是要给管升职的人看的,但是这些人并不知道我平时到底在干嘛,所以我就需要在短短一个report里向他们说明我是能胜任下一个等级工作的人。在我看来,说明的最好方式就是在report里讲故事。这个故事要从我为什么要做这个项目讲起,然后说我为了实现这个项目做了哪些事,在做这些事的过程中我遇到了哪些困难,我是如何解决的,最后这个项目结果如何,有什么影响力,给大家带来了哪些好处。这样一来,管升职的人就能清晰且快速地了解我的工作。回到临时策略的问题,如果我只是长期帮人修bug,实现feature,那我就会面临一个严重的问题,我没法讲故事了。因为我的工作散落四方,我很可能无法将他们串成几个流畅的故事,这样一来别人很难了解我的工作,也就很难量化我的工作数量与质量,很难判断我值不值得升职。乱七八糟的bug修得越多,就越难串成故事,意味着边际效应也就越明显。就算我可以把他们串成一个故事,那这个故事会有说服力吗?我的manager告诉我说,写升职report要从三个方面来写:leadership,impact,以及difficulty,让我们来仔细看看。首先帮别人的项目修bug,我没有leadership,最多是被leadership,不行;剩下的impact和difficulty其实是一回事,我在改的是别人的项目,别人会把有impact或者difficult的部分交给我吗?probably not。
这个临时策略我打算就用在刚进公司的第一阶段,这个阶段也被很多人叫做build trust的阶段,很容易理解,一般来说进组之前,大家谁也不认识我,没人会去看简历上写了什么,大家只想知道这个人来了能不能做出贡献。在这个阶段,多帮人修bug还是很有用的。一方面可以熟悉组里的project,熟悉组里的同事;一方面可以切实的帮助组里,显得很有存在感,具体可以选择领一个相对重要但是是因为组里的人没时间做才搁置的bug。这样几个月下来,大家也就可以对我做出信任or不信任的选择。在这段时间里,我需要思考我自己到底想要做什么样的项目。在这之后,就差不多是时候去主动争取属于我自己的项目了,这时候manager也会比较放心地把一个独立的项目交给我。有了属于自己的项目之后…我还没想好怎么办。
我认为在公司里,能够明确地知道自己想要什么,想好要怎么做,并为此付出适当的努力是很有必要的。付出太少是对人类社会规则的背叛,付出太多则是对无产阶级的背叛 :)
进Google之前就听说Google内部的节奏很慢,像养老一样,很多人就是rest and vest。进来之后我亲身感受了一下,感觉慢确实是挺慢的,特别是当下这个时间点,大家都是wfh,所以更感觉到慢了。但是慢背后的原因也还算是可以理解的,一方面就是Google本身提倡work-life balance,经常给大家放假,或者搞no meeting week之类的;另一方面就是,为了维护现有复杂系统的质量,很多的讨论确实就是必要的,赶进度只会让系统的质量越来越低,背上越来越多的tech debt。再说了,这些开会讨论的结果都是有记录的,整合之后会写到design doc里。我最近在写的一个design doc,manager对我的期待就是,把具体要改的代码链接都标到文档上去,我找代码的时候,都是精确到具体哪一行的,所以写的过程中,我基本对要改哪些代码也已经心里有数了。写完design doc之后,剩下写代码的过程确实以体力劳动为主。
其实Google的慢,也可以从Google内部系统的体量去解释。当一个系统到了Google这个体量之后,其复杂度是惊人的,平时看起来简单的改动,在这个系统下都显得不那么简单,因为要考虑的东西太多了。这样一个高质量复杂系统的建立与维护,离不开工程师们的悉心呵护。在现实世界里,这是很难得的,因为人总是倾向于把自己的利益放在第一位的,很多人只想快速地把东西搞出来,赚到钱与名声,以后的问题以后再说,大不了跳槽走人。事实上,Google里也有不少人抱怨的,抱怨为什么节奏这么慢,抱怨为什么公司赚钱的业务这么少,抱怨为什么股票涨不上去,但是幸运的是,至少在我眼里,Google还是在以一个比较一致的姿态在往前进的。有天赋的工程师有很多很多,Google也只是招了一小部分进来,但是为什么只有一个Google,我想这也算是一个原因吧。
总的来说,我很喜欢Google,也很喜欢我自己的组,还挺期待等到疫情得到控制的那天,去到办公室写会儿代码,再跟从来没有在线下见到过的同事们吃个饭,聊聊天,然后在硅谷呆一段时间。
(The End)
2020-03-06 00:00:00
Mar 06, 2020I quitted PhD
做出这个决定大约是在2019年的10月中旬, 从加入到退出,我在PhD这个program里呆了一年时间,这一段经历对我而言,其实就是一段祛魅的过程。所有那些我在加入时有过的情绪,热情、激动,甚至是虔诚,慢慢变成了不解、不安、与痛苦,最后一切都烟消云散,我可以内心毫无波澜地审视这一切。PhD在我心里变成了一个再普通不过的东西,我不再觉得PhD这个title值得我用几年的时间去换取,也不再觉得相对于其他我能做的事,读PhD的过程可以带给我更多的收获。
不得不说,来了美国之后发现这里确实处处洋溢着世界第一的骄傲,比如说,在美国你可以领教到很多world-class的事,所谓的世界级。投简历的时候你会看到,类似于“Join us to do the world-class product”的描述;学生手册上写着“The PhD program prepares students for faculty careers at world-class universities and for research positions in leading government or industrial research labs”;就连夏村的消防车上都写着,“Charlottesville, a world-class city”。第一次面对如此多world-class的时候,我的心中怎么可能没有任何波澜呢?谁不曾想成为world-class的一份子?但是慢慢地我发现事情并没有那么简单。
回想我当初第一次看到学生手册上写的world-class,心潮澎湃,着了魔一般急切地想要投身于“world-class research”然后“expand the frontiers of knowledge”。那时候的我几乎是虔诚地对待这个词,我觉得world-class曾经是对我来说是触不可及的东西,我为自己朝着world-class迈进而感到无比高兴。这种感觉随着我读论文的数量增加,research的进度增长而慢慢消失,放眼望去,密密麻麻的都是重复性的,无聊的,炒冷饭式的工作。由于巨大的失望,最终彻底走到了这种感觉的对立面,我才知道所谓world-class是如此不堪一击。诚然,我们大部分人都耗费了巨大的精力才走到了这一步,走到了world-class的面前。可是如果此时你看见前方的生活并不是你想要的,你会回头吗?
至于夏村消防车上写的world-class,我只当这是当地人的一种幽默,否则的话,那就只能是一种无知式的自豪了。欧洲的那些默默无闻的宁静小镇们,看到这个world-class,估计只会哑然失笑。
world-class,大概是我来美国受的第一次骗。
这一年时间里,我从完全不知道PhD为何物到quit PhD,我的观念发生了很大的变化,比如说PhD这个title在我心里已经失去了价值。我曾经以为每个想读PhD的人都是充满好奇心的,真心实意地想要拓宽人类知识边界的人,但是我发现现实世界里读PhD的一般就两种人,一是不知道自己当下该干嘛的人,于是读个PhD,在学校里继续混着;二是一心想要找教职的人。前者一般天天划水,水平十分低劣;后者往往非常努力,但是目的性也非常之强,他们精确地知道想要找到一份教职需要多少顶会paper,我尊重他们的努力,不过也仅此而已。
为什么说PhD在我心里已经失去了价值,因为我自己在做research的过程中,见到了太多的所谓划水PhD,实在令我感到不适。所以现在的我觉得PhD这个title并不能说明什么,如果一定要说明什么,那就是我知道这个人八成在学校里呆了好几年 :) 读过PhD的人都知道,PhD们之间的差距可以大到令人咂舌,我们只能通过他在PhD期间完成的工作来真正说明这个头衔的含金量。可惜的是,由于这世界上的大部分人都不是PhD,他们不懂得如何分辨一个好的PhD和坏的PhD*;又由于PhD在这个世界上的稀少程度,人们往往愿意盲目地奉上较好的待遇。单凭PhD的title就能得到良好待遇的事实,吸引了大批只对该title感兴趣的人涌了进来,同时也让一大堆已经了解自己不适合读PhD的学生赖在这个项目里不肯离开,因为他们渴求这个title。很多人就像爱因斯坦曾提到的,他们是“为了纯粹功利的目的而把他们的脑力产物奉献到祭坛上的”1。
我不知道是谁第一个把做research叫做搬砖的,但我不得不说这实在是天才般的比喻,PhD们日复一日,为了发paper而重复着微妙的实验。在这过程中,某些搬砖技巧不足,或者搬砖不够努力的人就会被请出这个项目;另一些人则非常努力的搬砖,不过与此同时有些人(比如我)会开始怀疑这一切的意义与动机。每个教授都会说自己是想要拓宽人类知识的边界的,PhD们也都是这么被告知的。一开始我也确实觉得research是关于一心为了拓宽人类知识边界的,后来我发现并不是;于是我退而求其次,只想做个老板眼中的“成功”PhD,但我又发现所谓“成功”PhD的终极意义居然就是找到一个好教职并继续做research,于是我开始问自己我是否真的想成为一个faculty。我想了想觉得没什么兴趣,比起一个做research的faculty,我宁愿去专心教本科生上课,我觉得那会带给我更多快乐。第二个学期的时候,我做了本科生Operating Systems这门课的TA,当时这门课分成了两个section,一个section是我老板上的,另一个section是C上的。一开始我不知道为什么C一个Berkeley来的,PhD期间research搞得风生水起的人,会来这里专心教书。但是渐渐我发现他对于教书的热情真的非常动人,这门OS是直接改的MIT的OS课,降低了一点难度又没有失去精髓;Piazza上有学生提出问题,他总是会及时地出现。有次半夜两点有学生问问题,我正在编辑回答的时候,突然发现怎么有个人在co-editing,仔细一看原来是C;每周开会C都会认真记下TA们的反馈,讨论课程的改进,其中有一个作业是实现ftp服务器,他一直期待大家提出一个更好的protocol让学生去实现,因为他始终觉得ftp设计得太垃圾了。刚才我看了一眼这个学期课程表,发现这学期OS两个section一共有两百多个学生,lecturer却只有他一个,也许是他觉得其他人教得都不如他吧。在我眼里,C的人生比他的大多数同事们都更有意义。
发现自己对faculty没兴趣之后,于是很自然地,我决定退出。说到底,一个“成功”PhD所需要的品质,比如头脑聪明,比如有想法,比如耐得住寂寞,仔细想想,这些品质放在任何行业和领域,都是非常宝贵的品质。对我来说,与其耗费好几年青春换得一个我没兴趣的职位(faculty)的入场券,不如带着这些品质去探索更大的世界。
有人说,PhD的过程是一种筛选,而这种筛选“总是筛掉最差的,但也筛掉最好的”2,其实有一定的道理。
*: 从某种意义上来说,这也是由如今research的方式决定的——大部分人都只在一个很小的细分领域内深耕,researcher们互相并不非常清楚彼此的research,而CS这个学科中又恰好充满了各种各样的细分领域。假设一个学校想要招生,很有可能学校根本没有那个细分领域的专家,评估新人的教授只能从侧面来考察这个候选人。不过从侧面考察这件事就很微妙了,说句实话,PhD这么多年读出来,不管自己的工作是不是真正重要,也能将自己的工作吹出花来了,说不定还早就给自己洗脑,相信自己的工作一定无比重要。毕竟如果自己都觉得自己的工作不重要,估计也就不愿意把自己的美好青春放在这些工作上了,早就半路quit了。↩
老板与学生的关系向来都很微妙,尽管学生手册里写的是,“RAs and advisors are colleagues in research and the employer-employee relationship is rarely visible as they work together to expand the frontiers of knowledge”,但是事实并非如此。我的老板就经常把读PhD比做一场战争,而我则是其中的一个soldier :)
读PhD的几乎每一天我都过得很郁闷,因为在我眼里,我的身份更像是一个worker,一个employee,或者一个soldier,这让我感到迷惑且痛苦。而且我无法忍受某些话术,比如
“我给了你这个project,你就应该尽力去实现它,如果你觉得我错了,那你就指出来,否则就按我说的做”
这个话术的精髓在于,引入一个看似正确的逻辑(if x then y else z)让学生闭嘴并且去干活,但是却闭口不提这个结果(y)所需要的苛刻的前置条件(x)。有哪个刚入学的PhD可以随随便便地指出老板的错误呢?如果不能指出错误,那么就只剩下了服从,在脑子一片迷茫的情况下服从,是我所不能忍受的事。正确的做法应该是真诚地对待学生,与学生建立良好的平等关系,及时地引导学生并解决学生的疑惑。或许有人会说,“你的要求太高了,怎么可能会有这种老板呢”,我觉得这种人就是典型的自我阉割惯了,把自己的要求降低到了一个不正常的维度,于是看到别人提出的正常的要求就会露出一副吓坏了的神色。
“如果你能完成按时这个project,你每天睡12个小时我也不在乎,(对于你现在的困境)唯一的办法就是变得more efficient”
Again,这个话术的精髓也在于忽略达到结果所需要的条件,只提出了看似宽容的每天睡12小时。这样一来,就可以把workload过大的责任消于无形,问题现在出在了你的身上,你牺牲了睡觉的时间娱乐的时间来做这个project,完全是你自己的选择,因为你不够efficient。计划制定者一点错也没有,相反,他很仁慈,在某种实则不可能的条件下,你甚至可以每天睡12个小时*。正确的做法应该是为学生制定合理的计划,如果学生实在达不到要求,那么可以直接提出让学生另寻出路;或者招募更有经验的postdoc来充当老板与学生之间的缓冲。给着卖白菜的钱指望学生出卖白粉的力气是不可取的,指望学生长时间地用爱发电也是不现实的,事实上我真的在很长一段时间里尝试用爱发电,结果是我开始失去动力,开始怀疑我是否还喜欢security。
我可以理解Assistant Prof.往往会面临巨大的来自tenure的压力,但是我不愿意有人将这种压力直接(甚至是加倍)传导到我的身上。
其实大学里也有很多可以制衡老板的机构,但是这些机构一般只能在与学术无关的事情上发挥作用。另外,我也并不相信他们真的会站到我的这一边,而且我并不觉得他们能为我做什么,这是我与老板之间的事。
听到有人说读博和婚姻很类似,我觉得是的,老板与我的关系就能说明很多。如同很多婚姻一样,我们也有一个非常良好的开始。我还记得当初我刚来读Master的第一个学期,天天对着课表发愁,觉得没什么感兴趣的课可以上。就在这时候,他正好作为一名新AP加入,并且开了一门新课。那时候我一看他的履历就知道他的方向和我的兴趣应该是完美契合的,我自然马上选了他的课。相似地,我相信他也肯定感觉到了我的兴趣与实力,我记得那才是那个学期的第一节课还是第二节课,他就开始邀请我读PhD,并且在那个学期剩下的时间里时不时地找我吃饭,劝说我读PhD。我虽然一直犹犹豫豫,始终没有答应,但是我知道我的内心肯定还是希望尝试的。我嘴上说着自己要刷题找实习,但是我花在research上的时间,至少十倍于刷题找实习的时间。那个学期之后,我还是转到了PhD,我以为这是一段佳话的开始,我以为我们会像一个前途无量的startup一样,破土而出,在这个学术界里占据一个角落,留下我们的名字。可惜事与愿违,尽管老板与我之间并没有想象中的剑拔弩张,只是日子一天天过去,我们都慢慢明白彼此不合适。说不可惜是不可能的,我们在技术上互相赏识,当初是他亲手写的推荐信,在春季不招生的情况下让我破格转到PhD,我也一直把他当作榜样。只不过我是一个缺乏安全感的人,他对我的各种要求使我感到烦躁并且失望,而且我们之间也太缺少深入的,与学术无关的谈话了。在这种情况下,我陷入了深深的焦虑,我知道再这样下去我只会变成一台不属于我自己的机器,当我quit之后,我才终于又感觉到,我的命运又被我牢牢地抓在手里了。
说到我与老板之间的相处,让我给你们举一个例子吧。我刚转到PhD的时候,是春季学期,那时候他对我说,如果我们能在暑假结束之前发出两篇顶会文章,那你就可以做到“above average”了。我当时并不知道两篇顶会意味着什么——直到后来我才知道在system领域,有很多PhD花了三年甚至更久才能发出第一篇顶会;直到后来我才发现,隔壁某大牛组的PhD,花了六年拿着两篇顶会文章就毕业了。但是在那个时间点,我选择无条件地相信了他,我只知道在这个时间之内完成两篇顶会,一定是一个很棒的成就。如果你对CS System这个领域有所了解,那你应该已经能猜到我接下来的大部分故事了。在接下来的日子里,我非常努力地工作,也没有时间去看别的PhD在干嘛,当然也就不知道老板给我定下的“above average”的实际难度究竟是多少,我只知道我感觉越来越难。我开始对自己非常失望,我满脑子都是“暑假结束前两篇顶会 = above average”,可是直到第一学期结束前,我还没有完成我的第一篇文章,那我得有多糟糕?而且更让我感到恐惧的是,我对发论文一无所知,我并不知道发一篇顶会需要多少步骤,我也并没有被告知下一步是什么,我只被告知我应该尽快完成我手上的任务。所以我很焦虑,对未来的未知以及无法掌控加剧了我的焦虑,我每天都在挫败感中度过。但是老板不一样,他从一开始就知道我做不到,实际上,就连他自己也做不到。他只是想通过这种方式来push我,让我尽可能多地搬砖罢了。我当然也希望尽可能多地搬砖,可是以这种方式来push我,利用我的恐惧与焦虑,我实在是不能接受,也不觉得这是一种decent的方式。事实上,按照我的性格,从他开始这么做的第一天起,我们的分离就只是时间问题了。暑假结束之后,我投出了我的第一篇文章,也许我已经超过了95%或者更多的PhD,但是那又怎么样呢?长久以来,我都背负着巨大的压力,我的一口气都是被那篇paper吊着,我以为投出去之后一切就会变好,可惜我并没有那种感觉。我只觉得空虚,觉得没有意义,觉得自己应该重新规划自己的人生了。那个时候,我对老板的做事方式耿耿于怀,而如今,quit后的我也已经理解了,我又能改变什么呢?他是真心实意地想要帮助我,希望我成功,也是100%地相信,用那种方式push一个人是对的,是有用的。也许是因为他的成长环境吧,他也对我说过一些他以前的事情。我为他感到难过,也为能理解他的我感到难过。我想把那个令我难过的东西称之为”亚洲人的悲哀“,或者更广义一点,”内卷后遗症“。
*: 这样的话术在生活中其实十分常见,它们往往重点渲染结果,而对苛刻的条件一带而过。比如,前几天一个朋友对我说,“比特币现在没有那么有用,但是如果公链做的很牛逼,那这个代币就值钱了”,这句话本身也许是对的,但是这并不代表你应该投资比特币。↩
上面说到了我的那种失控的焦虑以及命运不掌握在自己手中的感觉,这里我可以详细解释一下。在此之前,我想起两个看似无关的事:
收集信息的能力很重要,两个人想做同一件事,拥有更多信息的人有更大的成功机会,因为这些信息可以帮助这个人制定计划,评估风险,从而更好地把握整体走向。我的信息收集能力让我在大多数时候都掌握主动,并且信心十足,但是当我读PhD之后,事情产生了变化。首先,PhD应当探索人类知识的边界,由于当今学术的特性,每个人往往都只能专注于一小块领域,这块领域的资源是极其有限的,这个领域的论文就这么多,大家都读着同样的论文,似乎并没有什么信息优势可以获取。每个人都是平等的矿工,对着面前的各种难题一铲一铲地挖,期待着下面的金矿。可是事实果真如此吗?其实学术界也有信息优势,而它们存在于那些最顶尖的lab里,那些lab掌握了最前沿的信息,也引领了学术界的动向。而那些信息并不像我曾经获取过的信息一样,只要Google上查一会就能获得,相反,作为一个outsider,我接触不到那些信息,而那些信息几乎可以决定你在学术界的成就。这让我感到不安且烦躁,我觉得我的人生被莫名其妙地设置了一个天花板,我觉得我的命运不再掌握在我自己的手里。或许你听说过“老板的水平决定了你的上限”之类的说法,这就是了。
当然你会想,为什么要去蹭所谓的信息优势而不直面未知呢?那不是PhD的意义所在吗?说不定那些信息反而会限制你的思路,一个产生伟大研究的灵感就从你手上溜走了。头铁的我当然这么想过,但是老板是否会让你直面未知?特别是Assistant Prof.,要知道tenure的压力是很大的,AP决不会同意你冒着发不出论文的风险憋一个大招。就算你有了那种研究的自由*,你是否思考过直面未知的赢面又有多少?产生伟大研究的几率如此之低,而成本却是如此之高,与其沉浸在自己大多数时候都只能庸庸碌碌地做研究的痛苦之中,不如去寻找其他的方向。这让我想到Witten说过的一句话:“我们其实不需要100个弦论学者,只需要一两个就行了;但我们还是需要招100个研究弦论的学生,因为我们不知道他们之中哪一两个人能成为我们需要的弦论学者”。说实话,我不知道我是不是那百分之一(尽管我觉得我比我身边大部分人都强得多),可是我知道我百分之百可以成为一个很好的工程师,于是我的选择就渐渐清晰了。当然你可以鄙视地说,我看见了一笔不划算的投资,于是知难而退,which is very true. 说到底,人生太过短暂,我只想选择更能够实现自我价值的路。
*: 其实作为一个PhD学生谈研究的自由是一件很搞笑的事,就像太监讨论性生活一样没有意义,因为本质上学生是为老板打工的(为什么大家把导师叫做老板?)。至于赢面问题,有兴趣的人可以看下The PhD grind,里面讲到了Klee-UC在Stanford诞生的故事,一个来自顶尖学校,功成名就的Full Prof.,想要学生实现他的一个想法,花了整整五年。代价是什么呢?在这五年里有四个学生尝试过这个project,只有一个学生成功了,剩下的两个直接像我一样quit PhD了,还有一个(作者自己)去寻找别的项目了。
> In the end, it took three attempts by four Ph.D. students over the course of five years before Dawson’s initial Klee-UC idea turned into a published paper. Of those four students, only one “survived” —— I quit the Klee project, and two others quit the Ph.D. program altogether. From an individual student’s perspective, the odds of success were low.↩
四/五/六年是一段漫长的时间,也是很多PhD毕业所需要的时间,在这么长的时间里,到底该学些什么?下面是我的看法:
我们都很了解“授人以鱼,不如授人以渔”的道理,换个角度思考,如果要学习,到底该学鱼,还是渔呢?我想大家都知道要选择渔,那么渔在这个context下代表什么?在我眼里就代表处理各种事的方法,包括生活,包括research。学习这些方法,我认为并不需要花费太久的时间,而且重要的是,随着时间的推移,学习方法的收益将会急剧降低,因为你已经见过老板处理大多数事情的方法。举个例子,你用了一年的时间观察了老板90%的方法,那么剩下的五年你就只能观察剩下的10%了。剩下的一些方法,就像程序中的rare branch一样隐藏得很深,需要特定的事件来trigger之后,你才能看到老板是如何处理的,但是这些方法真的值得你花上很多年去学习吗?如果你觉得方法的精要藏在那些你尚未发现的方法里,那你大概率是错了,因为真正重要的,需要学习的方法,就藏在每天的生活里,每天的research里。或许你会说,“那老板还是比我强啊,我还是可以从他身上学到很多东西”,可是你确定你学到的是渔而不是鱼吗?考虑以下情况,我们假设鱼 = 渔的efficiency * 时间
,你有没有想过,当你到了老板现在的年龄的时候,你也许会比他拥有更多的鱼?其实结果并不重要,重要的是你有没有从这个角度思考过,也许很多人从来就没有想过,假以时日,你完全可以超过自己的老师们。我离开本科的时候,自信自己在写代码方面已经超过了我们学校的绝大多数老师,除了两个比较年轻的老师,不过我觉得,这没什么了不起的,两三年之后我就可以超过他们。
也许有人会说“没人值得你学习六年”太夸张,然后搬出爱因斯坦/费曼/普朗克云云,说自己愿意终身跟随这些大师学习。其实这又是把鱼和渔给混淆了,如果我与爱因斯坦共事,也许我六个月就会离开他了,一个月观察他的方法,剩下的五个月在纠结要不要离开他,毕竟我在离开一颗超级聪明的大脑 :) 渔,也就是做事的方法,是很因人而异的东西,有些方法需要你废寝忘食,比如每天两点睡五点起;有些方法则需要你智商180才能使用。有些人只看到了对方有一大堆鱼觉得对方身上一定有可学习的东西,其实是不对的。如果你看到这个方法对你而言毫无实践性,我看不到继续学习这个方法的意义。每个人都是独特的,没必要去想着做第二个爱因斯坦/费曼/普朗克,从各个人身上寻找可学习的长处,融合成为自己的方法,这才是最重要的。至于鱼的数量,当方法正确时,你需要的只是时间。当然,这种思考方式只适合像我一样希望修炼自己的钓鱼方式,然后钓到属于自己的鱼的人。向比自己厉害的人学习,从他们身上拿到鱼固然也可以给自己带来很好的结果,可是这却不是我所喜欢的方式。
所以呢,如果你想学习一个人做事的方法,根据对方水平不同,学习几个月到一年的时间即可。至于PhD为什么需要这么长时间,那是另一个复杂的问题。写到这里我突然想到一件事,在我转到PhD之前,我去找我们学院的一个快退休老教授,他问我为什么要读PhD,他说每个人读PhD都有他的原因,他当时读PhD因为他只想当个大学教授,而读PhD是通向大学教授的唯一路径。我时常想起这件事,并且感叹于他的坦诚。
或许有的人会说,从上面的字里行间可以看出,这些问题出现的主因在于我的水平不够,对于这种评价,我可以很肯定地告诉你,你错了,我可以很自信地告诉你,我就是我们学院最强的学生之一。我离开的原因之一单纯就是觉得我读得很不爽,不仅如此,我还呼吁所有PhD读的不爽的人都马上quit,去其他地方实现自己的价值。读PhD这事有时就好像996的怪圈,理论上只要大家都抵制996,拒绝为996的公司工作,那么996的现象就会消失,但是事实却是总有那么一帮没有骨气的人像舔狗一般地为公司工作着,永远以为自己和资本家穿同一条裤子。读PhD其实也一样,如果每个PhD都读得不爽就退学,那么老板还有可能会压榨学生吗?现实世界里有很多老板可以肆无忌惮地压榨学生,因为他们知道,无论如何,总会有不明真相或者执迷不悟的学生前赴后继地加入的。消除老板与学生间不平等的关系就像是一个美好愿望,它永远也不会实现。
想到这里,我突然觉得我不再年轻了,因为我开始希望别人能够听进去我的劝告,所谓过来人的经验。我在生活中,网络上,都观察到一些人,看到了有人从某老板门下quit,却仍前赴后继地想要加入。原因很简单,他们觉得别人离开都是因为能力不足,觉得自己都是特殊的那一个,觉得自己一定能抗下所有的事。我也曾经是这样的一个人,我就觉得自己就是那个最独特的人,不管所谓过来人如何描述某件事的难度,我也会觉得我自己一定可以的,他们将这件事描述得越难,我只觉得越兴奋,越想做成这件事。就好像我在第一学期的时候想要找实习,有个关系很好的朋友帮我联系了个面试。面试之前我在地里搜了一圈,确定了可能的面试题,但是我却一道都没有做。尽管我看过无数人说面试要刷题,但是我始终觉得我不需要,我觉得真正的面试就应该面对一道从未见过的题,然后在规定时间内解决它,那才刺激,那才是能力的体现。那是我的第一次面试,我面得很糟糕,毫无意外地挂了。那是一次很惨的面试,我第一次知道,尽管我自认为我的编程水平不知道高到哪里去了,可是我在毫无准备,毫无练习的情况下会表现得如此惊慌失措。面试官人很好,最后五分钟一直在安慰我,而我则因为意识到自己并不是那个特殊的人而在最后五分钟里一直努力忍住想哭的欲望。
是的,如果我一直坚持下去,我一定能完成这个PhD,我确实会完成了一个比较难的成就,可是那又怎么样呢?我得到的是我想要的吗?而我失去的又是我在乎的吗?我不再会纯粹为了挑战某件事而去完成它了,其实退一步看,摆在我面前的难题可以有很多,升职,创业,以及很多我还不知道的路。我没有时间再去想将他们全部完成了,我只想找到一条最能实现自我价值的路。
我见过很多人对他人的评价耿耿于怀,非常在意别人如何看待自己,对不公正的批评感到难过,气愤,甚至痛苦,我觉得大可不必。我的观点是,这个世界上的每个人,只有自己才有资格公正地,合理地对自己作出评价,原因很简单:只有自己才能完整地看到全部的自己,而其他所有人都只能看到我生命中的一部分。换句话说,所有他人的评价都是片面的,带有偏见的,不完整的,我应该使用挑剔的眼光来看每一个来自他人的评价。我也曾经因他人的评价而痛苦,可是后来我才发现,随意在意他人的评价是很危险的,随意给予他人评价我的权力无异于递了一把可以伤害自己的尖刀给对方。后来我意识到,很多人其实根本没有评价我的权力。
然而,把评价自己的权力牢牢抓在自己手上是危险的,“With great power comes great responsibility”。想象一下,如果全世界只有你自己可以评价你自己,那么你的评价会左右你对自己在这个世界中的定位,如果定位错误,那就有可能陷入巨大的困境。我的应对方式很简单,我非常非常虔诚地对待这种权力,时刻思考反省,自己对自己的评价是否公正,是否有失偏颇。一味地看高或者看低自己,都是我所不希望发生的。
说回PhD的事,几乎每个PhD candidate都会收到来自老板的大量评价,很多人天真地将老板的每个评价认真对待,却殊不知这些评价往往都是带有某种目的性的。大部分所谓的老板-学生关系,其实就是employer和employee的关系罢了,想想现实世界里下雇主和员工的关系,你还会像以前那样天真吗?很多老板会给一大堆负面评价,损害你的自尊,伤害你的感情,贬低你的能力,很有可能这些都只是为了让你更听话地卖力工作罢了,“你已经如此糟糕了,再不听从我的建议,你还有未来吗?”。在我quit之后,我意识到,这种东西其实跟某种所谓精神控制的把戏有很多共通之处 :)
在这里必须要说明的是,我在上面写的都是关于“人”的评价,除此之外,还有对“事”的评价。比如你做了某件事,然后你的队友/上级对你做的这件事做出了一个评价,这就叫做对“事”的评价。这些评价往往是需要听取的,大多数时候它们都能帮助你进步。可惜的是,现实世界往往是复杂的,只有很少的人可以清晰地分开对“人”的评价与对“事”的评价,人们有意无意地将两种评价混在一起,以达到某种目的。所以,当你看到一个评价时,请擦亮眼睛,别让无谓的评价伤害了你。
我在yinwang的文章里看到过一个很有趣的例子3,讲的是在Cornell求学的生活:
有人打了个比方,说Cornell说要教你游泳,就把你推到水池里,任你自己扑腾。当你就要扑腾上岸时,他在你头上用榔头一砸,然后继续等你上岸。当你再次快要扑腾上岸时,他又举起一块大石头扔到你头上,这样你就可以死了,可是Cornell仍然等着你游上岸… 这就是对我在Cornell的经历的非常确切的比喻。
…
现在我觉得自己就像那个到Cornell学“游泳精髓”人,本来就是会游泳的,可是每到岸边Cornell就搬起大石头来砸我,还说我不会游。于是我钻到水底下钻了一个洞,把水放干。
我也是一个渴望学到所谓“游泳精髓”的人,可我却发现在这一年里,我非但没有学到“游泳精髓”,反而天天被人说不会游泳,搞得很不开心。大多数时候,当我游到岸边时,我只会被一锤子砸晕然后被命令重新来过。可是,我不觉得我是一个不会游泳的人,相反,我很自信我可以游得很好,我不想屈服于这些奇奇怪怪的约束,我很清楚这不是我想要生活,于是我也在泳池底下开了个口,把水放干,然后离开。
决定跟老板分开的时候,学期还剩下两个月结束,学院里的老教授建议我在剩下的时间里找别的老师谈谈,找一个合得来的老师重新开始。我很感谢他真诚的建议,但是我从来没有找过别的老师,因为我早就发现我对其他教授的工作都不感兴趣。部分原因大概就是,我发现department里的所有老师,除了我的老板以外,都在做Machine Learning相关的工作————无论他之前究竟是做什么方向的工作。至于原因,自然是兴趣,只是我分不清是对Machine Learning的兴趣,还是对funding的兴趣。
我花了两三天决定了接下来要干什么*:找工作。很遗憾,我quit的时候是十月中旬,那时候已经是秋招的尾声了。我打开Google的招聘页面,发现校招都关了,我意兴阑珊地准备直接放弃,可是我的一个好朋友强烈建议我投一下社招。没想到的是,第二天Google的recruiter就找到我说要谈一下,因为当时很晚了,他还很贴心地帮我跳过了之前的面试直接让我去onsite。由于跳过了之前的面试,所以我只有十几天的准备时间,本来也没抱太大的希望,只想去湾区玩一圈,没想到去了之后居然一下把所有面试题都解决了。后来就是通知过了hiring committee然后有了口头offer。为什么没有收到正式offer呢?因为我投的是社招,Google的社招必须要先进行一个team match的阶段才会发offer,这个match一般在入职前的一两个月才会进行,而我则一直要到夏天才能毕业。一开始我也很焦虑,不停地催recruiter,但是这件事也超出他的掌控范围了,他也只能对我说,“你100%会得到offer的”,或者是“你110%会得到offer的”。现在也算是佛系了,反正想去就得等,先干点别的吧。
无论最后的结果如何,我都非常感谢Google带给我的美好体验,第一次去湾区,第一次去onsite,第一次在白板上写代码,更重要的是我终于又清晰地知道,我依然是那个编程很厉害的我 :)
其实这里还有一个插曲,2019年的年底,Google只招很少的new grad,很多在我之前面试并且通过面试的人,也没能拿到offer,因为招满了。可能因为我投的是社招,所以并不受缩招的影响。我觉得自己很幸运,有时候我在想,也许这就是冥冥之中的宿命吧。
(Update: 我已经拿到offer并决定去Google了,我很幸运地match到了一个自己想去的组,工作内容大概就是通过Program analysis来分析Malware,这跟我PhD期间做得几乎是一模一样的东西,我可以换一个地方继续做我喜欢的东西了:D 还有一个有趣的事,在我签字后的第二天,Bloomberg就出了一条新闻,说Google要significantly slow hiring in 2020)
再后来,到了该正式quit的时候,我拿着表格去找老板签字,快三个月没见了,去之前我还很好奇他会不会问我接下来要去哪。最后他还是问了,我说我应该要去Google了,我以为我说出这句话的时候会很爽,但是其实并没有,他很平静,我也很平静,就像某个学生来找一个素未谋面的教授签字一样。我从来不喜欢主动提出要走,就算签字已经签完了,我还是像以前一样坐在他面前,又像以前一样尴尬又沉默地过了三秒。最后还是像以前一样,“OK”,我先说,“OK”,他也说。我站起来准备走,他送我到门口,我想说点什么临别的话语,却脑子一片空白。我发现我还是像从前大多数的时候一样,来到这个办公室之前,总是没有准备好所有自己要说的话。只不过这一次,他应该不会再因为这个而感到不满了。慌乱之中,我留下一句“I may come back later to visit”,就这样吧。
*: 有朋友来信说很好奇为什么我之前写得雄心勃勃说想要做出伟大研究,而现在却只花了两三天就决定要退出。实际上,说是两三天,其实我只花了两三分钟,我给了我自己两三天只是因为我在强迫自己反复思考,我不希望自己后悔。至于只花了两三分钟的原因,自然是之前所有经历的总和累加起来的疲惫以及我发现读PhD这事根本不适合我。给大家分享一个我很喜欢的回答,我就是回答里那个“活在某种自我期许中的人” or “太成熟的人”。↩
我依然很尊敬我的老板,但是我们也真的不合适,他自己承认不知道怎么教我,不知道怎么跟我沟通,还说自己喜欢debug,但是却找不到改变我的方法,找不到问题出在哪里,当时我坐在他面前,没有说出口的话是,也许问题并不只出现在我身上…确实,后来招进来的学生也遇到了和我一模一样的问题,或许更严重,我尽我所能地帮助他们,也告诉他们这一切都曾经也在我身上发生过。或许等时间慢慢过去,两个人都会改变,慢慢适应彼此。但是我和他都太知道我们自己想要什么了,我们都深信自己的路是对的,我们不愿意妥协,也不可能改变自己而成全对方。无论如何,对于我们俩而言,与其呆在一起相互消耗,分开确实是最优的结果。特别是当他知道我很好,而我也知道他很好时,我们不自觉地就将对彼此的期望拔高到一个很高很高的地方,没有达到那种期望所带来的失望,是会把人搞疯的。所以,没有必要再继续了,希望我们一切都好,未来的某一天,我们的生命也许依然可以有所交集。
一年前,我将自己的部分经历po在网上,有一些人通过那篇文章找到我,有希望得到我的帮助的,也有单纯钦佩我的勇气与洒脱的。我很开心那篇文章产生了很多正面的影响,并且希望这篇也可以帮助到一些人。
(The End)
2020-02-23 00:00:00
Feb 23, 2020LLVM Pass与程序分析
注:本文所用LLVM版本为3.8
根据官方介绍,LLVM 是一堆模块化的,可复用的编译器以及工具链。LLVM Pass是其中非常重要的一部分,它可以让你对程序进行编译器级别的修改,但是又不需要你真的实现一个编译器。所谓编译器级别的修改——如果你对编译器有所了解的话,就知道编译器首先会通过lexical/syntax analysis将源代码解析成AST(abstract syntax tree,即语法树),然后对AST进行semantic analysis以及各种各样的optimization,最后生成目标代码。在对AST进行分析的过程中,编译器会对AST进行一次或多次,局部或全局的分析,这些分析被模块化了之后,每个分析往往只负责一个相对独立的功能,这些分析也被称作Compiler Pass。LLVM会将源代码统一表示成LLVM自己定义的IR(intermediate representation,即中间表示),并基于IR生成AST,然后再将AST交给用户,让用户根据自己的需求去写Pass对程序进行分析。
这篇文章主要是关于用LLVM Pass来实现简化版taint analysis(污点分析)的核心部分。
Taint analysis可以被看作是信息流分析(Information Flow Analysis)的一种,主要是追踪数据在程序中的走向。具体实现的话,用static analysis(静态分析)或者dynamic analysis(动态分析)都可以。这里主要是用static analysis。我们要实现的简化版taint analysis,目标是找到程序中所有有可能被攻击者控制的变量。下面我尝试简洁地说明什么是taint analysis。
system
函数,并执行恶意指令。通过追踪这些数据的走向,我们可以识别用户的输入是否具有这些特征。换句话说,在程序分析中,用户的原始输入长什么样并不重要,我们在意的是这段输入对于程序运行状态的影响。system
函数中。system
(导致command injection), PHP中的echo
(导致XSS),等等。a
处于tainted状态,因为程序对用户输入没有进行任何处理,直接将用户输入作为echo
的参数会导致XSS;但第三行中的a
就处于sanitized(untainted)状态,因为此时a
中的特殊字符被htmlspecialchars
转义过,XSS攻击在这里失效了。如果我们只关心XSS攻击,那我们就没必要再追踪a
这个变量了(除非a
的值在之后又被改变了)。这里的的htmlspecialchars
函数就可以叫做是XSS的sanitizer。1 $a = user_input(); // tainted
2 echo $a;
3 $a = htmlspecialchars($a); // not tainted because of sanitization
4 echo $a;
Taint analysis的难点在于已知source/sink的情况下,在茫茫多的变量中,如何精确地分辨source传过来的数据是否到达了sink。准确度很重要,如果不安全数据没有到达sink,但是你的分析却报了一条警告,那么这条警告就是false positive(FP);如果不安全数据到达了sink,但是你的分析却报了一条警告,那么这条警告就是true negative(TN)。没有人喜欢FP或是TN,试想一下一个杀毒软件整天报告说你的正常文件是病毒,却又遗漏了真正的病毒。
要追踪不安全数据的传播(taint propagation),其实很简单,变量总是一步一步传播的。比如说变量a
被tainted了,然后b = a
,那我们就说b
也被tainted了,就是这么简单。
Tainted variable的传播方式主要可以分为赋值指令、布尔指令、算数指令、分支指令、位运算指令等。其中值得注意的是布尔指令和位运算指令,他们不是总能传播taintness的。考虑以下代码:
bool a = user_input(); // tainted
bool b = false; // not tainted
bool res1 = a && b; // not tainted
bool res2 = a || b; // still tainted
我们可以看到res1
不再处于tainted状态,而res2
依然被tainted,背后的原因我这里就不说了。
位运算指令也涉及到上述的问题,除此之外,它还涉及到追踪的精度问题,考虑以下代码:
uint8_t a = atoi(user_input()); // tainted
uint32_t b = 0xfffffff; // not tainted
b &= a; // tainted, but how?
uint32_t c = b & 0xffff000; // is c tainted?
我们可以清楚地意识到第三行的b
是一个tainted variable,也就意味着它可能被攻击者所控制,根据之前提到的逻辑,c
当然也应该是tainted的,因为它的值是从b
里来的。但是事实如此吗?我们注意到a
的长度是8个bit,而b
的值是通过与a
进行&操作得到的,也就是说攻击者至多只能控制b
低位的8个bit,而c
的值则完全不受b
中低位的8个bit影响,所以实际上c
并不应该被taint。
在下面的实现中,为简便起见,我们不考虑上面提到的布尔指令与位运算带来的问题,我们会简单粗暴地认为它们也总是会传播taintness的。
如之前所说,在LLVM中,我们需要和LLVM IR生成出来的AST打交道,所以上述的high-level strategy必须落实到LLVM IR上才行。为了方便起见,这里的具体实现有所不同,上面说的是我们要先找到tainted data,然后追踪这些data如何在程序中传播。这里我使用的相反的策略,我打算找到程序中所有的常量(constant),那么除常量之外的,都是静态分析下无法确定具体值的变量,换句话说,它们的值是需要外部输入来确定的,也就是潜在的,可能被攻击者控制的值。 我们要关注的IR指令有以下几种:
StoreInst
:语法是store src, dst
,将src的值存到dst中,如果src的值是tainted的,那么dst的值也是tainted的LoadInst
:语法是dst = load src
,将src的值存到dst中,如果src的值是tainted的,那么dst的值也是tainted的CmpInst
:用于比较数字的大小,包括整数与实数,假如该指令的一个或两个操作数被tainted,那么其结果也被taintedCastInst
:用于类型转换,如果被转换的变量是tainted,那么转换后的变量也是tainted的BinaryOperator
:所有二元操作符,包括算术指令,布尔指令,位运算指令等,虽然之前提到过布尔指令与位运算指令的问题,但是这里为了简化,直接定义成,如果两个操作数中有一个或两个是tainted的,那么它们的结果也是tainted的LLVM中IR是以不同的的scope组织起来的,常用的scope从大到小排列如下:Module > Function > BasicBlock > Instruction。为了进行上述分析,我们可以通过遍历的方式来查看每一条instruction(在Module里遍历Function,在Function里遍历BasicBlock,在BasicBlock里遍历Instruction…),看这些指令是不是我们所关心的,但是也有更好的办法。回忆上面说到的AST,LLVM提供了不同scope的visitor(visitor pattern是compiler里最常见的设计模式之一),我们需要用到的是InstVisitor,顾名思义,InstVisitor会遍历AST中的每一条Instruction,我们只需重写我们关心的指令所对应的visit函数。
首先我们需要做准备工作,即编译LLVM,这里我们使用LLVM3.8,编译大约需要20-40分钟不等。编译完成后,文章后面提到的所有LLVM相关程序如opt
,llvm-dis
等,都可以在llvm/build/bin
目录下找到。
mkdir llvm; cd llvm
wget http://releases.llvm.org/3.8.0/llvm-3.8.0.src.tar.xz
wget http://releases.llvm.org/3.8.0/cfe-3.8.0.src.tar.xz
tar xf llvm-3.8.0.src.tar.xz
tar xf cfe-3.8.0.src.tar.xz
mv llvm-3.8.0.src src
mv cfe-3.8.0.src src/tools/clang
mkdir build; cd build
cmake ../src
make
然后开始写代码,首先我们需要新建一个struct并继承InstVisitor,并建立一个std::set
来存储所有的常量。
struct TaintInstVisitor : public InstVisitor<TaintInstVisitor> {
TaintInstVisitor() {}
~TaintInstVisitor() {}
set<Value*> const_vars;
}
另外,我们需要一个判定常量的方法,这里我们使用LLVM自带的isa<Constant>()
方法来检测目标变量是否是常量,如果一个变量是常量,那么我们就将这个常量加入上面定义的const_vars
中;如果一个变量的值是从常量派生而来的,那我们也将它加入const_vars
中,所以我们检测一个变量是否是常量的函数可以写成如下形式:
struct TaintInstVisitor : public InstVisitor<TaintInstVisitor> {
// ...
bool is_constant (Value* v) {
return (const_vars.find(v) != const_vars.end()) || (isa<Constant>(v));
}
}
接下来我们实现如何判断一个变量是否由常量派生而来。首先是StoreInst
,判断方式很简单,如果src
是常量,那么dst
也是常量
struct TaintInstVisitor : public InstVisitor<TaintInstVisitor> {
// ...
void visitStoreInst(StoreInst &I) {
// syntax: store src, dst
// if src is a constant, then dst is a constant
Value *op1 = I.getOperand(0); // src
if (is_constant(op1)) {
Value *operand2 = I.getOperand(1); // dst
const_vars.insert(operand2);
}
}
}
然后是LoadInst
与CastInst
,同样的如果src
是常量,那么dst
也是常量。在这里必须要提一下LLVM精巧的设计,在LLVM中,各种Instruction
是继承自Value
的,你可以通过Insturction
的各种方法来得到关于这条指令的各种信息,而这个Instruction
本身的值则代表了这条指令的返回值。比如说,load src
,我们知道这条指令是将src
的值赋给另一个变量,但是这个变量怎么表示呢?在LLVM里,load src
这条指令本身就代表了被赋值的那个变量。在单操作数的指令中,如LoadInst
与CastInst
,这条指令本身就代表了那个被赋值的变量。
struct TaintInstVisitor : public InstVisitor<TaintInstVisitor> {
// ...
void visitLoadInst(LoadInst &I) {
// syntax: dst = load src
// if src is a const, then dst is a const
Value *op = I.getOperand(0); // src
if (is_constant(op)) { // src is a const
const_vars.insert(&I); // dst is also a const
}
}
void visitCastInst(CastInst &I) {
// syntax: dst = load src
Value *op = I.getOperand(0);
if (is_constant(op)) {
const_vars.insert(&I);
}
}
}
剩下的则是各种二元操作符,包括比较,算术,布尔指令,等等,我们可以用一个统一的函数来处理它们。这里我们简单粗暴地认为只有当二元操作符的两个操作数都是常量的时候,我们才认为这个被赋值的变量也是一个常量。
struct TaintInstVisitor : public InstVisitor<TaintInstVisitor> {
// ...
void visitCmpInst(CmpInst &I) {
handle_binaryOp(I);
}
void visitBinaryOperator(BinaryOperator &I) {
handle_binaryOp(I);
}
void handle_binaryOp(Instruction& I) {
Value *op1 = I.getOperand(0);
Value *op2 = I.getOperand(1);
// TODO: actually boolean operator don't follow: &&, ||, &, |
if (is_constant(op1) && is_constant(op2)) {
const_vars.insert(&I);
}
}
}
最后我们创建一个TaintInstVisitor
的实例,并将其运行于这个Module,最后我们将检测到的所有常量打印出来:
struct Hello : public ModulePass {
struct TaintInstVisitor : public InstVisitor<TaintInstVisitor> {
// ...
}
virtual bool runOnModule(Module &M) {
TaintInstVisitor tv;
tv.visit(M);
set<Value*> const_vars = tv.get_const_vars();
for (auto it = const_vars.begin(); it != const_vars.end(); it++) {
errs() << *static_cast<Instruction*>(*it) << '\n';
}
return true;
}
}
写完了Pass,下面就要开始运行了。首先我们要知道LLVM Pass运行于LLVM的bitcode上,一般来说,使用clang编译来得到bitcode:
而我则偏好用wllvm来得到bitcode,wllvm是Python写的,你可以通过pip来安装它,然后用如下命令取得bitcode:
wllvm的优势在于,当你在编译一个拥有许多源文件的project时,你也可以通过上面这两条简单的指令来获取整个项目的bitcode。
获取bitcode之后,我们使用LLVM的opt来运行它,这里我们写的Pass叫做Hello,我们可以使用如下命令来运行这个Pass
下面我们通过下面这个简单的程序来验证一下,其中只有b
是常量,其他变量都是可以被攻击者控制的。
#include <stdio.h>
int main(int argc, char** argv) {
int a = argc;
int b = 1;
int c = a + b;
printf("%d\n", c);
}
使用上面提到的方法运行Pass,打印如下结果:
这说明该程序中共有两个变量的值为常量,第一个是b
,第二个是%2
。其中%2
是一个临时变量,它的值是通过load变量b的值得到的,也就是说它的值完全等于b
的值。由此来看,输出结果与我们之前的分析相符。
我们可以通过llvm-dis
将bitcode转换为IR,再来验证一下Pass输出的正确性。
上述命令会生成IR文件,名为a.out.ll
,主要内容如下:
@.str = private unnamed_addr constant [4 x i8] c"%d\0A\00", align 1
; Function Attrs: nounwind uwtable
define i32 @main(i32 %argc, i8** %argv) #0 {
entry:
%argc.addr = alloca i32, align 4
%argv.addr = alloca i8**, align 8
%a = alloca i32, align 4
%b = alloca i32, align 4
%c = alloca i32, align 4
store i32 %argc, i32* %argc.addr, align 4
store i8** %argv, i8*** %argv.addr, align 8
%0 = load i32, i32* %argc.addr, align 4
store i32 %0, i32* %a, align 4
store i32 1, i32* %b, align 4 ; %b is a constant
%1 = load i32, i32* %a, align 4
%2 = load i32, i32* %b, align 4 ; %2 is a constant
%add = add nsw i32 %1, %2
store i32 %add, i32* %c, align 4
%3 = load i32, i32* %c, align 4
%call = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i32 0, i32 0), i32 %3)
ret i32 0
}
通过查看IR我们可以发现,Pass的输出结果是正确的,在这个程序里,只有%b
和%2
的值是常量,而其他的值都是潜在的可以被攻击者控制的值。
利用LLVM Pass还可以做很多很多事,简单的比如说生成call graph,control flow graph之类的,或者找出代码中所有的循环;稍微复杂点的,就不只是分析,可能会涉及到instrumentation,也就是利用LLVM更改程序行为,比如说在源代码中引入一个新的函数,并在所有特定类型的指令之后都插入该函数的调用。
当然LLVM也不是无所不能的,比如它不适用于没有源代码的项目,对于这种情况,或许使用来自Intel的Pin来做动态分析显得更为合适。
文章写到这里,看起来taint analysis已经很强大了,可事实上依旧存在它难以处理的情况,比如implicit flows。
考虑以下代码:
x = user_input(); // tainted
y = x; // tainted
if (y == 0) { // tainted
z = 2; // not tainted
} else {
z = 1; // not tainted
}
system(z);
沿用我们之前写的analysis,我们会得出x
和y
是tainted的而z
并不是tainted的结论,进而得出“system(z)
是无法被攻击者控制的”这样一个结论。但是事实真的如此吗?我们注意到当y
的值为0的时候,z
的值必定是2;而当y
的值不为0时,z
的值必定为1。换句话说,z
的值是由y
的值来决定的,不同于上面提到的任何一种方式,这种方式是间接的,通过控制流(control flow)而非数据流(data flow)来决定的。
这时候,我们实际上陷入了一个进退两难的境地。激进的做法是,如果决定分支的变量与用户输入有关,则taint该分支中所有的变量,这会导致over-taint,很多不该被taint的变量最后也被taint了;保守的做法是,放弃在taint analysis中检测implicit flows,也就是说,在上面的代码中,z
将不会被标记为tainted。不过,无论选择那一种做法,都是不完美的,都会导致分析的结果出现偏差。
所以就没有更好的办法了吗?说实话,我也并不是很清楚。不过如果你有兴趣,我会建议你google一下“causality inference taint analysis”,也许你会发现比taint analysis更好的方法。
(The End)
2019-10-12 00:00:00
Oct 12, 2019飘来飘去,就这么飘来飘去
读了phd之后我一头扎进论文与代码中,整日思考如何才能更好地完成老板的任务,渐渐养成了很不健康的生活习惯,晚上三四点才睡觉,睡到早上九点十点又起来干活。美国的生活与瑞典不同,没有那种均匀分布的超市,在美国想省钱最好的办法就是隔一两周去Costco买一大堆东西回来,于是我的生活就是,每周七天都在干活,周末找一个下午去一次超市,买上一车的东西然后吃上一两周。来了一年了,也没投入过什么娱乐活动,生活非常贫瘠,时常回忆起在瑞典在欧洲的日子,自由又轻松,这种时候就看看头上的天空困惑着,我们真的是生活在同一片蓝天下吗?经常很想回欧洲看看,但是无奈美国的签证已经到期,重新签证的风险太高,新来的学弟被check了99天才拿到签证,我只能叹一口气,并且觉得自己失去了翅膀,生活太过无聊。
有一天我对我本科的老师说,phd对我来说是个牢笼,到期的签证就是牢笼上的天花板,我们这一代人为了学到更好的知识,过上更好的生活,不得不漂洋过海,来到遥远的地方,却仍不知道应该在何处安放自己。这让我想起我在罗马碰到的两兄弟,我是如此羡慕他们,他们生在罗马,长在罗马,在罗马上了小学,中学,大学,在罗马读着phd… 曾经我觉得在这世界上飘来飘去很有趣,可以看遍这世界的角角落落,可是现在我才意识到,一个人总有飘累的时候,飘累的时候人应该回家吧,可是我的家在哪里呢?我发现我找不到安放我灵魂的地方。
以前我总是兴致勃勃地想,我要去世界上的很多很多地方,慢慢看慢慢体会,直到找到一个适合我的地方,然后停留在那里,又或者满地球的流浪。老师说我是成熟了,我不知道,我觉得我只是累了,读phd并没有带给我快乐,反而使我陷入了深深的疲惫以及自我怀疑。想到在瑞典的时候,我跟导师说我准备去美国读书了,他问我,你以后打算回国(中国)吗?我说怎么可能回去,你没听说吗,我们的president都变成emperor了。他只是说,哦那其实并不重要。那时候的我并不能理解,就像我并不理解他一个美国top15出来的phd为何会来到这个瑞典的本地学校当lecturer,后来我才明白他没说出口的句子应该是,只要你觉得那个地方合适,你就留下来好好生活吧。看看他的履历,印度到美国到英国到瑞典,一路走来,其实我们都是这世界上不安的灵魂,我们并没有那么幸运,出生在属于自己的罗马,我们在这世界上飘来飘去,都想要寻找属于自己的一个天地。
飘来飘去,就这么飘来飘去。
Update:
最近看了两个视频,一个关于美国的偷渡客,一个是关于国内的这些打工者。看吧,其实我们都在流浪,都很孤单。
(The End)