Ⅱ.楔子 -- C♭和 cbc

发布于:2025-06-13 ⋅ 阅读:(19) ⋅ 点赞:(0)

前言

编译器可将C♭这种语言编译为机器语言。首先对C♭语言的概要进行简单的说明

一、C♭语言的概要

1.C♭的Hello, World!

C♭ 是C语言的简化版,省略了C语言中琐碎的部分以及难以实现、容易混淆的功能,实现起来条理更加清晰。虽然如此,C♭仍保留了包括指针等在内的C语言的重要部分。因此,理解了C♭的编译过程,也就相当于理解了C程序的编译过程。
让我们再来看一下用C♭语言编写的Hello,World!程序,代码如下:

import stdio;
 int
 main(int argc, char **argv)
 {
    printf("Hello, World!\n");
    return 0;
 }

可见该程序和C语言几乎没有差别,不同之处只是用import替代了#include,仅此而已哦。

2.C♭中删减的功能

为了使编译器的处理简明扼要,下面这些C语言的功能不会出现在C♭中。

● 预处理器
● K&R语法
● 浮点数
● enum
● 结构体(struct)的位域(bit field)
● 结构体和联合体(union)的赋值
● 结构体和联合体的返回值
● 逗号表达式
● const
● volatile
● auto
● register
简单地说一下删除上述功能的原因。
首先,C♭同C语言最大的差异在于C♭没有预处理器。认真地制作C语言的预处理器会花费过多的时间和精力,进而无法专注于主题——编译器。
但是,因为省略了预处理器,所以C♭无法使用#define和#include。特别是不能使用#include,将无法导入类型定义和函数原型,这是有问题的。为了解决该问题,C♭使用了与Java 类似的import关键字。import关键字的用法将稍后说明。
数据类型方面也做了一些变化。
首先,删除了和浮点数相关的所有功能。
其次,由于C语言的enum和生成名称连续的int型变量的功能本质上无太大区别,因此为了降低编译器实现的复杂度,这里将其删除。至于结构体和联合体,主要也是考虑到编译器的复杂度,才删除了类似的使用频率不高或非核心的功能。
volatile和const还是比较常用的,但因为cbc几乎不进行优化,所以volatile本身并没有太大意义。const可以有条件地用数字字面量和字符串字面量来实现。
最后,auto和register不仅使用频率低,而且并非必要,所以将其也删除了。

3.import关键字

这就对C♭中新增的import关键字进行说明。
C♭ 在语法上和C语言稍有差异,而且没有预处理器,所以不能直接使用C语言的头文件。
为了能够从外部程序库导入定义,C♭提供了import关键字。import的语法如下:

import 导入文件ID;

下面是具体的示例。

import stdio;
import sys.params;

导入文件类似于C语言中的头文件,记载了其他程序库中的函数、变量以及类型的定义。
cbc 中有stdio.hb、stdlib.hb、sys/params.hb等导入文件,当然也可以自己编写导入
文件。
导入文件的ID是去掉文件名后的“.hb”,并用“.”取代路径标识中的“\”后得到的。
例如导入文件stdio.hb的ID为stdio,导入文件sys/params.hb的ID为sys.params。

4.导入文件的规范

下面让我们看一个导入文件的例子吧,cbc中的stdio.hb的内容如下:

// stdio.hb
 import stddef;  // for NULL and size_t
 import stdarg;
 typedef unsigned long FILE;   // dummy
 extern FILE* stdin;
 extern FILE* stdout;
 extern FILE* stderr;
 extern FILE* fopen(char* path, char* mode);
 extern FILE* fdopen(int fd, char* mode);
 extern FILE* freopen(char* path, char* mode, FILE* stream);
 extern int fclose(FILE* stream);
:
:

只有下面这些声明能够记述在导入文件中。
● 函数声明
● 变量声明(不可包含初始值的定义)
● 常量定义(这里必须有初始值)
● 结构体定义
● 联合体定义
● typedef
函数及变量的声明必须添加关键字extern。并且在C♭中,函数返回值的类型、参数的类型、参数名均不能省略。

二、C♭编译器cbc的构成

阅读有一定数量的代码时,首先要做的就是把握代码目录以及文件的构成。将对本书制作的C♭编译器cbc的代码构成进行一下说明。

1.cbc的代码树

cbc 采用Java标准的目录结构,即域名倒序,将倒序后的域名作为包(package)名的前缀,按层次排列。比如,域名是blog.csdn.net,则包名以net/csdn开头,接着是程序的名称blog,其下面排列着cbc所用的包。

2.cbc的包

包 包中的类
asm 汇编对象的类
ast 抽象语法树的类
compiler Compiler 类等编译器的核心类
entity 表示函数和变量等实体的类
exception 异常的类
ir 中间代码的类
parser 解析器类
sysdep 包含依赖于OS的代码的类(汇编器和链接器)
sysdep.x86 包含依赖于OS和CPU的代码的类(代码生成器)
type 表示C♭的类型的类
utils 小的工具类
在这些包之中,asm、ast、entity、ir、type这5个包可以归结为数据相关(被操作)的类。另一方面,compiler、parser、sysdep、sysdep.x86这4个包可以归结为处理相关(进行操作的一方)的类。
把握代码整体结构时最重要的包是compiler包,其中基本收录了cbc编译器前端的所有内容。例如,编译器程序的入口函数main就定义在compiler包的Compiler类中。

