探索mullender.c-深入了解第一位IOCCC获胜者

2020-08-30 13:12:39

几年前,我在medium.com上写了两个帖子,解释一些令人困惑的程序的工作原理。在这些帖子之后,其他的事情被优先考虑了,我有一段时间忘记了迷惑的世界。不过,现在我发现自己又回到了这个问题上,原因我将在另一篇文章中解释,我发现研究混淆代码非常有价值。所以,我心想‘好吧,我最后检查的那些程序真的很简单。研究一个真正令人困惑的程序会是什么感觉?

这篇帖子就是关于那次旅行的故事。我将讨论代码,我是如何让如此陈旧且晦涩难懂的代码运行的,并将我与其中一位原始作者(他在弄清楚其中一些问题方面非常有帮助)的对话片段包括在内。如果所有这些还不够,我设法获得了原始的PDP和VAX源代码,它将在获得许可的情况下托管在这里。我想非常感谢Sjoerd Mullender和Don Libes在复制这些材料时给予的帮助和许可。

因为旧的模糊程序往往更容易剖析,我决定留在IOCCC(国际模糊C代码竞赛)的早期。在那些早期的程序中,有一个程序非常有趣-那是第一个IOCCC获胜者mullender.c,我也在下面复制了它,因为它相当短。C短主[]={277,04735,-4129,25,0,477,1019,0xbef,0,12800,-113,21119,0x52d7,-1006,-7151,0,0x4bc,020004,14880,10541,2056,04010,4548,3044,-6716,0x9,4407,6,5568,1,-30460,0,0x9,5570,512,-30419,0x7。0,0x8,0,4,0,';,';,0,12,0,4,0,';,0,020,0,4,0,30,0,026,0,0x6176,120,25712,';p';,072163,';r';,29303,29801,';e';};

这样的程序是从哪里开始的呢?嗯,IOCCC提供的提示总是很好的第一步。从这个提示中,我们可以学到一些理解程序如何工作所必需的东西:

由crt0.o提供的C启动例程将控制转移到名为‘main’的位置-通常是一个函数,但在本例中它是一个短线数组。

这些短文(以不同格式表示以增强模糊性)形成了一组有意义的PDP-11和VAX-11指令。

第一条指令是PDP代码的分支,由于VAX调用主“函数”的方式,它跳过了程序的第一个字(我稍后会讨论这一点)。

从本质上讲,该程序是一组以各种格式编码的机器代码指令。之所以运行它,是因为这段代码比ANSIC早了五年,而且旧的编译器似乎对“main”是不是函数的顾虑要小得多(我试图用现代编译器将“main”定义为一组短码,但它不喜欢这一点)。

我承认,PDP-11比我的时代更早。我成长在Windows95/98/2000时代,直到最近我甚至不知道什么是PDP-11。这种无知让我有点困惑,在eBay上简单地看一眼就知道,我不会很快得到一台可以工作的PDP-11。不过幸运的是,存在一个仿真器。进入SIMH,它更像是一套适用于各种历史计算机的仿真器--PDP-11是其中之一,幸运的是,VAX-11是另一种。

在2.11BSD中经历了几次错误的开始和失败的冒险之后,我意识到我不知道我在做什么,所以我决定需要一些帮助。这个程序的两个原始作者是Robbert van Renesse和Sjoerd Mullender,为什么不试着联系他们呢?我照做了。罗伯特没有回复我的电子邮件,但幸运的是Sjoerd给我回了电话,并很高兴地讨论了这个项目。他告诉我,他们使用的PDP-11操作系统是Unix v7,所以我继续走这条路。

我设法找到了在这个仿真器上安装Unixv7的指南,现在我的仿真器正在运行,并且安装了操作系统,我可以将其打包成一个文件,编译并运行它。下面是我得到的信息。

动画有点起伏不定,但即使您仍然可以看到它在屏幕上打印“:-)”,直到它被迫退出(没有干净的方法来阻止它)。

在我与Sjoerd的讨论中,我发现它打印的字符串不完全是“:-)”,而是";:-)\b\b“(注意:开头应该有2个空格字符,但是我的模板引擎出于某种原因删除了一个),它正好是9个字节长。前2个字符是空格,后跟:-),最后是四个‘\b’字符。‘\b’字符表示退格。退格键有什么作用?嗯,如果你从它们开始的时候倒数4个字符,你会发现我们只剩下一个空格-就是在这一点上,程序再次循环并打印字符串。这发生得足够快,以至于在我们看来,“:-)”正在屏幕上滚动-这是一个聪明的效果。

