Compiling a Compiler

Compiling a Compiler

作者: lihuawei, 可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息。 | <script src="http://www.lihuawei.com/English.js" type="text/javascript"></script> English Version
链接:

Compiling a Compiler

  UNIX是以C语言写成的。使用C语言的其中一个优点是造成了UNIX的可携性。另一方面,工作站的销售对象是需要大量计算的工程师﹑科学家等等;因此不同于PC,在工作站级以上的电脑上,compiler是一项附在作业系统中的基本配备。 UNIX系统中必定附有C compiler。既然要保持可携性, UNIX系统里面所附的的C compiler也得和UNIX系统一样,用C写作。

  C compiler本身,也是用C写的。当一个语言的compiler 也用该语言本身来写的时候, 便会发生一些有趣的事情。 也许您会问, 既然电脑上面已经有 C compiler了,那么我们要再去compile另一个compiler的source code作什么?答案可能是,原有的内建C compiler可能比较简陋或着老旧,因此我们想把新的 compiler用旧的compiler编译,然后当成系统内建的compiler用。换句话说,我们就这么扩充了系统的内建compiler。

  Glasgow大学的GRASP计划,也用这样的过程发展他们的Haskell compiler。 Haskell是一个functional language(85级同学记得Programming Language课中提到的functional programming吗?)。Functional programming总是带有较多的学术味而缺乏实用经验。Haskell语言本身仍有不少需要再扩充的空间。GRASP 计划用 Haskell 来写Haskell compiler:先从简单的写起, 产生一个最原始的 Haskell compiler,然后用这套原始的Haskell语言写一个功能较强的 compiler (把原来的Haskell扩充了),再用第二版的 Haskell 语言写第三版的compiler ....。由于都是compiler,因此并不会减低效率。一个好处是,每次扩充语言,接下来立刻用新的语言写compiler,于是我们可以立刻看出新加功能是否有用处?该怎么用?如此累积的经验,正可以作Haskell语言以后发展设计的参考。GRASP 计划的理想就是〞把functional proramming带出实验室〞。

  UNIX的创造人之一,Ken Thompson,在他的 Turing Award Lecture中,便由这个主题加以发挥,说了一些有趣的故事。C 是一个被拿来写作业系统的语言。写作业系统的人很难忍得住诱惑,不在系统里面装些后门的。想想看,如果我写作业系统时,偷偷在login 的部份加一段程式码,使得全世界的这套作业系统只要看到我的account和密码就让我进去,给我root权限,这该是多爽呀。 但是我不能直接在 login 的 source code 里面这样写,否则一下就被人抓到了(既然 source code流通,就是要给人看的呀)。 该怎么办呢?就从compiler里面动手脚,称作patch1吧:在compiler中多加一道手续, 如果发现被compile的原始程式〞疑似〞在作login动作,就把它开个漏洞,让我进得去。

  但是这样也不见得行得通。Compiler以后也会改版,新版的compiler可能不是我在写。装系统的人也不见得用我的compiler。怎么办呢?于是我在compiler 的source code中作第二次手脚,称作patch2:如果这个compiler觉得在compile 的程式〞疑似〞另一个 compiler 的 source 的话,就加入上面的patch1和这个 patch2本身。

  好,现在作业系统推出了,CC1 是我写的内建compiler,其中有我动的两个手脚。现在某人在compile UNIX, 不得不用这个compiler。然而CC1 中已经有了 patch1,于是一旦compile到login, compile出来的login程式就被动了手脚。只要看到我的名字,就一定让我进系统,给我root权限。

,----------.    +---------------+    ,--------------.   
| login |     | Compiled  |    | login   |   
| source | =====> | by CC2   | =====> | Program  |
| (clean) |     | patch 1作用 |     |(受感染了!)|
`--------- '    +----------------+     `-----------'

  既然 compiler CC1会作怪, 那么自己写 compiler 总可以了吧? 然而,C compiler还是得用C写,写好了之后,用谁来compile呢? 只有用CC1来compile。 CC1发现新写的CC2是一个compiler的source code,于是 patch2 就发挥作用了。 CC1会在CC2中也加入patch1和patch2。于是CC2也被〞污染〞了。

,------------.      +-------------+     ,------------------.
| CC2  |      | Compiled |     | CC2     |   
| source | =====>  | by CC1  | =====> | patch 2作用 |
| Program |      | (clean)   |      |含 patch1,2 |
`-----------'      +-------------+     `-------------------'

  如果再用CC2来compile一个正常的login程式,由于CC2中有了patch1,所以 compile出来的login程式也会有后门,让我任意的login;