3.compiler包中的类群

我们先来看一下compiler包中的类:

类名 作用
Compiler 统管其他所有类的facade类(compiler driver)

Visitor
DereferenceChecker
LocalReferenceResolver 语义分析相关的类(第9章)
TypeChecker
TypeResolver

IRGenerator 从抽象语法树生成中间代码的类(第11章)

Compiler类是统管cbc的整体处理的类。编译器的入口函数main也在Compiler类中定义。
从Visitor类到TypeResolver类都是语义分析相关的类。
最后,IRGenerator是将抽象语法树转化为中间代码的类。

4.main函数的实现

最后,我们一起来大概地看一下Compiler类的代码。Compiler类中main函数的代码如下:

 static final public String ProgramName = "cbc";
    static final public String Version = "1.0.0";
    static public void main(String[] args) {
        new Compiler(ProgramName).commandMain(args);
    }
    private final ErrorHandler errorHandler;
    public Compiler(String programName) {
        this.errorHandler = new ErrorHandler(programName);
    }

main 函数中,通过new Compiler(ProgramName)生成Compiler对象,将命令行参数args传递给commandMain函数并执行。ProgramName是字符串常量"cbc"。
Compiler类的构造函数中,新建ErrorHandler对象并将其设为Compiler的成员。之后,在输出错误或警告消息时使用该对象。

5.commandMain函数的实现

接着来看一下负责cbc主要处理的commandMain函数。原本的代码中包含较多异常处理的内容,比较繁琐,因此这里只列举主要部分:

     public void commandMain(String[] args) {
        Options opts = Options.parse(args);
        List<SourceFile> srcs = opts.sourceFiles();
        build(srcs, opts);
    }

commandMain 函数中,首先用Options类的parse函数来解析命令行参数args,并取得SourceFile对象的列表(list)。一个SourceFile对象对应一份源代码。实际的build部分,是由build函数来完成的。
Options 对象中的成员如下:
在这里插入图片描述
Options对象中还定义有其他成员和函数,因为只和代码生成器、汇编器、链接器相关,所以等介绍上述模块时再进行说明。

6.Java5泛型

可能大家对List这样的表达式还比较陌生,所以这里解释一下。
List表示“成员的类型为SourceFile的列表”,简单地说就是“SourceFile对象的列表”。到J2SE 1.4为止,还不可以指定List、Set等集合中元素对象的类型。从Java 5开始,才可以通过集合类名<成员类名>来指定元素成员的类型。
通过采用这种写法,Java编译器就知道元素的类型,在取出元素对象时就不需要进行类型转换了。
这种能够对任意类型进行共通处理的功能称为泛型。在Java5新增的功能中,泛型使用起来尤其方便,是不可缺少的一项功能。

7.build函数的实现

我们继续看负责build代码的build函数,其代码大概如下:

         public void build(List<SourceFile> srcs, Options opts)
                                        throws CompileException {
        for (SourceFile src : srcs) {
            compile(src.path(), opts.asmFileNameOf(src), opts);
            assemble(src.path(), opts.objFileNameOf(src), opts);
        }
        link(opts);
    }

首先,用foreach语句将SourceFile对象逐个取出,并交由compile函数进行编译。compile函数是对单个C♭文件进行编译,并生成汇编文件的函数。
接着,调用assemble函数来运行汇编器,将汇编文件转换为目标文件。
最后,使用link函数将所有的对象文件和程序库链接。
可见上述代码和第1章节中叙述的build的过程是完全对应的。

8.compile函数的实现

最后我们来看一下负责编译的compiler函数的代码。剩余的assemble函数和link函数将在的第4部分进行说明。
compiler函数中也有用于在各阶段处理结束后停止处理的代码等,多余的部分比较多,所以这里将处理的主要部分提取出来,如下:

    public void compile(String srcPath, String destPath,
                        Options opts) throws CompileException {
        AST ast = parseFile(srcPath, opts);
        TypeTable types = opts.typeTable();
        AST sem = semanticAnalyze(ast, types, opts);
        IR ir = new IRGenerator(errorHandler).generate(sem, types);
        String asm = generateAssembly(ir, opts);
        writeFile(destPath, asm);
    }

首先,调用parseFile函数对代码进行解析,得到的返回值为AST对象(抽象语法树)。
再调用semanticAnalyze函数对AST对象进行语义分析,完成抽象语法树的生成。接着,调用IRGenerator类的generate函数生成IR对象(中间代码)。至此就是编译器前端处理的代码。
之后,调用generateAssembly函数生成汇编语言的代码,并通过writteFile函数写入文件。这样汇编代码的文件就生成了。

总结

从下一章开始,我们将进入语法分析的环节。


网站公告

今日签到

点亮在社区的每一天
去签到