事实证明,直接检查C代码并不是很有用。事实也证明,组装也好不到哪里去。以下是组件外观的一个小示例(从PDP-11的角度来看):

.text.globl_main.data.EVEN_MAIN:.WORD 0425.WORD 04735.WORD-010041.WORD 031.WORD 0735.WORD 01773.WORD 05757。

那么,该怎么办呢?实际上,您可以为PDP-11设置一个交叉编译器,如果您完成了这项工作,您可以使用针对PDP-11二进制文件的objdump程序。

下面是对象转储的相关部分-整个内容都在这里,但并不是所有内容都有用。Sjoerd确认PDP代码为FROM_MAIN+0x2c到_MAIN+0x4a(含)。当然,第一个字也是PDP-11指令。我省略的所有内容都与程序的VAX部分有关,我们稍后再来讨论。(这也不是正确的VAX代码,因为反汇编程序认为一切都是PDP-11)。

0:0115 br 0x2c 2c:11c4 mov PC,R4 2E:0be4 TST-(R4)30:e5c4 0009 SUB$11,R4 34:1137 0006 mov R4,$0x3e 38:15c0 0001 mov$1,R0 3c:8904 sys 4 3e:0000暂停40:0009。word 11 42:15c2 0200 mov$1000,R2 46:892d syv$1。

PDP-11主要用于八进制。所有地址和指令都以八进制编码。

在类似mov pc,r4的指令中,您要将pc移到r4,而不是r4移到pc。就我个人而言,我很快就习惯了这一点。

原始程序中的第一个数字是277,将其放入十六进制转换器,就会得到115,就像我们在objdump中看到的那样。不过,更有趣的是将其转换为八进制-425的结果。不过,0425是如何得到br 0x2c的呢?嗯,为此,你需要拿些手册来。我弄到了很多这些电脑的旧手册,但其中最有用的是1973年出版的PDP-11/45手册,它会很有帮助地描述指令格式,以及它到底做了什么。下面是无条件分支指令的页面示例。

如您所见,它将程序计数器设置为PC+偏移量的2倍-在我们的例子中是25(八进制)。因为我们程序开始时的程序计数器是2(因为它指向要执行的下一个单词),所以我们得到2+(25x2),或2+52=54,这将转换回十六进制得到2c。凉爽的!。

现在我们已经了解了分支是如何工作的,我们可以检查主要内容了-我不会深入介绍每个小说明的手册,但是如果您感到无聊并享受这类事情,那么这样做是很有趣的。

MOV PC、R4 TST-(R4)SUB$11、R4 MOV R4、$0x3E MOV$1、R0系统4 HALT.WORD 11

我们将程序计数器移入R4寄存器,然后使用“test”(TST)指令。这具有清除PSW(处理器状态字)中的一些标志的效果,并且由于寄存器使用的是‘自动递减模式’,它将从存储在R4的值中减去2。当使用自动递增/递减时,它将在字节指令中递减1或在字指令中递减2。之后,我们从R4中减去11,但11实际上是9(用于八进制吗?)。然后我们将R4存储到…中。0x3e?如果您回头看对象转储,您会发现0x3e是‘HALT’或0000。该指令用R4的内容覆盖该值。我们将1移入R0(1表示STDOUT),并调用“sys4”。如上所述,下一行将是R4的值,.word11只是将八进制11直接插入到该点中。如果您想知道R4中到底是什么,以及我们为什么要做这些事情,请耐心等待-我们马上就到了。

如果您熟悉系统调用,您可能知道发生了什么。如果不是,所有这些都是为了设置Write系统调用。我实际上在回溯计算堆栈上发了一篇帖子,在内部讨论了这个调用的工作原理-如果你想进一步了解它的工作原理,我建议你去看看。

我们将1000放入R2,然后调用系统调用55。在此之后,我们使用“sob”指令-它是“减一并转移”,而不是邀请哭泣-来查看R2是否为0。如果不是,我们分支到上一条线路并再次调用系统呼叫55。循环,直到R2为0。系统呼叫55,嗯?那是哪一个?如果你查一下系统呼叫号,你可能会认为这是重启系统调用--可惜,它不是。这是一个未定义的系统调用,Sjoerd声明包含这3行代码只是为了给程序带来轻微的延迟(大概是为了在屏幕上更清晰地打印)。

最后,在延迟之后,我们返回到0x2c,然后有趣的事情又重新开始了。

