1. 词法分析

词法分析器根据词法规则识别出源程序中的各个记号(token),每个记号代表一类单词(lexeme)。源程序中常见的记号可以归为几大类:关键字、标识符、字面量和特殊符号。词法分析器的输入是源程序,输出是识别的记号流。词法分析器的任务是把源文件的字符流转换成记号流。本质上它查看连续的字符然后把它们识别为“单词”。

2. 语法分析

语法分析器根据语法规则识别出记号流中的结构(短语、句子),并构造一棵能够正确反映该结构的语法树。

3. 语义分析

语义分析器根据语义规则对语法树中的语法单元进行静态语义检查,如果类型检查和转换等,其目的在于保证语法正确的结构在语义上也是合法的。

4. 中间代码生成

中间代码生成器根据语义分析器的输出生成中间代码。中间代码可以有若干种形式,它们的共同特征是与具体机器无关。最常用的一种中间代码是三地址码,它的一种实现方式是四元式。三地址码的优点是便于阅读、便于优化。

5. 中间代码优化

优化是编译器的一个重要组成部分,由于编译器将源程序翻译成中间代码的工作是机械的、按固定模式进行的,因此,生成的中间代码往往在时间和空间上有很大浪费。当需要生成高效目标代码时,就必须进行优化。

6. 目标代码生成

目标代码生成是编译器的最后一个阶段。在生成目标代码时要考虑以下几个问题:计算机的系统结构、指令系统、寄存器的分配以及内存的组织等。编译器生成的目标程序代码可以有多种形式:汇编语言、可重定位二进制代码、内存形式。

7 符号表管理

符号表的作用是记录源程序中符号的必要信息,并加以合理组织,从而在编译器的各个阶段能对它们进行快速、准确的查找和操作。符号表中的某些内容甚至要保留到程序的运行阶段。

8 出错处理

用户编写的源程序中往往会有一些错误,可分为静态错误和动态错误两类。所谓动态错误,是指源程序中的逻辑错误,它们发生在程序运行的时候,也被称作动态语义错误,如变量取值为零时作为除数,数组元素引用时下标出界等。静态错误又可分为语法错误和静态语义错误。语法错误是指有关语言结构上的错误,如单词拼写错、表达式中缺少操作数、begin和end不匹配等。静态语义错误是指分析源程序时可以发现的语言意义上的错误,如加法的两个操作数中一个是整型变量名,而另一个是数组名等。

第二篇摘自:http://jpkc.nwpu.edu.cn/dzjc/jsjrj/text/chapter01/section01/r2a.htm

计算机能读懂的语言是机器码,但对人来说由1和0组合的二进制序列既难写又难读。于是出现了用英文字母代表操作码的汇编语言,汇编语言是机器语言的符号化,汇编语言是面向机器的,使用汇编语言编程需要直接安排存储,规定寄存器、运算器的动作次序,汇编语言与计算机紧密相关,不同的计算机在指令长度、寻址方式、寄存器数目、指令表示等方面都不一样,由于汇编语言不便于进行数学描述,而且不可移植,于是出现了高级语言。高级语言是面向计算过程的和面向问题的语言,只与解题的步骤有关,而将高级程序设计语言"翻译"成机器语言的工作则是由编译程序来完成的。程序员的工作则是把要计算的问题化成高级程序设计语言的表达式、语句、过程/函数、对象,而不是机器指令序列。
  把高级语言程序翻译成机器语言程序有两种做法:编译和解释,相应的翻译工具也分别叫做编译器和解释器。以下分别讲述。
1.编译器工作原理
  编译器逐行扫描高级语言程序源程序,编译的过程如下:
(1)词法分析(Lexical Analysis)。识别关键字、字面量、标识符 (变量名、数据名)、运算符、注释行(给人看的,一般不处理)、特殊符号(续行、语句结束、数组)等六类符号,分别归类等待处理。
(2)语法分析 (Syntax Analysis)。一个语句看作一串记号 (Token)流,由语法分析器进行处理。按照语言的文法检查判定是否是合乎语法的句子。如果是合法句子就以内部格式保存,否则报错。直至检查完整个程序。
(3)语义分析 (Semantic Analysis)。语义分析器对各句子的语法做检查:运算符两边类型是否相兼容;该做哪些类型转换 (例如,实数向整数赋值要"取整");控制转移是否到不该去的地方;是否有重名或者使语义含糊的记号,等等。如果有错误,则转出错处理,否则可以生成执行代码。
(4)中间代码生成。中间代码是向目标码过渡的一种编码,其形式尽可能和机器的汇编语言相似,以便下一步的代码生成。但中间码不涉及具体机器的操作码和地址码。采用中间码的好处是可以在中间码上做优化。
(5)优化。对中间码程序做局部优化和全局 (整个程序)优化,目的是使运行更快,占用空间最小。局部优化是合并冗余操作,简化计算,例如x:=0可用一条"清零"指令替换。全局优化包括改进循环、减少调用次数和快速地址算法等。
(6)代码生成。由代码生成器生成目标机器的目标码 (或汇编)程序,其中包括数据分段、选定寄存器等工作,然后生成机器可执行的代码。
  高级语言源程序经编译后得到目标码程序,还不能立即装入机器执行,因为程序中如果用到标准函数(它们生成的目标码已存放在模块库中),还需对编译后得到的目标模块进行连接。连接程序 (Linker)找出需要连接的外部模块,然后到模块库中找出被调用的模块,调入内存并连接到目标模块上,形成可执行程序。执行时,把可执行程序加载 (Loading)到内存中合适的位置 (此时得到的是内存中的绝对地址),就可执行了。其示意图为图1.1所示     图1.1 编译、连接和执行程序的过程 2.高级语言程序的解释执行
  编译型语言由于可进行优化 ( 有的编译器可做多次优化 ) ,目标码效率很高,因此是目前软件开发的最主要编程语言。常见的程序设计语言,如 C/C++ 、 Pascal 、 FORTRAN 等都是编译型语言,用这些语言编写的源程序,都需要进行编译、连接,才能生成可执行程序。这对于大型程序、系统程序、支持程序来说是十分有利的,虽然编译时花费了不少时间,但程序的执行效率是很高的。不过,在有些场合,对程序的执行效率要求不高的场合,没有必要在编译上花费大量的时间,可以对高级语言源程序采取解释执行的方式。
  解释执行需要有一个解释器 (Interpreter) ,它将源代码逐句读入。第一步先作词法分析,建立内部符号表;再作语法和语义分析,并作类型检查 ( 解释语言的语义检查一般比较简单,因为它们往往采用无类型或动态类型系统 ) 。完成检查后把每一语句压入执行堆栈,并立即解释执行。因为解释执行时只看到一个语句,无法对整个程序进行优化。但是解释执行占用空间很少。
  操作系统的命令、Visual Basic、Java、JavaScript 都是解释执行的 ( 其中有些语言也可以编译执行 ) 。解释器不大,工作空间也不大,不过,解释执行难于优化、效率较低,这是这类语言的致命缺点 。 第三篇摘自:http://apps.hi.baidu.com/share/detail/32154894 《Visual C 编译器原理》 Jan Gray在1994曾经写了一篇叫做C++ under the Hood的文章,介绍了Visual C++的实现细节。这篇指南就是基于Jan的文章之上,我同时会将Jan文章中让人难于理解的地方详细阐述。希望这篇指南可以让更多的人了解C++的底层实现机制。 The layout of a Class struct B {
