编译器的学习笔记
以前一直觉得编译器的内容过于底层和抽象,在如今高级语言流行的今天,掌握编译器的细节并没有很大的价值。不过,这一年多的工作经验让我觉得,知其然而且知其所以然是很关键的,而且对于做技术的人来说,长远的pay off会更大。记得自己有时候在编程或者调试的时候被一些小问题卡住,一卡就是一下午。但是组里的senior打眼一看就大约知道问题是什么,如何解决。所以说,在工作的初期,还是要求根问底,把基础打牢,才能在后期有大幅度的飞跃。
扯回编译器,我学习的资料主要是standford的complier这门课,根据我目前的感觉,这个online course的讲解是我见过的最好的编译器课程,语言很清晰,而且有非常清晰的板书和过程演示,非常非常适合自学者的使用。https://www.coursera.org/course/compilers
写一个编译器,很重要的两个方面是语法的解析以及编译指令的生成。之前自己在UW读书的时候,上过一门函数式编程的课,其实期末的大作业就是写一个小语言,并且实现了类似于语法解析的函数,所以这门课我就跳过了syntax的部分(而且那部分相对来说比较枯燥,我比较喜欢与机器和指令相关的内容,这样对于对自己代码的理解有很好的帮助)。
Activation Record: 不知道怎么翻译成中文。基本上就是在函数调用的时候存在stack上的一个结构。内容包括: result (返回值),arg(参数),control link(子方法调用的action record),return address(下一条机器指令的内存地址)。当每次一个function被invoke的时候,一个activation record就会被push到stack上去,写入arg,control link和return address。很多时候连续调用的过程中产生的action record往往在内存中是连续的,有些编译器就可以通过强制这种结构来玩一些高效的指令操作来读取activation record. 当然,这不是唯一的activation record的设计方式,很多activation record没有control link因为他们通过固定的位置移动就可以得到caller的activation record;有的ac没有return value/arg的部分,因为他们直接把这些反复读取的值写到寄存器中等等。总的来说,activation的作用就是提供一个结构,hold sufficient data for both callees to execute precedure and caller to resume previous execution. 就是这么一个承接作用,但是它非常重要,需要和code generation的部分一起设计实现。
程序的内存分布:如果画一个长方形来表示内存使用段,上边的是low address,下边代表high address。依次从上到下的内存使用部分分别是:code(指令),static data,stack(一般来说stack向high address grow),最后是heap,不过heap是从低边向low address grow。stack和heap相对grow,但他们相遇的时候就是程序的内存用光之时。
一次完整的过程调用: 调用者把args/old frame pointer(这些东西根据具体的AR设计有所不同) push到stack中,然后jump && link到被调用过程的指令地址中。被调用过程会先push return address(exit后的指令地址)到堆栈,然后执行具体的过程内指令。当过程需要返回的时候,那么重置frame pointer/堆栈指针,然后根据返回地址跳转到下一条指令,stack与执行过程前保持一致(preserve the stack)。
对象在内存中的布局:一个对象的所有数据在内存中是连续存放的,其中对象包含的属性存放于相对于对象起始地址的一个固定便宜位置,这样可以保证对象方法以及子类的方法可以永久的正确读取对象属性值。具体来说,在课程上讲解的简易语言的内存对象数据包括,一个class tag,unique identifier,一个object size,一个dispatch pointer,指向类的方法表地址,具体的attributes。对于类的继承,比如类B继承了类A,那么类B对象中的类A布局完全不变,只是在其后加入了类B独有的属性。对于dispatch table,类的方法也是一个固定的相对于table起点的offset,而且这个offset对于所有继承它的subclass也成立。
浙公网安备 33010602011771号