手把手教你做一个 C 语言编译器设计

“手把手教你构建 C 语言编译器” 这一系列教程将带你从头编写一个 C 语言的编译器。希望通过这个系列,我们能对编译器的构建有一定的了解,同时,我们也将构建出一个能用的 C 语言编译器,尽管有许多语法并不支持。

在开始进入正题之前,本篇是一些闲聊,谈谈这个系列的初衷。如果你急切地想进入正篇,请跳过本章。

前言

为什么要学编译原理

如果要我说计算机专业最重要的三门课,我会说是《数据结构》、《算法》和《编译原理》。在我看来,能不能理解“递归”像是程序员的第一道门槛,而会不会写编译器则是第二道。

(当然,并不是说是没写过编译器就不是好程序员,只能说它是一个相当大的挑战吧)

以前人们会说,学习了编译原理,你就能写出更加高效的代码,但随着计算机性能的提升,代码是否高效显得就不那么重要了。那么为什么要学习编译原理呢?

原因只有一个:装B。

好吧,也许现在还想学习编译原理的人只可能是因为兴趣了。一方面想了解它的工作原理;另一方面希望挑战一下自己,看看自己能走多远。

理论很复杂,实现也很复杂?

我对编译器一直心存敬佩。所以当学校开《编译原理》的课程后,我是抱着满腔热情去上课的,但是两节课后我就放弃了。原因是太复杂了,听不懂。

一般编译原理的课程会说一些:

1、如何表示语法(BNF什么的)

2、词法分析,用什么有穷自动机和无穷自动机

3、语法分析,递归下降法,什么 LL(k),LALR 分析。

4、中间代码的表示

5、代码的生成

6、代码优化

我相信绝大多数(98%)的学生顶多学到语法分析就结束了。并且最重要的是,学了这么多也没用!依旧帮助不了我们学习编译器!这其中最主要的原因是《编译原理》试图教会我们的是如何构造“编译器生成器”,即构造一个工具,根据文法来生成编译器(如 lex/yacc)等等。

这些理论试图教会我们如何用通用的方法来自动解决问题,它们有很强的实际意义,只是对于一般的学生或程序员来说,它们过于强大,内容过于复杂。如果你尝试阅读 lex/yacc (或 flex/bison)的代码,就会发现太可怕了。

然而如果你能跟我一样,真正来实现一个简单的编译器,那么你会发现,比起可怕的《编译原理》,这点复杂度还是不算什么的(因为好多理论根本用不上)。

项目的初衷

有一次在 Github 上看到了一个项目(当时很火的),名叫 c4,号称用 4 个函数来实现了一个小的 C 语言编译器。它最让我震惊的是能够自举,即能自己编译自己。并且它用很少的代码就完成了一个功能相当完善的 C 语言编译器。

一般的编译器相关的教程要么就十分简单(如实现四则运算),要么就是借助了自动生成的工具(如 flex/bison)。而 c4 的代码完全是手工实现的,不用外部工具。可惜的是它的代码初衷是代码最小化,所以写得很乱,很难懂。所以本项目的主要目的:

1、实现一个功能完善的 C 语言编译器

2、通过教程来说明这个过程。

c4 大致500+行。重写的代码历时一周,总共代码加注释1400行。项目地址: Write a C Interpreter。

声明:本项目中的代码逻辑绝大多数取自 c4 ,但确为自己重写。

预警

在写编译器的时候会遇到两个主要问题:

1、麻烦,会有许多类似的代码,写起来很无聊。

2、难以调试,一方面没有很好的测试用例,另一方面需要对照生成的代码来调试(遇到的时候就知道了)。

所以我希望你有足够的耐心和时间来学习,相信当你真正完成的时候会像我一样,十分有成就感。

虽然标题是编译器,但实际上我们构建的是 C 语言的解释器,这意味着我们可以像运行脚本一样去运行 C 语言的源代码文件。这么做的理由有两点:

1、解释器与编译器仅在代码生成阶段有区别,而其它方面如词法分析、语法分析是一样的。

2、解释器需要我们实现自己的虚拟机与指令集,而这部分能帮助我们了解计算机的工作原理。

编译器的构建流程

一般而言,编译器的编写分为 3 个步骤:

1、词法分析器,用于将字符串转化成内部的表示结构。

2、语法分析器,将词法分析得到的标记流(token)生成一棵语法树。

3、目标代码的生成,将语法树转化成目标代码。

已经有许多工具能帮助我们处理阶段1和2,如 flex 用于词法分析,bison 用于语法分析。只是它们的功能都过于强大,屏蔽了许多实现上的细节,对于学习构建编译器帮助不大。所以我们要完全手写这些功能。

所以我们会根据下面的流程:

1、构建我们自己的虚拟机以及指令集。这后生成的目标代码便是我们的指令集。

2、构建我们的词法分析器

3、构建语法分析器

编译器的框架

我们的编译器主要包括 4 个函数:

1、next() 用于词法分析,获取下一个标记,它将自动忽略空白字符。

2、program() 语法分析的入口,分析整个 C 语言程序。

3、expression(level) 用于解析一个表达式。

4、eval() 虚拟机的入口,用于解释目标代码。

这里有一个单独用于解析“表达式”的函数 expression 是因为表达式在语法分析中相对独立并且比较复杂,所以我们将它单独作为一个模块(函数)。

因为我们的源代码看起来就像是:

#include

#include

#include

#include

int token;            // current token

char *src, *old_src;  // pointer to source code string;

int poolsize;         // default size of text/data/stack

int line;             // line number

void next() {

    token = *src++;

    return;

}

void expression(int level) {

    // do nothing

}

void program() {

    next();                  // get next token

    while (token > 0) {

        printf("token is: %c\n", token);

        next();

    }

}

int eval() { // do nothing yet

    return 0;

}

int main(int argc, char **argv)

{

    int i, fd;

    argc--;

    argv++;

    poolsize = 256 * 1024; // arbitrary size

    line = 1;

    if ((fd = open(*argv, 0)) < 0) {

        printf("could not open(%s)\n", *argv);

        return -1;

    }

    if (!(src = old_src = malloc(poolsize))) {

        printf("could not malloc(%d) for source area\n", poolsize);

        return -1;

    }

    // read the source file

    if ((i = read(fd, src, poolsize-1)) <= 0) {

        printf("read() returned %d\n", i);

        return -1;

    }

    src[i] = 0; // add EOF character

    close(fd);

    program();

    return eval();

}

上面的代码看上去挺复杂,但其实内容不多,就是读取一个源代码文件,逐个读取每个字符,并输出每个字符。这里重要的是注意每个函数的作用,后面的文章中,我们将逐个填充每个函数的功能,最终构建起我们的编译器。

本节的代码可以在 Github 上下载,也可以直接 clone

git clone -b step-0 https://github.com/lotabout/write-a-C-interpreter

这样我们就有了一个最简单的编译器:什么都不干的编译器,下一章中,我们将实现其中的eval函数,即我们自己的虚拟机。

参考资料

最后想介绍几个资料:

1、Let’s Build a Compiler 很好的初学者教程,英文的。

2、Lemon Parser Generator,一个语法分析器生成器,对照《编译原理》观看效果更佳。

祝你学得愉快。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容