public:
int bm1;
protected:
int bm2;
private:
int bm3;
};
Struct B 在内存中的layout是怎么样的? Visual C++保证B中的member variables 在内存中的layout与它们生命的顺序一致。Struct B在内存的中layout应该是这个样子的: Single Inheritance struct C {
int c1;
void cf();
};
struct D : C {
int d1;
void df();
};
在Visual C++中保证在C的member variables 在内存中的位置永远在D的起始位置。就像这样: 这样做的好处是当C* pC = new D();Visual C++不需要为pC做额外的displacement 转换。pC 的address equal D* pD = new D();中的pD. Multiple Inheritance 比较复杂:
struct E {
int e1;
void ef();
};
struct F : C, E {
int f1;
void ff();
};
多重继承比较复杂,他们的Base和Derived的指针的位置不再相同。
F f;
// (void*)&f == (void*)(C*)&f;
// (void*)&f < (void*)(E*)&f;
通过如下的Diagram of layout你可以看得更加清楚: 为什么在图中C在E的上面?这是Visual C++ 的convention罢了,基类在内存中的layout correspond to 他们的的声明顺序。因为C的声明在E的前面,所以我们看到的F在内存的layout就是这样子的。
由此图可知,E *pE = new F() 与C *pC = new F()中的pE 和pC指向的内存位置并不相同,对于pC 来说compiler不需要额外做任何事情,但是对于pE,为了让它指向E在内存中的位置compiler需要进行一种叫做displacement的调整。 Virtual Inheritance 请考虑这种情形:
struct Employee { ... };
struct Manager : Employee { ... };
struct Worker : Employee { ... };
struct MiddleManager : Manager, Worker { ... }; 无疑,按照我们之前的叙述,MiddleManager在内存中的layout应该是这个样的: 在内存中的有两个Employee的实例,如何Employee 很小那么这种冗余是可以忽略的,可是如果Employee很大呢? 那么有没有什么方法可以让Manager 和Worker在内存中共享同一个Instance呢?这就是Virtual Inheritance需要解决的问题。
在享受这种优化的服务之前,你应该将你的类体系结构编程这样:
struct Employee { ... };
struct Manager : virtual Employee { ... };
struct Worker : virtual Employee { ... };
struct MiddleManager : Manager, Worker { ... }; 也就是在希望被sharing 的基类前面加上Virtual关键字,多么直观啊。
struct G : virtual C {
int g1;
void gf();
};
struct H : virtual C {
int h1;
void hf();
};
struct I : G, H {
int i1;
void _if();
}; 之后你的类在内存中的就应该是这个样子: 其中vbptr中存储的是对Employee的相对displacement.
Data Member Access
在没有继承的情形:
C* pc;
pc->c1; // *(pc + dCc1); c1 的访问类似于*(pC + displacement of c1 within C);在本例子中根据Class C的定义和Diagram of layout我们可以发现displacement == 0.
在单继承的情形中:
D* pd;
pd->c1; // *(pd + dDC + dCc1); // *(pd + dDCc1);
pd->d1; // *(pd + dDd1); 根据我们之前的Diagram不难看出pd->c1 == *(pd + displacement from D to C + displacement from C to c1).这种情形中displacement == 0。
pd->d1 == *(pd + displacement from D to d1). 这种情形中 displacement == 4。
在多重继承中,情形稍微复杂些,但所有的displacement 还都是常量(constant)。
F* pf;
pf->c1; // *(pf + dFC + dCc1); // *(pf + dFc1);
pf->e1; // *(pf + dFE + dEe1); // *(pf + dFe1);
pf->f1; // *(pf + dFf1);
我想何以根据我们之前的Diagram轻松的算出每一个displacement。
虚拟继承又是怎么的呢?
I* pi;
pi->c1; // *(pi + dIGvbptr + (*(pi+dIGvbptr))[1] + dCc1);
pi->g1; // *(pi + dIG + dGg1); // *(pi + dIg1);
pi->h1; // *(pi + dIH + dHh1); // *(pi + dIh1);
pi->i1; // *(pi + dIi1);
I i;
i.c1; // *(&i + IdIC + dCc1); // *(&i + IdIc1); 对g1,h1,以及i1的访问很容易理解,我想说说对c1的访问。
pi->c1是一种动态的访问。在runtime的时候编译器不知道pi的真正type是什么,这时就要用到之前说过的vbptr,(*(pi + dIGvbptr))[1]是指在特定的vbptr中(不论vbptr是属于 G还是H)其对于base virtual class的偏移地址。至于为什么是(*(pi + dIGvbptr))[1] 而不是 (*(pi + dIGvbptr))[0],我猜这也是Visual C++的设计使然吧。 如果你知道(*(pi + dIGvbptr))[0]中放的什么,请让我知道? br />对于i.c1的访问,因为这是一种静态的访问,为了节省开销C++对它的处理直接而干脆。之所以C++敢于这么做是因为在I中displacement of i在这种静态声明中是固定不变的。 Casts 理解了以上概念相信Casts between 2 types就不是什么问题了,一下是我们常见的一些cast在Visual C++中的实现手段。
对于多重继承来说:
F* pf;
(C*)pf; // (C*)(pf ? pf + dFC : 0); // (C*)pf;
(E*)pf; // (E*)(pf ? pf + dFE : 0);
对于虚拟继承来说:
I* pi;
(G*)pi; // (G*)pi;
(H*)pi; // (H*)(pi ? pi + dIH : 0);
(C*)pi; // (C*)(pi ? (pi+dIGvbptr + (*(pi+dIGvbptr))[1]) : 0); 什么,没看懂?那么就再看一遍我对Data Member Access的描述吧。
Member Functions
struct P {
int p1;
void pf(); // new
virtual void pvf(); // new
};
对于一个non-static 成员变量的访问应该是这样的(我想因该大部分程序员都会了解吧)member function被调用的的时候会被传入一个this指针他的类型是:
Type X * const。(有人想过为什么是会是这样的声明而不是const Type X * const 或者const Type X *么?
如果声明为const Type X *那么我们将无法通过this指针修改member variables。至于const Type X * const么实际上当你 将pf定义成:void pf() const;那么传入的this就是const Type X * const的。通过Type X * const 我们不能擅自修改this指针本身,不信你试试。)
所以对于pf的调用实际上应该是这个样子的:
void P::pf() { // void P::pf([P *const this])
++p1; // ++(this->p1);
} Overriding Member Functions 考虑以下声明:
struct Q : P {
int q1;
void pf(); // overrides P::pf
void qf(); // new
void pvf(); // overrides P::pvf
virtual void qvf(); // new
};
Overridden member function包括 static 和 dynamic 调用。在C++中使用virtual关键字来区分。
情形1:static resolution:
当一个member function被重写且没有virtual那么,对他的调用在compiling 的时候就已经determined.
P p; P* pp = &p; Q q; P* ppq = &q; Q* pq = &q;
pp->pf(); // pp->P::pf(); // P::pf(pp);
ppq->pf(); // ppq->P::pf(); // P::pf(ppq);
pq->pf(); // pq->Q::pf(); // Q::pf((P*)pq);
pq->qf(); // pq->Q::qf(); // Q::qf(pq); 当pp->pf() 以及 ppq->pf()这两种情形,调用它们的指针类型在compiling是就已经安插。因为没有Virtual 那么就没有多态的干扰,Visual C++将忠实于->运算符左侧的类型,并且将此类型作为this传入此函数。 情形 2:dynamic resolution:
pp->pvf(); // pp->P::pvf(); // P::pvf(pp);
ppq->pvf(); // ppq->Q::pvf(); // Q::pvf((Q*)ppq);
pq->pvf(); // pq->Q::pvf(); // Q::pvf((P*)pq); 可怜的C++编译器,将如何决议overridden member function 的类型呢?为了解决这个问题vfptr被引入。
通常被安插在memory layout的第一个位置,它指向此class的 vftable。 Vftable中存储的是所有virtual functions的地址。就像这样: 当子类重写了父类的方法那么vftable中相应的entry 就应该被改写,如图: C++就是通过这种方式来进行overridden member function 的dynamic resolution。 Virtual Functions: Multiple Inheritance 这是本指南最刺激和有趣的一部分,我要向你介绍著名的Thunk技术。
考虑一下情形:
struct R {
int r1;
virtual void pvf(); // new
virtual void rvf(); // new
};
struct S : P, R {
int s1;
void pvf(); // overrides P::pvf and R::pvf
void rvf(); // overrides R::rvf
void svf(); // new
}; 这样的layout应该如何画?我猜是这样的: S s; S* ps = &s;
((P*)ps)->pvf(); // ((P*)ps)->P::vfptr[0])((S*)(P*)ps)
((R*)ps)->pvf(); // ((R*)ps)->R::vfptr[0])((S*)(R*)ps)
当我lunching以上两种调用,我所期望的的函数语义应该是就像每个函数注释后面的一样。毕竟->运算符左侧的是一个S*对吧,所以传入member function的指针也应该是S*。当使用P*是问题很简单,P*和S*指向的是相同的内存地址,C++ compiler不需要做任何事情。但是当使用R*后有点问题,R*和S*指向的内存地址不同。那么我们就要使用一些技巧让R*指针转化为S*。对于这个问题的解决办法基本上就是使用一种叫做Thunk的技术。重写 entry of pvf within vftable。
重写的方法很多,在VC++中重写后的结果像这样: S::pvf-adjust: // MSC++
this -= SdPR;
goto S::pvf()
呵呵,很简单是么,将原先指向R*的this指针- displacement of S from R, 然后jump 到真正的S::pvf()的函数地址中。 Constructors and Destructors Constructor 和 Destructor我们常见,但是不能使用。通常有compiler将其分解成为多部构造。
Constructor 被分解后应该是这样的:
1)对于一个most derived类,初始化vbptr,并调用virtual base 的构造函数。
2)调用non-virtual base classes 的构造函数。
3)调用data members的构造函数
4)初始化vfptr。
5)执行用户写在constructor中的代码。
Destructor被分解后应该是这样的:
1) 初始化vfptr
2) 执行用户卸载destructor中的代码。
3) 调用data member 的析构函数,顺序是与data member 在类中声明的顺序相反。
4) 调用non-virtual bases的析构函数,与声明的顺序相反。
5) 对于一个most derived 的类,调用它的virtual base的析构函数。

  