,--------.     +---------------+     ,----------.
| login |      | Compiled |     | login  |   
| source |=====> | by CC2   |=====>  | Program |
| (clean)|      |(patch 1,2) |     | (patched!) |
`--------'      +--------------+     `--------------'

  如果用CC2 compile另一个compiler CC3,由于CC2中已经被加入了 patch2, CC3又会被污染,也就是说CC3这个compiler中还是会有patch1和patch2......如此一来,全世界的每一套UNIX都种下了这个后门,可以让我任意login!

  然而这些patch都只在binary档之中出现。CC2的source code一切正常,所以从source code完全看不出有什么不对劲呢!我们还可以进一步湮灭証据。一旦装好一套系统,公开的CC1 source code中不必有动过手脚的程式码,只要让它被动过手脚的compiler编译就可以了。

  有着无辜的包装,事实上内容暗藏玄机的程式,称作〞特洛伊木马〞。 这个特洛伊木马的故事有趣吗?(註1)

  用C语言写C compiler,写出来的程式会是个什么样子呢? 举个例子,一个C compiler可能有一段前置处理程式在处理C字串中的溢出字元。比如说,compiler 需要把如下的字串:

"Figure listings :/nFigure1/tA Complete Tree/n....."

给转换成:

Figure listings :<换行码>Figure1A Complete.....

  这段程式可能看起来像这样(为简单起见,这个程式从标准输入读进原始码,送到标准输出):

if ( (c=getchar() )=='/' )
 switch (getchar()) {
  case 'n' : putchar('/n'); break;
  case 't' : putchar('/t'); break;
         :
}
else
 putchar(c);

  好像有点奇怪,是吗?明明用if和switch把溢出字元’/’以及后面的’n’,’t’, 分开了,在putchar的地方又送出’/n’, ‘/t’。如果您见多了用某语言写自己的 compiler的情况,对于这种程式段落也就见怪不怪了(註2)。

  C语言是个处在大家周遭而不常被注意到的例子。LISP语言只须简单的parser,不分程式和资料,使得用LISP写LISP interpreter的情形更是普遍,也是常用的教材。85级用的PL课本Chezzi & Jazayeri 的functional programming一章中,最后附的LISP程式就是一个LISP interpreter。如果您研究一下,会发现一些感觉挺像上面那个例子的段落。我自己玩了几年的LISP,到头来反倒除了LISP interpreter 之外,就不会写其他什么有用的程式了。这也是一个奇怪的现象呢。

参考资料: ACM Turing Award Lectures : the first twenty years 1966 to 1985 QA76.24

註:

  1. 原本我以为Ken Thompson只是写写罢了。后来据一些人说,这完全是Ken Thompson 本人干过的真人真事。想像他老远到交大来参加conference, 大摇大摆的走上二楼机房,若无其事地login成root的情况吧。你相不相信呢?
  2. 假设旧的compiler CC1并看不懂’/v’这个控制字元(垂直对齐), 我们 想要有一个具有这个字元的compiler。新compiler CC2的source code可 能是这样:
switch (getchar()) {
 case 'n' : putchar('/n'); break;
 case 't' : putchar('/t'); break;
 case 'v' : putchar('/v'); break;
        :
}

是compiler CC1并没有办法compile这段程式,因为CC1看不懂程式中的’/v’!这似乎是一个逻辑陷阱,在实际develop的时候得多花手续。详见Ken Thompson的原文。

Ken Thomposon 现身说法 ?! Compiling a Compiler 解答篇

by 8123033 穆信成