所以这一切都很好。我们设置一个系统调用来打印字符串,我们延迟一点,然后重复。不过,绳子在哪里呢?如果您猜到R4是指向该字符串的指针,那么您就错了--但是该字符串不在PDP-11代码中。所以你可能认为它在VAX代码里,但它也不在那里。在这一点上,理解这个程序的“部分”-即它是如何布局的-是有用的。这是经过深思熟虑的,也是设计的一部分。

我希望你会觉得我的ASCII艺术解释有用,虽然它在手机上看起来不太好,所以如果你在手机上阅读,这里是与图片相同的图表。

PDP-11条目+|VAX条目|+v|+-+|+-|初始分支|-&>;ADDRESS_Main+0x0|+-+||+-+|+-|VAX代码|<;--。ADDRESS_Main+0x2-_Main+0x22||+-+||+-+||字符串字节|-&>;ADDRESS_Main+0x23-_Main+0x2B||+。ADDRESS_MAIN+0x2C-_MAIN+0x4A|+-+|+->;|VAX代码|-&>;ADDRESS_MAIN+0x4C-_MAIN+0xA0+。

正如您在上面看到的,打印的字符串不在任何部分的“代码”中-相反,它存在于两个部分之间。对于PDP,它就在它的前面。这就是为什么我们将程序计数器(PC)移到R4中,然后将其减去2并减去9-字符串是9字节长。这给我们留下了R4中字符串开头的地址。

这也说明了为什么PDP-11 objdump文件只有部分用处,它将这些字节解释为指令,而它们不是指令。这最初误导了我,让我相信它们在代码执行方面有一定的目的,但实际上并没有那么复杂。也就是说,字节并不做代码和数据的“双重任务”--它仅仅是小心地放在两个代码段之间的数据。

我们可以在ADB中验证这一点(“高级调试器”--也不是Android Debug Bridge;如果这是高级的,我不想看到常规的“db”程序)。下面是ADB会话的图片,显示减法、寄存器转储以及打印的字符串和字节的八进制表示。

您可能会注意到,这些数字都是八进制的(甚至字符串的字节也是如此)。它们也与objdump编号不匹配-这是因为C启动代码。

我知道你在想什么--所有这些旧东西都很漂亮,但如果我们把码头加进去肯定会更好。嗯,我完全同意。我不想麻烦地在这些旧机器上安装另一个操作系统,幸运的是,对我来说,有人已经在SIMH上分发了一个运行4.3BSD的VAX-11的坞站映像。

我启动了所有程序并编译了程序,正如预期的那样,它工作了。这一次没有运行的GIF-它看起来和上次完全一样。

VAX程序的汇编代码无济于事,就像PDP汇编代码一样。以下是一个示例:

_main:.long 0x9dd0115.long 0x19efdf.long 0x1dd0000.long 0xbef03fb.long 0x32000000.long 0x527fff8f.long 0xfc1252d7。

让事情更复杂的是,VAX体系结构没有可用的objdump程序(我能找到)。我怀疑这在很大程度上是因为它被用于专有空间,在大企业/大学之外从来没有真正的市场。因此,基本上,查看此问题的唯一方法是使用dbx调试器-这是最初随BSD提供的调试器。

00000408 blq 40b0000040a推送$90000040c推送42b00000412推送$100000414调用$3,4260000041b配置$7fff,r200000420 DECL r200000422 bneq 42000000424 BRB 40a00000426 halt00000427 halt00000428 chmk$40000042a ret0000042b addp4$20,$3a,$2d,$。

是的,它比PDP的东西复杂得多。试图弄清楚什么是相关的是很困难的,也许是因为这段代码从来不是由人写的。以下是Sjoerd说的:

我对VAX汇编了解不多,所以我们使用C编译器来创建VAX代码。我们没有像PDP代码那样自己从头开始编写。这就是VAX码更复杂的原因,包括PDP码之后的额外数据。

我试图掌握一些VAX汇编的最新情况,但大多数都非常复杂--特别是与PDP相比。手册告诉我某事物的行为是“不可预知的”(它总是使用全部大写)的次数是…。很多。

因此,为了保存我的理智,我放弃了对它的全部理解(如果你是VAX专家,尽管我很想听听你的想法--联系一下)。话虽如此,有些事情确实值得解释。

“呼叫”指令。这将调用一个具有堆栈上的多个参数的过程。此指令还使用目标位置的第一个字作为要保存的寄存器的掩码,这就是我们在VAX上运行时能够跳过第一个分支到PDP代码的方法。(在这种情况下,调用‘call’的是C启动例程)。

