C语言编译器Parser和CodeGen的过程(下)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: C语言编译器Parser和CodeGen的过程(下)

递归调用parse_expr

传入一个优先级即目前操作符栈栈顶的优先级是个数字

先判断是否为一个数据 比如num或字符串

sizeof也是一个数据 返回整数值

或者是identitify 比如变量

或者是函数调用 function call

无论是什么 它返回的是一个数据

然后把数据存在ax寄存器中

如果数据压栈的话 就会压在真正的内存stack空间里面

这样就实现了数据栈 data stack

操作符压栈是通过递归调用的函数栈实现的

处理完数据之后 再处理优先级最高的符号(

然后是*和&和!和~(位的取反)

然后是正号和负号

最后是前置的++和--

以上是一元操作符的处理过程 然后递归的调用该过程

+正号其实什么都不需要做 直接忽略即可

处理完一元操作符就会处理二元操作符即上图while循环

通过优先级爬上的算法来处理

假设遇到的token是比已经在栈上的更大的

新的优先级更大才能进入while循环

否则先把栈里面的操作符先处理掉

image.png

如果新的比操作符栈顶的操作符优先级更高

进入while循环内部 根据二元操作符的顺序来处理具体的内容

假设表达式是a=3+5

一开始是a变量 进入parse_expr

先处理a变量 结果放入ax寄存器

ax是数据栈data statck的栈顶

然后发现是=赋值操作符 入栈

即取a的地址 然后把地址压栈

然后通过调用parse_expr 解析3+5

先处理num 3

ax就变成3了

然后+优先级比赋值符更高 所以进入while循环

while循环里面就会找到+(And)

此时数据栈中是a和3

操作符栈中是=和+

然后是5 入栈 ax即数据栈的栈定为5

此时数据栈中是a和3和5

然后把操作符中的+出栈pop

数据栈中的3和5出栈 调用add命令

然后结果放入ax为8

然后把栈中的地址(比如data区230230)拿出来 把ax中的值存入地址中去


代码生成


image.png

c语言通过各种函数和全局变量组成

通过函数的定义以及函数的调用来理解整个函数代码生成逻辑

main函数涉及到整个程序的入口和出口问题

通过它来理解参数和局部变量是怎么定义和使用的

上面的代码中没有全局变量

定义一个全局变量int global;

在main函数中赋值 global=1;

基于这样的例子来理解代码生成的核心思想:

解析完了源代码

生成了相应的vm指令之后

在正式开始执行代码之前

会得到

1、在code区中有要执行的指令即源代码

image.png


2、还有data区的数据


image.png

除此之外 其他的都处于初始化的状态即还没有数据的状态

比如stack区和register 这些都是在代码执行之后才开始有数据的

image.png

还有一个就是symbol table


image.png

symbol table在代码解析(生成)完成之后就没有任何作用了

它的作用在代码的定义和使用这2部分衔接生成的过程

比如int add() 这行代码是add函数的定义

int ret 这行代码已进入add定义的解析的过程

在解析这行的时候就已经完成了parse到gencode的过程

但是在使用add的时候是在第10行

这个时候关于解析需要的中间数据就存放在symbol table中

当把整个代码全部解析完 sybmot就没有用了

代码生成其实就是生成code区和data区这2个区的数据

对应到x86的汇编asm里面的两块区域

一个text 这里面有很多指令 比如mov add

另一个是就是data 里面有全局变量、字面量的内容

这2块区域就对应着code区和data区

想要把c4改造成像x86一样生成text和data文件 然后在加载文件也是可以的 即虚拟机是一块单独的代码 编译器也是一块单独的代码

add函数的代码

image.png

image.png

如果有全局变量的定义 也不会对生成的代码区的指令有影响的

变量名叫什么是不重要的 只要在使用的时候能找到定义的变量在data区所对应的地址即可

code区生成的东西其实就是函数定义

函数定义内部其实就是各种语句 各种逻辑

对于代码中的字面量以及全局的变量都会定义在data区

对于局部变量的定义完全是没有任何代码对应的

LEA -1 对应到ret = a+b 左半部分

main函数开始调用add函数的指令是:

image.png

从准备参数开始 第一个参数1 第二个参数2

call add函数对应的地址

解析到add的时候会通过符号表找到对应的地址

就可以精确的生成call add地址这样的指令

image.png

然后看add生成的代码 它其实就是保存ret的地址

因为它是局部变量 保存它跟栈相对的位置

比如是全局变量 比如global

这里就不会用lea了

因为lea是基于bp地址

如果是全局变量的话 可以指定load immediatily一个地址

然后push到栈里面 等待后面去存储

把结果存储到这个地址中去

LEA 3是第一个参数1 把它装到栈里面去

LEA 2是第二个参数2

然后把栈里面的1和ax=2相加

把结果存在目前栈保存的地址中去

这个地址就是前面存起来的ret的地址

相当于现在的结果就在ret里面了

return的时候是返回的第一个局部变量即LEA -1

然后把结果存在ax中

return就是把ax数据返回给了上一层

image.png

然后回到这里去清理参数

main也是一样的逻辑

image.png

首先会加载一个string(c语言中是char*,char指针 它的数据其实是一个地址)在data区的地址

然后准备printf的参数

然后准备add的参数

然后调用add

这个就是要生成的代码区的所有数据

然后来详细讲一下

定义部分和使用部分代码解析和生成的细节

image.png

这个符号表中包含了前面代码中所有会遇到的符号

包含if、else、while关键字

类型关键字 int、char

语句关键字return

native call关键字 比如 printf

然后是代码自定义的关键字

main是在预处理关键字的时候就会把它存储进去

因为要提前记住main在symbol table的地址

回头根据这个地址就可以找到main的函数入口

再开始执行最终生成的代码

接着就是在函数定义、全局变量定义写进symbol table的

比如在解析到add指令的时候 就会把add写入symbol table

如果发现它是一个function 会把目前的地址写入

这个地址其实就是在生成的过程中 code区

所处的指针位置是什么 那么函数地址就是什么

比如code区是空的(0~max) 在解析第一个函数的时候

生成的第一个指令就是要为局部变量生成栈空间

比如有一个局部变量就会生成NVAR 1指令

后续就是这个函数的执行逻辑 一直到ret

这个函数里生成的第一条指令就是就是函数在symbol table中value的值

如果要call这个函数的时候 就是把pc call到这个地址执行NVAR 1指令即可

在函数定义的时候 关键要存储这个函数的初始地址

在使用(call)的时候 只需要找到add这个identity对应的token value地址即可

全局变量在定义的时候留一个地址

那这个地址就是data区为它分配的一个地址

比如解析到int global变量的定义


image.png

0到pc指针之间就是为global预留的空间

为了能在使用的时候精确找到这个地址 就把它存在symbol table中

在使用的时候 只需要把地址加载到ax里面即可(IMM 430128744)

ax就指向了这个地址空间

如果给这个地址空间存储东西 或者 想调用它的值

都可以通过SI\LI(int)或SC\LC(char)等这些基础的指令根据这个地址对global的变量做一些操作

对局部变量解析的时候 比如add中的ret 也是需要知道它的地址就可以了

地址是相对于它在栈基的位置

使用的时候是以bp为分割点的

bp之前有函数的返回地址和参数定义

bp下面是局部变量的地址 比如ret

image.png

就可以知道每个局部变量在栈中和bp的相对位置

在symbol table中存的就是这样的原生序号

比如参数a存的就是0

参数b存储的是1

使用的是LEA 基于bp位置 ,用bp的位置减去序号

比如第一个参数a 3-0=3

LEA 3就可以调到第一个参数

对于第一个局部变量ret ,3-4=-1 即LEA -1

具体看下如何用代码实现上述定义和使用的过程

image.png

当token为{的时候 那么是函数调用

先处理参数 ,参数也是一个表达式

add(1,2)这种情况参数是字面量

add(add(1,2),2) 这种情况参数又是一个表达式

所以递归调用parse_expr 求表达式值

这个值在ax寄存器中

然后把值push到栈中

准备一个参数列表 有多少个值 就push多少个

while循环之后 就把值push到栈中了

接下来是两种情况

一个是native call 那么就用对应的指令 直接调用

对应的指令就是symbol table中的value

那么就直接生成代码对应的指令就好了

如果是定义的函数function

那么代码区就会call identitfiy对应的token value即function的地址

call里面就会存返回值即call完了之后的地方

然后又会处理bp

处理完之后 pc指针又会回到被调用的地方

调用完之后 就会执行清理的代码

把栈中的参数清理掉即栈里面的1、2就会被清理掉

DARG i ,i是有多少个参数i就是几

如果是num 就会当作一个字面量来处理

字面量生成代码直接是LOAD IMM

就直接加载到ax中去

然后就是处理非枚举变量(局部变量和全局变量)

如果是局部变量就是LEA bp的序号-局部变量的序号

如果是全局变量 就是Load IMM 全局变量的地址

在使用的时候就需要根据类型

如果类型是char 就load char 把char加载进来

如果是整数 就load int

如果是指针 就什么都不用load ,ax里面就应该是它的地址

Load IMM这个地址之后 这个指针里面拿到的就是地址

parse stmt怎么做代码生成的

image.png

前面说了如何用指令来表示if while逻辑的过程

但是在代码解析的过程中 如何生成对应的指令呢

如果解析的是if语句 if(a<0){}else{}

parer_expr 解析if后面表达式的值 存放在ax中

根据ax中的值 来决定跳转的位置

如果ax是0 则这个表达式是false

如果是false的话 则跳转到else的位置来执行

首先在代码区生成的指令是JZ

第二个就是JZ到false这个点位

但目前还不知道false点位

因为还没有解析if{}中的逻辑

不过没有关系 先用变量b把这个点位存储起来

等后面再确定这个点位 再回写到这个地方先空着

parse_stmt去解析if{}语句快

然后把true相关的指令就全部都执行完了

如果a是大于0的 if(a>0){}else{}

JZ不会跳转 直接执行true的指令

如果true指令执行完了之后 不能接着执行 因为下面就是flase的指令了

需要直接跳转到if结束的地方即JUMP一个位置

这个位置是哪个 还不知道 因为false还没有解析

还是要把要JUMP的地方先空出来 存一个变量c

空出来之后 就进入了false的解析地址了

就知道了fasle的点位在哪里 就是JUMP之后的地方

这个就是false语句开始的地方

所以这是时候把code的位置存回变量b

假设这个位置是430430 那么JUMP b就是JUMP 430430

然后开始解析false的statement

false解析完之后 就是代码退出的地方

退出的地方假设是430450

那么这个位置就是true statement直接JUMP的位置

这个时候就会这个end if的点位存回变量c

以上就是if的parse和codegen的过程

if之后再说下while

while(a>0){}

在判断了while token之后

先把这个时候 code区的点位存储下来 存到变量a中

为什么需要存储这个 因为循环没有结束的话 需要跳转回来

所以code区的最后一个指令肯定是一个JUMP a的位置

这样才能有一个循环

然后开始解析while里面表达式的值

每次都要parse 因为a每次都在不断的变化 否则就while死循环了

解析表达式的值存ax

如果这个时候ax的值为0了

while就结束了 需要跳转出while循环

即JZ end while 那个点位 但目前还不知道

先存在变量b中

while里面的语句块也是通过同样的方式 parse_stmt解析

最后JUMP a的位置

假设是430430

JUMP完之后code的位置假设是430450

然后把430450回写到变量b

以上就是关键语句的解析

最后是表达式求值的代码生成过程

image.png

首先看下减法 a-b的形式

因为已经解析到-减法这个符号了

-前面的已经解析好了并且在ax里面了

在解析的过程中是这么个顺序

然后把减号忽略掉

然后把ax中的值push到栈里面

假设a的值是5 把5push到栈中

然后去解析第二个参数

第二个参数也是一个表达式 不管是单独的变量还是字面量或者是函数调用返回的结果

解析这个表达式

如果解析的表达式它的优先级大于或等于乘号

假设是a-b*5

先把b*5解析完了 然后再做减法

根据优先级爬山算法

首位的符号是乘号

b的值在ax中 假设是3

那么在生成的代码中除了刚刚的那个push之外

然后就是解析-减号后面的代码

解析完了 然后相减

这里有一些跟类型系统相关的一些逻辑

包括后面的++和{}都有这样的问题

如果说是两个指针相减 point-point

相当于求2个指针地址的相对位置

由于地址是8个byte 64位的

求出相减之后的结果需要除以8

假设地址相差2位 相减结果就相差16个byte

把ax=16 push到栈中

这个时候把栈中的16除以8

ax=2

2就是a-b的结果


image.png

如果指针减一个正常的整数呢

即需要指针往后挪几位

挪的这个位数最终在地址值中相应的乘以8

比如指针的值是430430

加1位 下一位的地址是430438

真正加之前需要把加的值乘以8

这个就是指针加减比如字面量的过程

两个正常的整数相减 直接sub一下就完了

把第一个参数push到栈中

第二个参数b在ax中了

然后直接sub

就可以得到ax结果了

后置的加加相当于目前ax里面的值加1

加完1之后再存回ax中去

中括号[]左边的值一定是一个地址

比如a[3]相当于这个地址加3之后

再给它求值 *(a+3)

对指针做一个相应的加

加完之后再把值load进来

最后看下cpc源码的main函数

image.png

看下编译器主干逻辑

首先需要加载所需要编译的源码

比如cpc hello.c

然后把虚拟机做初始化 主要是内存和寄存器

该申请内存的申请内存

该设置为0的设置为0

然后准备cpu这个语言所需要的所有的关键字

这里面包含了main

然后就是parse方法

这里面会处理全局定义

包括变量和函数

变量就直接处理了

函数会调用两个子函数 一个是parse_params 参数列表

另一个是parse_function函数体

parse_function会调用一个很关键的parse_statement

处理各种功能语句

语句里面调用parse_expr做表达式求值

这个就是整个parse和codegen的过程

在这样的一个调用链中完成的

调用parse函数完成之后 就生成了code区所有指令

以及data区的所有的全局变量也好各种字面量也好

write_as把code区的所有指令输出

最后执行code区的所有指令

然后看下怎么找到main函数的入口

因为main入口并不是在code区0的位置

image.png

在做keyword解析的时候会拿到main在symbol table的指针

它的value就是main在code区的地址

pc先指向这个位置 后续执行的时候就是从main的入口位置开始执行的

main的参数怎么传进来的呢

cpc hello.c xx xx xx

cpc编译器源码第一个参数之后的位置就是所编译代码的main参数


image.png

返回值是stack区max-2的位置

return了之后 pc就会修改成这个地址

这个地址按理说是一个地址 但它实际上是一个push指令

那么就会执行push即把ax push到stack中

最终调用exit命令

exit命令就是把栈里面的值当作exit的参数退出

整个程序就是这么退出的

所以在栈的最开始初始的时候

存入exit、push、main参数、main的返回地址

再接着就是main的函数栈

当main的函数栈结束之后就可以退出代码了

现在就知道了整个函数的入口和出口

整个过程就是把pc指令拿出来

然后跟着指令是什么做对应的操作

pc是不断的++的

如果有JMP pc也会有一些跳转

这个就是代码生成之后运行的主干逻辑


结尾


最近三篇文章介绍了:

C语言编译器概要设计思路一

虚拟机指令集&栈与函数调用

从最开始的虚拟机指令集以及函数调用的实现逻辑

到parse逻辑(词法分析、语法分析、表达式求值)

最后是代码生成核心的思想和逻辑


相关文章
|
7月前
|
自然语言处理 中间件 编译器
C语言的编译器和中间件开发
C语言的编译器和中间件开发
|
7月前
|
设计模式 中间件 编译器
C语言编译器
C语言编译器
|
2月前
|
编译器 C语言
C语言编译器为什么能够用C语言编写?
C语言编译器为什么能够用C语言编写?
47 9
|
编译器 Linux C语言
c语言的编译器vs2019的安装及简单实用
c语言的编译器vs2019的安装及简单实用
190 0
|
7月前
|
存储 编译器 程序员
C语言调试大作战:与VS编译器共舞,上演一场“捉虫记”的艺术与科学
C语言调试大作战:与VS编译器共舞,上演一场“捉虫记”的艺术与科学
|
7月前
|
编译器 C语言 Windows
windows MinGW C语言编译器安装及环境变量配置教程
MinGW被称为Windows版的GCC,安装包下载地址:提示:该安装包下载完之后,相当于安装好了MinGW,之后即可配置环境变量!所以,可以先新建好一个专门用来存放MinGW安装包的文件夹。
290 2
|
7月前
|
存储 编译器 C语言
【C语言必知必会 | 第二篇】编译器的安装与使用
【C语言必知必会 | 第二篇】编译器的安装与使用
85 0
|
7月前
|
自然语言处理 Java 编译器
C语言是什么 C语言历史 编译器怎么运行C语言的代码 怎么学好C语言
C语言是什么 C语言历史 编译器怎么运行C语言的代码 怎么学好C语言
|
监控 编译器 C语言
C语言关于解决vs编译器scanf等函数输入不安全
在VS编译器中,scanf等函数并不会对你输入值进行长度监控,因此在某些层面上就很容易造成内存的溢出。
205 0
|
自然语言处理 IDE 编译器
【C语言】--编译及编译器
【C语言】--编译及编译器
136 0