http://www.cis.nctu.edu.tw/chinese/doc/research/doc/cismagazine/cis-magazine-84-1.html

  Comp.lang.lisp 有一段时间挺混乱的. 一个Lisp讨论区, 最新的 post 却都不是什么lisp 讨论. 有人在批评 C 语言, 有人在抱 怨 UNIX 的指令太难记, 当然有人批评就有人反驳. 一些老读者 终于看不下去了, 不得不大声疾唿要回归这个讨论区的本来面貌, 但看大家热烈的兴头, 要打完这许多场笔战还有得等的呢.

  就在这时候, 有人又想起了 Ken Thomposon 在他的图宁奖演讲中 所提及, 放在C compiler 中的那个特洛伊木马了. 我曾在资讯人 园地 『 Compiling a Compiler 』 中谈到这个故事. 『特洛伊 木马』, 指的是外表看起来无害, 但内里暗藏玄机的程式. 据 Ken 所说, 他在 C compiler (事实上应该是 pre-processor) 中放了一个后门, 如果compiler发现它所处理的程式是 login 的 原始程式, 它就会在里面加入一段程式码, 让 Ken Thomposon 可以 login 进去. 由于 UNIX 得用 C compiler 来编译, 这意味 着 Ken 将可以进入任何装 UNIX 的电脑; 那么, 我换个干净的 compiler 总可以吧? 但新 compiler 的原始码也得由旧compiler 来编译, 而为了让这个特洛伊木马能流传久远, 如果旧 com- piler 发现自己正处理的的是另一个 (正常的) C compiler, 它 就会把上面那段『如果是login, 就加入一段...』 的那段程式码, 给插到这个compiler里面去.

  就在 comp.lang.lisp 正为了一些 UNIX / C 的问题吵得兴高采 烈之际, 大家又回想起了这个老故事. 于是有人问了, 真有这么 回事吗? 抑或 Ken Thomposon 只是说说而已? 如果他真做了这么 巧妙的一个特洛伊木马 , 现在他已经可以进入世界上任何一台 UNIX 工作站, 如入无人之境了. 有人说, 这不过是 『一个可能 发生的假想』而已. 毕竟要写出这样的一个compiler, 可需要很 高的技术水准呢. 也有人说, 他当然把这个骇人听闻的 compiler 给写出来了, 只是没有流出去罢了. 有人引了Ken的一段话, " The actual bug I planted in the compiler would match code in the UNIX ‘login’ command”, 看到没有, 『actual』 唷, 当然是真有其事啦. 更有人言之凿凿地保证, Ken 在一次 UNIX user group 集会中曾暗示着, 他已经把这个特洛伊木马给 送出去啦!

  终于有人在翻出Ken Thomposon的 email address后, 亲自去向他 本人求证了. Ken 的回信说, 他并没有看comp.lang.lisp 的习惯. 听说了这个消息后, 才上去看了看相关的讨论. 然而, 直接在 newsgroup 里头回话似乎会引起更多的误会, 于是他就不亲自出 面啰!

  原来, Ken 在做出了这个特洛伊木马之后, 便半哄半骗地让 Unix Support Group 装了他们的compiler. 大约一个月后, login程式 就如同预期地被感染了. 然而Unix Support Group 也不是等闲之 辈. 不久之后, 有人发现了, 这个C pre-processor 的 symbol table 怎么怪怪的? 于是他们直接研究 compiler 产生的目的码, 看看到底 Ken Thomposon 在搞什么鬼. 最后 Ken 的计谋就曝光 啦.

  Unix Support Group 也不急着找 Ken 算帐, 倒是用很妙的方法 解决了这个特洛伊木马. 能直接产生目的码的 compiler 大都有 一个 -S 的选项, 亦即要 compiler 产生组合语言档, 而非目的 码. 产生组合语言档的一个理由是让人来读, 可以除错用. 因此 为了避免被发现, 这个特洛伊木马在用 -S 编译的时候是不做坏 事的. Unix Support Group 便把所有的程式用 -S 选项编译一次, 再另外用 assembler 产生目的档. 于是, 这个特洛伊木马就消失 啦!

  Ken 说, 这个特洛伊木马仅在他们之间被当作一个恶作剧的小把 戏, 从来没有流出去过. 得到这个答案的 Jay R. Ashworth 很 得意地把这封 email 贴了出来, 并且告诉大家, 最好把这封信存 起来, 以备下次又有人说起这件悬案时, 就可以拿出来现宝啰! 以下是 Jay R. Ashworth 贴的原文. 档头和引言已经删掉了.

---------------------------------------------------------

Proving that the real Mrs. Robinson stood up.

It occured to me last week that ken@research.att.com is _still_ a valid address, 25 years later... so I asked. Here, from Ken himself, is the Real Story:
) From ken@plan9.att.com Sun Apr 23 14:42 EDT 1995
) Received: from plan9.att.com by IntNet.net (5.x/SMI-SVR4)
) id AA19375; Sun, 23 Apr 1995 14:42:51 -0400
) Message-Id: <9504231842.AA19375@ IntNet.net>
) From: ken@plan9.att.com
) To: jra@IntNet.net
) Date: Sun, 23 Apr 1995 14:39:39 EDT
) Content-Type: text
) Content-Length: 928
) Status: RO
)
)

thanks for the info. i had not seen
) that newsgroup. after you pointed it
) out, i looked up the discussion.
)
) writing to news just causes more
) misunderstandings in the future. there
) is no way to win.
[

note: I asked him if he minded my posting the reply, he had no objection ] ) fyi: the self reproducing cpp was
) installed on OUR machine and we
) enticed the "unix support group"
) (precursor to usl) to pick it up
) from us by advertising some
) non-backward compatible feature.
) that meant they had to get the
) binary and source since the source
) would not compile on their binaries.
)
) they installed it and in a month or
) so, the login command got the trojan
) hourse. later someone there noticed
) something funny in the symbol table
) of cpp and were digging into the
) object to find out what it was. at
) some point, they compiled -S and
) assembled the output. that broke
) the self-reproducer since it was
) disabled on -S. some months later
) the login trojan hourse also went
) away.
)
) the compiler was never released
) outside.
)
) ken

Everyone: please save this post, so the next time the question comes up,
you can just go look. :-)

Cheers,
-- jr 'will bug legends for food' a
--
Jay R. Ashworth High Technology Systems Consulting Ashworth
Designer Linux: The Choice of a GNU Generation & Associates
ka1fjx/4 "I minored in babbling in college... and got +1 813 790 7592
jra@baylink.com honors in it." --Brian Heath NIC: jra3

Unix System Lab
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值