跳转至

总体设计

目的

从头编写一个 C 语言的编译器能对编译器的构建有一定的了解,同时,也将构建出一个能用的 C 语言编译器(尽管有许多语法并不支持)

实际上构建的是 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 是因为表达式在语法分析中相对独立并且比较复杂,所以将它单独作为一个模块(函数)

下面是相应的源代码,它的流程为:读取一个文件(内容为 C 语言代码),逐个读取文件中的字符,并输出

#include <memory.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int token;           // current token
char *src, *old_src; // pointer to source code string;
int pool_size;       // 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++;

  pool_size = 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(pool_size))) {
    printf("could not malloc(%d) for source area\n", pool_size);
    return -1;
  }

  // read the source file
  if ((i = read(fd, src, pool_size - 1)) <= 0) {
    printf("read() returned %d\n", i);
    return -1;
  }
  src[i] = 0; // add EOF character
  close(fd);

  program();
  return eval();
}