“chmk$4”。该指令切换到内核模式,我只能假设$4是Write系统调用。我没有找到很好的解释来解释这到底是怎么回事。

通过调用调用的过程解释了上面的一些代码。具体地说,就是这一位:

这是将字符串的长度、指向字符串的指针和我们要写入的文件描述符-STDOUT放到堆栈上。

关于VAX的事我只能说这么多。我希望我能解释一下PDP代码后面的比特是做什么的,但是似乎没有人知道这个。

为了结束这一点,下面是该计划的故事。我很好奇它是怎么来的,Sjoerd和Robbert是怎么听说IOCCC的--毕竟这是它存在的第一年。

罗伯特和我当时都是VU(阿姆斯特丹自由大学)的学生(我们的专业是CS,因为我们刚开始的时候没有CS课程)。我们接到一项任务,要为计算机网络课程制作两个程序。这些程序被认为是通过不可靠的通道从一个程序可靠地向另一个程序发送数据。这条通道是用一对管道模拟的。

为了好玩,我们决定创建一组令人困惑的节目,只针对PDP,这样做,但要绕过频道。(即作弊,此后进行必要的模糊处理。)。我们的计划奏效了,我们上交了。

当然,老师笑得很开心,然后拒绝了我们的意见。(我们很了解他,所以我们可以逍遥法外。)。

然后IOCCC出现了。我不记得我们是怎么知道这件事的,但当时有一个覆盖全球的消息网络Usenet,我们在那里阅读了一大堆新闻组。我敢肯定它是在那里宣布的,我们也看到了。

因为我们最近才创建了这些模糊程序,所以我们决定可以对模糊C程序使用相同的技术。我们把赌注提高了一点,把它变成了“便携的”。

为了增加混淆,我们对数组中的整数使用了不同的格式,一些是十进制的,一些是八进制的,一些是十六进制的,当值合适时,有些是ASCII字符。

因为这是第一次比赛,我们没有看到任何老参赛作品,也没有看到任何其他参赛者。当然,我们知道#定义和技巧,你可以用它来做,但是我们在这个程序中不需要这样做,事实上,我们尽可能地把它作为“标准”。当时有一个名为“CB”的C美容师程序,它会加强你的程序,使布局看起来更好。我们的程序在CB下是幂等的。

在整个研究过程中,Sjoerd向我提到了一本90年代初的书,名为Don Libs的“迷惑的C和其他谜团”,书中包含了自己对mullender.c的分析,他认为这与我正在做的类似。当然,我必须拥有它。因此,我找到了一本价格合理的二手书,并耐心地等待它的到来(注:如果你想要这本书,请继续在eBay上查看-而不是亚马逊!)。

它确实到了,真是太棒了。下面是封面、目录和随附的5张和1/4张软盘(这是我唯一拥有过的那种大小的软盘)。

不过,有趣的不是我有这本书。不,这本书很有趣,因为它包含PDP和VAX程序的原始源代码,以及生成mullender.c提交的C程序。没有这些,只有通过调试工具才能探索这个程序。幸运的是,Sjoerd Mullender和Don Libes允许我在这里主持关于mullender.c的摘录,供所有人阅读(PDF警告)。

所有的代码功劳都属于Sjoerd Mullender和Robbert van Renesse。这本书的版权(1993)属于Don Libes,并已获得许可再版。

这只是一些随机发生的事情的集合,这些事情并没有真正在主帖子中找到合适的位置。

我的退格键在Unix v7上不起作用。这是一个已知的问题,我试图修补内核,但没有成功。

因为我在一个现代的shell中工作,所以有时某些旧代码会与字体连字一起显示,我觉得这很有趣。

我必须学习“ed”编辑器才能在Unix v7上输入程序。4.3BSD有vi,但我不能说它工作得很好。

最初我以为我可以在裸机PDP-11上重现程序行为,谈到雄心勃勃。

到目前为止,VAX是我读过的最复杂的旧电脑。找一本手册,看一看。

不幸的是,SIMH文档相当糟糕。他们的PDP-11文档只描述了可用的外围设备以及各种比特的含义。它没有详细解释如何在PDP-11命令行上工作(即,在引导Unix之前会得到什么)。我怀疑这些东西主要是由知道自己在做什么的人使用的。

4.3BSD有一个奇怪的错误,无论我在做什么,当前日期和时间都会打印在屏幕上。

Dbx有一个“抱怨”命令,可以让您向程序维护者投诉-不幸的是,它不起作用。

Dbx概述-尽管这是旧的,它还不够旧,不足以涵盖我使用的dbx的版本,但它仍然有一些帮助。