前言
本文分析代码来自 github:https://github.com/lotabout/write-a-C-interpreter/blob/master/xc.c。并非亲自实现。这个一个小型的C语言编译器,或者称为解释器,是 c 语言的极度简化版。不过包含了词法分析、语法分析以及代码生成等功能。该代码在进行语法分析的同时进行词法分析。本文大部分参照原作者内容,不过增加了自己的解释。
正文
先总体介绍改代码中包含的函数
next(): 词法分析的入口
program():语法分析的入口
global_declaration: 全局声明,包括 全局变量、函数以及 enum 变量的声明
function_declaration:函数解析
function_parameter:函数参数的解析
function_body:函数体的解析
enum_declaration:enum 变量的解析
expression(level):函数体内表达式的分析,包含对变量、函数、枚举类型的分析以及对操作符的分析
eval():虚拟机的入口,对相关代码做解释
该编译器一共支持3种类型:enum { CHAR, INT, PTR };
不支持 struct;
变量必须先声明再使用;
没有 do-while、for、switch,只有 if 和 while;
有 text、stack、data 三个段;
包含 PC、SP、BP、AX 四个有关寄存器。
有如下指令集:
enum { LEA ,IMM ,JMP ,CALL,JZ ,JNZ ,ENT ,ADJ ,LEV ,LI ,LC ,SI ,SC ,PUSH,
OR ,XOR ,AND ,EQ ,NE ,LT ,GT ,LE ,GE ,SHL ,SHR ,ADD ,SUB ,MUL ,DIV ,MOD ,
OPEN,READ,CLOS,PRTF,MALC,MSET,MCMP,EXIT };
相关指令含义如下:
LEA 2 :将栈往上的2个位置变量加载到寄存器 加载本地变量
IMM :将代码区位置指定的代码加载到寄存器 加载到全局变量
JMP 2 :将代码区跳转到2这个位置 跳转
JSR 2 :将下一个代码区位置保存到栈顶,并将代码区跳转到2这个位置 跳转到子程序
BZ 2 :如果全局变量为a,则跳转到下一个代码区域,否则跳转到2这个位置 零跳转
BNZ 2 :如果全局变量不为a,则跳转到下一个代码区域,否则跳转到2这个位置 非零跳转
ENT 2 :保存前一个程序的栈顶到当前栈上,并设置新栈顶,和局部变量区 开辟一个新的子程序
ADJ 2 :栈调整,往前2个位置 栈跳转
LEV :栈回滚到当前程序的栈顶,然后恢复上一个程序的栈顶,再恢复上一个程序的代码区,最后恢复上一个程序的栈 子程序退出
LI :将寄存器的整数变量加载出来 加载整数
LC :将寄存器的字符变量加载出来 加载字符
SI :将栈顶指定位置的地址保存整数 保存整数
SC :将栈顶指定位置的地址保存字符 保存字符
PSH :将寄存器变量保存到栈上 保存栈 EXIT 将栈顶变量打印出来,作为返回值,并退出来 退出整个程序 JZ/JNZ: 零跳转/非零跳转
CALL:调用子程序
CALL:调用子程序
他们的含义都在 eval() 函数中进行了解释:
if (op == IMM) {ax = *pc++;} // load immediate value to ax
else if (op == LC) {ax = *(char *)ax;} // load character to ax, address in ax
else if (op == LI) {ax = *(int *)ax;} // load integer to ax, address in ax
else if (op == SC) {ax = *(char *)*sp++ = ax;} // save character to address, value in ax, address on stack
else if (op == SI) {*(int *)*sp++ = ax;} // save integer to address, value in ax, address on stack
else if (op == PUSH) {*--sp = ax;} // push the value of ax onto the stack
else if (op == JMP) {pc = (int *)*pc;} // jump to the address
else if (op == JZ) {pc = ax ? pc + 1 : (int *)*pc;} // jump if ax is zero
else if (op == JNZ) {pc = ax ? (int *)*pc : pc + 1;} // jump if ax is not zero
else if (op == CALL) {*--sp = (int)(pc+1); pc = (int *)*pc;} // call subroutine
//else if (op == RET) {pc = (int *)*sp++;} // return from subroutine;
else if (op == ENT) {*--sp = (int)bp; bp = sp; sp = sp - *pc++;} // make new stack frame
else if (op == ADJ) {sp = sp + *pc++;} // add esp, <size>
else if (op == LEV) {sp = bp; bp = (int *)*sp++; pc = (int *)*sp++;} // restore call frame and PC
else if (op == LEA) {ax = (int)(bp + *pc++);} // load address for arguments.
else if (op == OR) ax = *sp++ | ax;
else if (op == XOR) ax = *sp++ ^ ax;
else if (op == AND) ax = *sp++ & ax;
else if (op == EQ) ax = *sp++ == ax;
else if (op == NE) ax = *sp++ != ax;
else if (op == LT) ax = *sp++ < ax;
else if (op == LE) ax = *sp++ <= ax;
else if (op == GT) ax = *sp++ > ax;
else if (op == GE) ax = *sp++ >= ax;
else if (op == SHL) ax = *sp++ << ax;
else if (op == SHR) ax = *sp++ >> ax;
else if (op == ADD) ax = *sp++ + ax;
else if (op == SUB) ax = *sp++ - ax;
else if (op == MUL) ax = *sp++ * ax;
else if (op == DIV) ax = *sp++ / ax;
else if (op == MOD) ax = *sp++ % ax;
else if (op == EXIT) { printf("exit(%d)", *sp); return *sp;}
else if (op == OPEN) { ax = open((char *)sp[1], sp[0]); }
else if (op == CLOS) { ax = close(*sp);}
else if (op == READ) { ax = read(sp[2], (char *)sp[1], *sp); }
else if (op == PRTF) { tmp = sp + pc[1]; ax = printf((char *)tmp[-1], tmp[-2], tmp[-3], tmp[-4], tmp[-5], tmp[-6]); }
else if (op == MALC) { ax = (int)malloc(*sp);}
else if (op == MSET) { ax = (int)memset((char *)sp[2], sp[1], *sp);}
else if (op == MCMP) { ax = memcmp((char *)sp[2], (char *)sp[1], *sp);}
词法分析
因为该语法不支持 struct,所以用一个整型数组来保存相关的标识符(id)信息。每个标识符由 9 个部分组成,分别是:Token(标识符对应的标记), Hash(标识符的hash值), Name(标识符的名称), Type(标识符的类型,如int, char), CLass(标识符的类别,如数字,全局变量), Value(标识符存放的变量值,若标识符是函数,则存放函数地址), Bclass, BValue, IdSize(BXXXX,当全局标识符和局部标识符同时存在时,用作存放局部标识符)。
这里的词法分析并没有一次吧所有符号解析出来,而是每次只解析一个符号。
用于词法分析的程序是 next().
对标识符的处理
整体思路是:每次向前读取一个字符,直到读取一个完整的标识符为止。然后判断该标识符是否在符号表中,若在,结束;若不在,加入符号表,结束。其中 last_pos是该标识符起点处的上一个位置。
最外层的while循环用于遍历字符串,遇到换行,行数+1,遇到‘#’,跳过,因为这个语法不支持宏定义,如遇到的第一个字符满足a-z或A-Z或_,则说明有可能是标识符,然后while遍历得到该标识符。symbols 是符号表的地