分享

程序是用编程语言写的,编译器也是程序,先有编译器还是编程语言

 山峰云绕 2018-01-22

程序是用编程语言写的,编译器也是程序,先有编译器还是编程语言      

【【蝙蝠侠运维开发】程序是用编程语言写的,编译器也是程序,先有编译器还是编程语言】http://toutiao.com/group/6513455386396197383/?iid=15906422033&app=explore_article&timestamp=1516552928&tt_from=copy_link&utm_source=copy_link&utm_medium=toutiao_ios&utm_campaign=client_share 

             

我爱七龙珠

首先肯定的是先有的编程语言,哪怕这个语言简单到只有一个符号。先是设计好语言的规则,然后编写能够识别这套规则的编译器,否则若没有语言规则做为指导方向,编译器编写将无从下笔。

第1个编译器是怎么产生的,这个问题我并没有求证,不过可以谈下自己的理解,请大伙儿辩证地看。

这个问题属于哲学中鸡生蛋,蛋生鸡的问题,这种思维回旋性质的本源问题经常让人产生迷惑。可是现实生活中这样的例子太多了,如:

1.英语老师教学生英语,学生成了英语老师后又可以教其他学生英语。

2.写新的书需要参考其它旧书,新的书将来又会被更新的书参考,就像本书编写过程一样,要参考许多前辈的著作。

3.用工具可以制造工具,被制造出来的工具将来又可以制造新的工具。

4.编译器可以编译出新的编译器。

这种自己创造自己的现象,称为自举。

自举?是不是自己把自己举起来?是的,人是不能把自己举起来的,这个词很形象的描述了这类“后果必须有前因”的现象。

以上前三个举的都是生活例子,似乎比第4个更容易接受。即使这样,对于前三个例子大家依然会有疑问:

1.第一个会英语的人是谁教的?

2.第一本书是怎样产生的?

3.第一个工具是如何制造出来的?

其实看到第2个例子大家就可能明白了,世界上的第一本书,它的知识来源肯定是人的记忆,通过向个人或群众打听,把大家都认同的知识记录到某个介质上,这样第一本书就出生了。此后再记录新的知识时,由于有了这本书的参考,不需要重新再向众人打听了原有知识了,从此以后便形成了书生书的因果循环。

从书的例子可以证明,本源问题中的第一个,都是由其它事物创建出来的,不是自己创造的自己。

就像先有鸡还是先有蛋一样,一定是先有的其它生命体,这个生命体不是今天所说的鸡。伴随这个生命体漫长的进化中,突然有一天具备了生蛋的能力(也许这个蛋在最初并不能孵化成鸡,这个生命体又经过漫长的进化,最终可以生出能够孵化成鸡的蛋),于是这个蛋可以生出鸡了。过了很久之后,才有的人类。人一开始便接触的便是现在的鸡而不知道那个生命体的存在,所以人只知道鸡是由蛋生出来的。

很容易让人混淆的是编译c语言时,它先是被编译成汇编代码,再由汇编代码编译为机器码,这样很容易让人误以为一种语言是基于一种更底层的语言。

似乎没有汇编语言,c语言就没有办法编译一样。拿gcc来说,其内部确实要调用汇编器来完成汇编语言到机器码的翻译工作。因为已经有了汇编语言编译器,那何必浪费这个资源不用,自己非要把c语言直接翻译成机器码呢,毕竟汇编器已经无比健壮了,将c直接变成机器码这个难度比将c语言翻译为汇编语言大多了,这属于重新造轮子的行为。

曾经我就这样问过自己,php解释器是c语言写的,c编译器是汇编写的(这句话不正确),汇编是谁写的呢?后来才知到,编译器gcc其实是用c语言写的。咋一听,什么?用c语言写c编译器?自己创造自己,就像电影超验骇客一样。当时的思维似乎陷入了死循环一样,现在看来这不奇怪。其实编译器用什么语言写是无所谓的,关键是能编译出指令就行了。编译出的可执行文件是要写到磁盘上的,理论上,只要某个进程,无论其是不是编译器,只要其关于读写文件的功能足够强大,可以往磁盘上写任意内容,都可以生成可执行文件,直接让操作系统加载运行。相像一下,用python写一个脚本,功能是复制一个二进制可执行文件,新复制出来的文件肯定是可以执行的。那python脚本直接输出这样的一个二进制可执行文件,它自然就是可以直接执行的,完全脱离python解释器了。