compiler related的更多相关文章

  1. July 27th 2017 Week 30th Thursday

    A smile is the most charming part of a person forever. 微笑永远是一个人身上最好看的东西. Smile in the mirror, and yo ...

  2. ncnn阅读 - CMakeLists.txt

    CMAKE_TOOLCHAIN_FILE This variable is specified on the command line when cross-compiling with CMake. ...

  3. How a C++ compiler implements exception handling

    Introduction One of the revolutionary features of C++ over traditional languages is its support for ...

  4. The JSR-133 Cookbook for Compiler Writers(an unofficial guide to implementing the new JMM)

    The JSR-133 Cookbook for Compiler Writers by Doug Lea, with help from members of the JMM mailing lis ...

  5. The Win32 Rundll and Rundll32 Interface Related Topics

    The Win32 Rundll and Rundll32 Interface Related Topics Microsoft Knowledge Base Article Q164787 Appl ...

  6. Language and Compiler Features Since Delphi 7

    from: http://edn.embarcadero.com/cn/article/34324 Language and Compiler Features Since Delphi 7   In ...

  7. Java programming language compiler

    https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javac.html\ javac - Java programming l ...

  8. The IDL compiler

    The IDL compiler or bindings generator transcompiles Web IDL to C++ code, specifically bindings betw ...

  9. Delphi For Linux Compiler

    Embarcadero is about to release a new Delphi compiler for the Linux platform. Here are some of the k ...

