编译器
1、编译器定义
将高级别语言翻译成更底层的机器可执行的语言
2、工业级编译器的编译过程
编译过程分前端和后端两个阶段
2-1
前端
前端即parser:将源代码翻译成中间代码,以便给后端程序进一步处理
parser过程分两个步骤
- 词法分析即tokenize
词法分析的目标是把人类语言简单处理一下告诉计算机这些词都是什么含义
比如把int单词识别出来告诉计算机是整型;add识别出来告诉计算机是一个函数
- 语法分析
通过词法分析计算机已经知道每个词是什么意思 通过语法分析可以将词语组成一个完整的句子
语法分析的目标就是按照语法(蕴含着逻辑和运算)生成中间代码,这个中间代码就是AST(抽象语法树)
用逆波兰式来表示:
+(+(*32)4)6
有些语言比如lisp这是函数式语言,本身的语言的原始状态就非常类似于逆波兰式,这种语言会很轻易的得到抽象语法树
高级语言比如java、c想要得到最终语法树想要经历上述2个过程
2-2
后端
后端是编译最核心的关节
分为2个组件
- 可选的optimizer优化器(编译器最难最核心的组件)
输入中间代码经历一系列的优化过程又得到了同一种的中间代码
它做了一些优化工作比如剔除不影响结果的没用的代码(死代码剔除)、转换一些逻辑(函数内联)、做一些替换(常量替换)得到一个更利于执行的性能更高的代码
- Code Generator即CodeGen代码生成器
生成最终目标平台(目标机器)的可执行代码的生成器
将中间代码翻译成目标代码
比如翻译成x86平台的汇编:movq、push、add
java虚拟记jvm:load、store、add
llvm(LR)语言给各个平台 比如x86平台、amd平台、arm架构等都实现了上述两个过程
想要做一个可用的编译器的话 最简单的方式就是写个parser把目标的语言翻译成llvm(LR) 选定想要的平台生成最终的机器代码
这个语言不仅用在编译器领域,也用在机器学习、数据库优化等领域
自定义C语言编译器设计思路
自定义c语言编译器的设计思路
- 前后端合一,没有中间优化过程
- 目标代码基于自定义的VM即不是x86也不是jvm
目标是为了简单,这个自定义的虚拟机麻雀虽小但五脏俱全
- 编译过程是one pass的过程即读到源码,一行一行的去编译和分析
当把所有代码全部pass完了之后,目标代码就已经生成好了,就不需要再读源码了
源码只读一遍就可以完成parse和codeGen的过程得到想要的自定义虚拟机的机器码
但是有一些c语言的特性是不支持的 但也无伤大雅
比如说函数的局部变量必须得一定在函数的最开头的位置
int add(int a,int b){ int ret; int tmp; tmp=a+b; ret=tmp; return ret; } ret和tmp这两个局部变量 其实也可以定义在a+b后面 但因为是one-pass过程 所以必须定义在add函数最开始的位置
自制VM设计概要设计
1
计算是基于集群器Register和stack两个组件来做的
这样做的目的是为了简化
比如有个两元计算
加法操作:两个数相加a+b
如果用传统的x86的物理机的架构
它的运算单元在cpu内部
能够处理的数据在cpu内部的寄存器 比如ax、bx
这些寄存器能够做运算把结果输出到寄存器
不能基于栈做运算
因为栈在内存里
内存的读取速度跟计算cpu的速度相差100倍以上
所以在实际的物理机中要计算a+b需要把数据从内存加载到寄存器
运算完之后再写回寄存器
如果要存储这个资源 还需要再写回内存
这里为了简化设计 只有一个通用的寄存器
一元计算直接基于这个寄存器
二元计算基于栈顶(stack peek)去跟通用寄存器一起去计算
所以它的ALU(算数逻辑单元)是基于stack和ax这两个位置运行的
2
pc寄存器 program counter
指定目前运行到哪条指令了
下条指令就是pc+1
保持代码按既有的顺序去执行
有2个指针寄存器
一个是stack pointer(sp 维护栈顶)一个是base pointer(bp 维护上一个栈的栈顶:如果这个栈要回去的时候能找到上一个栈在什么地方 一个函数调用起一个新栈 执行完函数要回到函数之前的位置 代码是可以回去了 栈的内容也得回到原来的位置)
3
内存空间
- vm指令编译好之后 得有一个内存空间来存放
- 存放栈的空间
- 还有静态数据、字面量在编译的过程中存在data的内存空间里
比如32位地址空间的程序有4G的虚拟内存会分为这几块
比如 4+3*2+6 做成的AST:
最顶上是内核代码 做系统调用的话 会读到内核的代码
在这里设计VM的时候没有用到堆
因为并不想主动去实现动态内存
动态内存可以通过一种作弊的方式去实现即Native-Call
4
指令集
包括3块
4-1
一个是save and load这一类的指令
内存到寄存器的操作叫做load
寄存器写回内存叫save
这一类的内存和寄存器的指令都叫save and load
4-2
第二个就是运算类的指令
- 算术运算:四则运算
- 位运算
- 逻辑运算:与 或 非
4-3
第三个就是分支跳转类指令
语言实现判断、循环、函数跳转
4-4
为了简化实现 做了一些作弊类的指令Native-Call
它主要处理IO(print、open、write、read文件)和动态内存(malloc、free、memset)的操作
正常的平台是不需要Native-Call的 通过前3类指令可以实现
但比较复杂 这里为了简化实现 做了作弊类的指令
大致了解VM的运行原理
有一段代码 helloworld
编译完之后 代码区存放指令
data存放字面量、helloworld的ascii码
栈是从大到小的 栈刚编译完的时候 sp、bp都指向初始位置max位置
data和code是从小到大的 初始位置是处于0的位置
ax寄存器是一个空的状态
pc寄存器开始在0的位置 执行第一行代码pc+1
把指令读出来
看看是什么指令 做相应的操作
看是去data区取数据呢
还是栈的指针向下减呢
还是把栈里的数据加载到寄存器呢
还是对寄存器和栈里的数据做一个计算呢
根据指令的不同就会有不同的行为结果
当把所有的指令执行完 退出
当然也会有跳转 pc可能跳转到其他位置了
最终会把所有的指令(代码)执行完
栈指针也会通过函数的不断调用加加减减
最终回到开始位置
data里面的数据也都使用好了
最终的结果保存在ax寄存器中了
代码就结束了