编译器其实就是语言,因为编译器在设计之初就是先要规划好某种语言,根据这个语言规则来写合适的编译器。所以说,要发明一种语言,关键是得写出与之配套的编译器,这两者是同时出来的。最初的编译器肯定是简单粗糙的,因为当时的编程语言肯定不完善,顶多是几个符号而已,所以难以称之为语言。只有功能完善且符合规范,有自己一套体系后才能称之为语言。不用说,这个最初的编译器肯定无法编译今天的c语言代码。编程语言只是文本,文本只是用来看的,没有执行能力。最初的编译器肯定是用机器码写出来的。这个编译器能识别文本,可以处理一些符号关键字。随着符号的越来越多,不断地去改进这个编译器就是了。

以上的符号说的就是编程语言。后来这个编译器支持的关键字越来越多了,也就是这个编译器支持的编程语言越发强大了,可以写出一些复杂的功能的时候,干脆直接用这个语言写个新的编译器,这个新的编译器出生时,还是需要用老的编译器编译出来的。只要有了新的编译器,之后就可以和老的编译器说拜拜了。发明新的编译器实际上就是能够处理更多的符号关键字,也就是又有新的开发语言了,这个语言可以是全新的也可以是最初的语言,这取决于编译器的实现。这个过程不断持续,不断进化,逐渐才有了今天的各种语言解释器,这是个迭代的过程。

程序是用编程语言写的,编译器也是程序,先有编译器还是编程语言

这张图片在网络上非常火,它常常与励志类的文字相关。起初看到这个雕像在雕刻自己时,我着实被感动了,感受到的是一种成长之痛。今天把它贴过来的目的是想告诉大家,起初的编译器也是功能简单,不成规范,然而经过不断自我“雕刻”,它才有了今天功能的完善。

下面的内容是我参考了别人的文章,由于找不到这位大师的署名,只好在此先献上我真挚的敬意,感谢他对求知者的奉献。

要说到C编译器的发展,必须要提到这两位大神——C语言之父Dennis Ritchie和Ken Thompson。Dennis和Ken在编程语言和操作系统的深远贡献让他们获得了计算机科学的最高荣誉,Dennis和Ken于1983年赢得了ACM图灵奖。

编译器是靠不断学习,不断积累才发展起来的,这是自我学习的过程,下面来看看他们是如何让编译器长大的。

我们都知道转义字符,转义字符是以’\’开头的多个字符,通常表示某些控制字符,它们通常是不可键入的,也就是这些字符无法在键盘上直接输入,比如’\n’表示回车换行,’\t’表示tab。由于以’\’开头的字符表示转义,因此要想表示’\’字符本身,就约定用’\’来转义自己,即’\\’表示字符’\’。转义字符虽然表示的是单个字符的意义,在编译器眼里转义字符是多个字符组成的字符串,比如’\n’是字符’\’和’n’组成的字符串,好啦,交待完毕。

起初的c编译器中并没有处理转义字符,为叙述方便,我们现在称之为老编译器。如果待编译的代码文件中有字符串’\\’,这在老编译器眼里就是’\\’字符串,并不是转义后的单个字符’\’。为了表明编译器与做为其输入的代码文件的关系,我们称做为输入的代码文件为应用程序文件,尽管被编译的代码文件是实现了一个编译器,而在编译器眼里,它只是一个应用程序级的角色。例如,gcc –c a.c中,a.c就是应用程序文件。

现在想在编译器中添加对转义字符的支持,那就需要修改老编译器的源代码,假设老编译器的源代码文件名为compile_old.c。被修改后的编译器代码,已不属于老编译器的源代码,故我们命名其文件名为compile_new_a.c,下面是修改后的内容。

程序是用编程语言写的,编译器也是程序,先有编译器还是编程语言

代码compile_new_a.c

其中,函数next()的功能是返回待处理文本(即被编译的源码文件)中的下一字符,强调一下是单个字符,并不是记法分析中的单词(即token)。

用老编译器将新编译器的源代码compile_new_a.c编译,生成可执行文件,该文件就是新的编译器,我们取名为新编译器_a。为了方便理清他们的关系,将他们列入表格中。

编译器自身源代码编译器应用程序源代码输出文件名
compile_old.c老编译器compile_new_a.c新编译器_a,支持’\\’