随机推荐

  1. 100个linux系统常用指令

    1.ls [选项] [目录名 | 列出相关目录下的所有目录和文件 -a 列出包括.a开头的隐藏文件的所有文件-A 通-a,但不列出"."和".."-l 列出文件 ...

  2. 生产环境LAMP搭建 - 基于 fastcgi

    生产环境LAMP搭建 - 基于 fastcgi 由于在module模式,php只是已http的模块形式存在,无形中加重了http的服务负载,通常在企业架构中,使用fastcgi的模式,将所有的服务都设 ...

  3. CentOS Linux release 7.6.1810全新安装 Zimbra 8.8.12邮箱

    1.1  基础环境配置 1.1.1  主机名配置 [root@mail ~]# hostnamectl --static set-hostname mail.example.com [root@mai ...

  4. GTF/GFF

  5. c语言——字符串

    char str[] = "http://c.biancheng.net"; char str[] = "http://c.biancheng.net"; ch ...

  6. A计划 hdu2102(BFS)

    A计划 hdu2102 可怜的公主在一次次被魔王掳走一次次被骑士们救回来之后,而今,不幸的她再一次面临生命的考验.魔王已经发出消息说将在T时刻吃掉公主,因为他听信谣言说吃公主的肉也能长生不老.年迈的国 ...

  7. Docker从零到实践过程中的坑

    欢迎指正: Centos7 下的ulimit在Docker中的坑 http://www.dockone.io/article/522 僵尸容器:Docker 中的孤儿进程 https://yq.ali ...

  8. 栈经典列题:Rails

    解题心得: 1.这题是先进后出的顺序,所以使用栈(先进后出表). 2.搞清楚题意,需要达成的序列和进入的序.不要弄混了. 3.思维混乱的时候要冷静,冷静,冷静~~~~! 题目: Description ...

  9. 水题:HDU1034-Candy Sharing Game

    解题心得: 1.我是用的模拟的算法直接模拟的每一轮的分配方法的得出的答案,看到有些大神使用的链表做的,好像链表才是这道题的镇长的做法吧. 题目: Candy Sharing Game Time Lim ...

  10. mybatis特殊字符处理

    在mybatis 的mapper.xml文件中特殊字符处理方式  仅供参考 出处:http://yaobenzhang.blog.163.com/blog/static/214395113201561 ...