这下编译出来的新编译器_a可以编译含有转义字符’\\’的应用程序代码了,也就是说,待编译的文件(也就是应用程序代码)中,应该用’\\’来表示’\’。而单独的字符’\’在新编译器_a中未做处理而无法通过编译。所以此时新编译器_a是无法编译自己的源代码compile_new_a.c的,因为该源文件中只是单个’\’字符,新编译器_a只认得’\\’。

先更新他们的关系,见下表。

编译器自身源代码编译器应用程序源代码输出文件名
compile_old.c老编译器compile_new_a.c新编译器_a,支持’\\’
compile_new_a.c新编译器_acompile_new_a.c编译失败

也就是说,现在新编译器_a,无法编译自己的源文件compile_new_a.c,只有老编译器才能编译它。再啰嗦一下,新编译器_a无法正确编译自己的源文件compile_new_a.c的原因是,compile_new_a.c中’\’字符应该用转义字符的方式来引用,即所有用’\’的地方都应该替换为’\\’。再回头看一下新编译器_a的源代码compile_new_a.c,它只处理了字符串’\\’,单个’\’没有对应的处理逻辑。下面修改代码,将新修改后的代码命名为compile_new_b.c。

程序是用编程语言写的,编译器也是程序,先有编译器还是编程语言

代码 compile_new_b.c

其实compile_new_b.c只是更新了转义字符的语法,这是新编译器_a所支持的新的语法,下面还是以新编译器_a来编译新的编译器。

用新编译器_a编译此文件,将生成新编译器_b,将新的关系录入到表格中。

编译器自身源代码编译器应用程序源代码输出文件名
compile_old.c老编译器compile_new_a.c新编译器_a,支持’\\’
compile_new_a.c新编译器_acompile_new_a.c编译失败
compile_new_a.c新编译器_acompile_new_b.c新编译器_b,支持’\\’

继续之前啰嗦两句:用编译器去编译另一编译器的源码,也许有的同学觉得很费解,其实您把被编译的编译器源码当成普通的应用程序源码就特别容易理解了。上面的编译器代码compile_new_b.c,其第3、6、7行的字符串’\\’被新编译器_a处理后,会以单字符’\’来代替(这是新编译器_a源码中return语句的功能),因此最终处理完成后的代码等同于代码compile_new_a.c。

现在想加上换行符’\n’的支持:

程序是用编程语言写的,编译器也是程序,先有编译器还是编程语言

由于现在编译器还不认识’\n’,故这样做肯定不行,不过可以用其ascii码来代替,将其命名为compile_new_c.c。

程序是用编程语言写的,编译器也是程序,先有编译器还是编程语言

compile_new_c.c

用新编译器_a来编译compile_new_c.c,将生成新编译器_c,新编译器_c的代码相当于代码compile_new_c.c中所有’\\’被替换为’\’后的样子,如下所示,暂且称之为代码compile_new_c1.c:

程序是用编程语言写的,编译器也是程序,先有编译器还是编程语言

代码compile_new_c1.c

编译器自身源代码编译器应用程序源代码输出文件名
compile_old.c老编译器compile_new_a.c新编译器_a,支持’\\’
compile_new_a.c新编译器_acompile_new_a.c编译失败
compile_new_a.c新编译器_acompile_new_b.c新编译器_b,支持’\\’
compile_new_a.c新编译器_acompile_new_c.c新编译器_c,间接支持\n

最后再修改compile_new_c.c为compile_new_d.c,将10用’\n’替代。

程序是用编程语言写的,编译器也是程序,先有编译器还是编程语言

代码compile_new_d.c

用新编译器_c编译compile_new_d.c,生成新编译器d,将直接识别’\n’。同理,新编译器d的代码相当于代码compile_new_d.c中,所有字符串’\\’被替换为字符’\’、字符’\n’被替换为数字10后的样子,即等同于代码compile_new_c1.c。

编译器自身源代码编译器应用程序源代码输出文件名
compile_old.c老编译器compile_new_a.c新编译器_a,支持’\\’
compile_new_a.c新编译器_acompile_new_a.c编译失败
compile_new_a.c新编译器_acompile_new_b.c新编译器_b,支持’\\’
compile_new_a.c新编译器_acompile_new_c.c新编译器_c,间接支持\n
compile_new_c.c新编译器_ccompile_new_d.c新编译器d,直接支持\n

编译器经过这样不断的训练,功能越来越强大,不过体积也越来越大了。

累死哥了。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多