首页
社区
课程
招聘
[原创] LL(1) 语法分析器解析算数表达式生成AST,和我学习编译的原因
发表于: 2018-12-24 19:03 14198

[原创] LL(1) 语法分析器解析算数表达式生成AST,和我学习编译的原因

2018-12-24 19:03
14198

我的想法是:
首先VMP是什么?
VMP在加壳虚拟化代码的时候显示的为什么是程序编译中?
仔细想想编译器是什么?
C语言编译器 就是把程序员能看懂的C语言代码翻译成机器能看懂的汇编代码。
而VMP编译器就是把机器能看懂的汇编代码翻译成VMP虚拟机能看懂的代码。

试想是否可以写一个反VMP编译器。就是把VMP代码再次编译成机器能看懂的汇编代码? 可行性我现在不能100%的确定,但是理论上是可以实现的。只不过是时间长短的问题。 比如说VMP的虚拟栈分析我的一些看法是:

比如一段VMP代码:
vPushImm4 100
vPushReg r1
vPopReg r2
vPushImm 200
vPopReg r3

默认有一个栈偏移
SP = 0

当vPushImm 100 执行的时候这个时候提取出 两个词法单元

(IMM, 100) // IMM 是立即数, 4是词法单元的属性
当我们看到vPushImm4的时候可以当做生成了一个临时变量。
首先把 SP + 4。

SP = SP + 4
然后生成一个中间变量
vLocal4 = 100 // 这个vLocal4 中的4就是SP的偏移

执行 vPopReg r2 的时候获取到两个词法单元

r2

此时把当前SP的值给r2,然后SP = SP - 4
生成类似的代码
r2 = vLocal8
SP = SP - 4 (此时SP = 4)

执行 vPushImm4 200的时候
SP = SP + 4 (此时SP = 8)
生成代码
vLocal8 = 200

上述的VMP语句综合起来就是
vLocal4 = 100
vLocal8 = r1
r2 = vLocal8
vLocal8 = 200

如果在应用 机器无关优化的话:
vLocal4 = 100
r2 = r1
vLocal8 = 200

当然更加具体的,我学习的还没有那么深入。还没学到中间代码生成,我是先有这个想法再去学编译原理的。现在看到语法翻译的部分。我会的东西都会发帖子出来的。有一起学的朋友可以留言一起。

图片描述

在说LL(1)算法之前 我们必须要了解几个东西:

消除左递归,和提取左公因子。其实相对来说没有那么重要,这个部分可以手动转换。但是算法也在我之前的帖子中给出了。

如果说一点编译原理没有看过的话,那么这篇文章你看起来可能云里雾里的。

文法 是必须要所了解的点。
FIRST集FOLLOW集 是所有语法分析器都要用到的算法,非常非常重要。 如果说 FIRST集FOLLOW集 没有掌握好的话,那么所有的LL文法LR文法都是掌握不了的,更别说写出对应的算法了。

和大家简单介绍一下文法:
文法的全称是 上下文无关法, 他是一种规则用来约定一个语言的语法规则。其实正则表达式也是一种上下文无关法,但是不代表文法就是正则。相对于正则表达式,文法的表达能力更强,描述能力更强。

文法由四个部分组成:

本文不会将所有概念说到,因为太多了。

比如说一个算数表达式文法:

S -> E
E -> E + E
E -> E - E
E -> E / E
E -> E * E
E -> (E), num

在 -> 左部称为产生式头部(左部), 在 -> 右部的称为产生式体(右部)。
所有的产生式头 都是一个非终结符号。
所谓非终结符号就是 抽象的描述了一种规则。
而终结符号就是一个具体的事物。

在这个文法中 {S, E} 这两个是非终结符号, { +, -, *, /, num, (, ) }这些是终结符号。
但是上面的例子中说的文法并不是一个LL(1)文法。因为他是一个左递归文法。
E -> E + E 产生式头部和产生式体的第一个符号是相同的,就会导致LL(1)文法陷入一个死循环。LL(1)文法是一个文法最左推导过程,从左往右的推导。

当我们看到 S -> E的时候 我们会去推导 产生式体E
当我们看到 E -> E + E 的时候我们会去推导产生式 E + E, 但此时产生式体的第一个符号 E 让我们又陷入了要解析产生式E,然后就陷入一个死循环出不去了。(更详细的大家看书中所说的)
所以当我们看到 E -> E + E 的时候就要消除左递归。(可能还有间接左递归, 具体看我另外一篇文章)

在按照算数表达式优先级(括号优先级最大,其次是乘除法, 在是加减法)我们给出的文法是:
S -> E
E -> E + T | E - T | T
T -> T * F | T / F | F
F -> (E) | num

这里的 "|" 是用来分割产生式体的, 这些用 | 分开的产生式他们拥有相同的 左部。
下面的产生式是用我的程序消除左递归后的文法。
S -> E |
E -> TE| T -> FT |
F -> (E) | num |
E-> +TE | -TE| ε | T -> *FT| /FT | ε |

上文的图片中说了, LL(1)文法就是根据向前看1个输入就可以确定应用哪个产生式进行解析。
比如说 现在我们要推导E: 他有3个产生式体: +TE | -TE| ε FIRST(E) = {+, -, ε} 这是他的FIRST集。
当我们输入中的符号是 + 号时, 我们就选用+TE这个产生式来推导, 是 - 号时选用-TE
但是我们发现这个E还可以推导出ε (空串), 也就是说 E是可以为空的。 那我们什么时候应用 E-> ε这个产生式? 此时我们就要查看什么终结符号可以跟在 E的后面也就是 FOLLOW(E) = { ), $ } 这两个符号。也就是说当遇到 ), $ 这两个终结符号时。我们就应用 E -> ε这个产生式。

举个简单的例子:

S -> ABC
A -> a | ε
B -> b
C -> c
这个文法有4个产生式: S 是文法的开始符号。
这个产生式要匹配一个 abc 或者 bc 的字符串, 也就是终结符a是可以不存在的, 因为非终结符A是可以推导出空串的。那我们看看什么时候应用这个产生式。 当非终结符A可以推导出空串的时候 我们就要获取FOLLOW(A)的值,此时可以跟在A后面的只有B,而FIRST(B)的值是b.就是说当A产生式遇到终结符b时那么就应用 A -> ε这个产生式。

图片描述

图片描述
这个伪代码其实很简单,不过我的程序中加上了生成语法树。可能略微复杂。

现在给出完整代码:
https://github.com/hixiaosan/cpp_dragon.git

Parser.h

Parser.cpp

LL1.h

LL1.cpp

main.cpp

当我们输入错误的表达式语法时就会报错:
图片描述

当我们输入正确的语法时就会在程序运行目录生成一张 1.jpeg的图片。
图片描述
图片描述

其实多叉树生成这种图片的算法,大家有兴趣可以看一下。我修修改改花了一整天才实现的。

 
 
 
 
 
 
 

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2019-1-17 13:19 被kanxue编辑 ,原因:
收藏
免费 7
支持
分享
最新回复 (21)
雪    币: 6112
活跃值: (1212)
能力值: (RANK:30 )
在线值:
发帖
回帖
粉丝
2
沙发
2018-12-24 19:06
0
雪    币: 2391
活跃值: (309)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
3
CCkicker 沙发
我感觉我表达能力太弱了...
2018-12-24 19:32
0
雪    币: 1392
活跃值: (5172)
能力值: ( LV13,RANK:240 )
在线值:
发帖
回帖
粉丝
4
菜鸟级X 我感觉我表达能力太弱了...
这是自己动手写编译器SCC的书吧?作者的代码是用的TCC的
2018-12-24 19:49
1
雪    币: 2391
活跃值: (309)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
5
IamHuskar 这是自己动手写编译器SCC的书吧?作者的代码是用的TCC的
这个都是我根据编译原理书上写的代码,tcc是c语言写的好吗
2018-12-24 19:50
0
雪    币: 2046
活跃值: (265)
能力值: ( LV7,RANK:104 )
在线值:
发帖
回帖
粉丝
6
LZ的目的是反向编译虚拟代码?如果直接脱壳子呢?脱壳是治标不治本?
2018-12-24 23:58
0
雪    币: 81
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
就喜欢有专研精神的
2018-12-25 10:15
0
雪    币: 2635
活跃值: (5083)
能力值: ( LV9,RANK:225 )
在线值:
发帖
回帖
粉丝
8
楼主还缺不缺端茶倒水的小弟?
2018-12-25 12:18
0
雪    币: 1392
活跃值: (5172)
能力值: ( LV13,RANK:240 )
在线值:
发帖
回帖
粉丝
9
菜鸟级X 这个都是我根据编译原理书上写的代码,tcc是c语言写的好吗
不是,我是说书有点像以前看过的那本手动写编译器SCC的某些章节。那本书的作者是基于TCC做的。
2018-12-25 15:20
0
雪    币: 2391
活跃值: (309)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
10
IamHuskar 不是,我是说书有点像以前看过的那本手动写编译器SCC的某些章节。那本书的作者是基于TCC做的。
那本书 基本就是把 TCC 代码搬过去了
2018-12-25 15:36
0
雪    币: 2391
活跃值: (309)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
11
三十二变 楼主还缺不缺端茶倒水的小弟?[em_51]
大哥,要不你收我做小弟吧
2018-12-25 15:37
0
雪    币: 249
活跃值: (346)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
理论上肯定没问题,vmp里面数据流和控制流的陷阱处理起来就够搞死几个人了。理想很丰满,现实很骨感,单纯一个反编译器要从理论到实际应用那是得经历多少洗礼,目前仍不能完全应对各种对抗,更何况是对vmp。反编译理论上都差不多,不分语言的,但是真正实用于对抗级别反编译器,估计就ida-xray团队吧,jeb团队,都是反编译器界的代表,好像还有一个是看雪的GDA吧
2018-12-25 21:50
0
雪    币: 2391
活跃值: (309)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
13
hackevil 理论上肯定没问题,vmp里面数据流和控制流的陷阱处理起来就够搞死几个人了。理想很丰满,现实很骨感,单纯一个反编译器要从理论到实际应用那是得经历多少洗礼,目前仍不能完全应对各种对抗,更何况是对vmp。反 ...
总要有人去尝试的...
2018-12-26 08:43
0
雪    币: 2391
活跃值: (309)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
14
hackevil 理论上肯定没问题,vmp里面数据流和控制流的陷阱处理起来就够搞死几个人了。理想很丰满,现实很骨感,单纯一个反编译器要从理论到实际应用那是得经历多少洗礼,目前仍不能完全应对各种对抗,更何况是对vmp。反 ...
而且个人认为 写VMP 反编译器,要比写 TCC语言编译器 还要来的简单的多, 毕竟词法单元和文法都不是一个数量级的。VMP的词法单元和文法结构相对来说简单的多。
2018-12-26 09:31
0
雪    币: 630
活跃值: (95)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
我写过词法分析器,LALR的语法分析器。做研究可以。利用所学的东西做分析其他东西也没问题,如果真要去实现编译器,估计作者不行,不是所智力不行。很多条件制约,而且性能优化要花很长时间。我的LALR语法分析器分析实际的C程序语法并生成AST要近5分钟,而别人的几秒。优化的差距不是一点点。后面的制导翻译,中间代码生成,指令选择,寄存器选择,优化问题,难度不是一点点。如果有单位支撑,如果有经费,可以搞。
当年的豪情都没了,可能我老了。希望后来者有更好的条件,更大的决心。

鼓励一下的是:我现在对源代码、二进制代码、或任一规范语法文件的分析工具编写都比较自信了。
2018-12-27 18:47
0
雪    币: 2391
活跃值: (309)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
16
internetH 我写过词法分析器,LALR的语法分析器。做研究可以。利用所学的东西做分析其他东西也没问题,如果真要去实现编译器,估计作者不行,不是所智力不行。很多条件制约,而且性能优化要花很长时间。我的LALR语法分 ...
老哥 你说的是没问题的, 这件事本身就是一个循序渐进的过程。学习的过程,而且我相信我可以写出来的。VMP编译器相对说C语言编译器, 词法单元少,文法简单,相对来说编译器前端的工作量很少。编译器后端可以自己写 就自己写,如果实在写不出来交给LLVM也是未尝不可的。
2018-12-27 22:51
0
雪    币: 1392
活跃值: (5172)
能力值: ( LV13,RANK:240 )
在线值:
发帖
回帖
粉丝
17
菜鸟级X 老哥 你说的是没问题的, 这件事本身就是一个循序渐进的过程。学习的过程,而且我相信我可以写出来的。VMP编译器相对说C语言编译器, 词法单元少,文法简单,相对来说编译器前端的工作量很少。编译器后端可以 ...
楼主整理一下吧。标题上加  1 2 3 4 更有顺序 
2018-12-28 11:41
0
雪    币: 1392
活跃值: (5172)
能力值: ( LV13,RANK:240 )
在线值:
发帖
回帖
粉丝
18
internetH 我写过词法分析器,LALR的语法分析器。做研究可以。利用所学的东西做分析其他东西也没问题,如果真要去实现编译器,估计作者不行,不是所智力不行。很多条件制约,而且性能优化要花很长时间。我的LALR语法分 ...
确实如此,就跟我当年写防火墙一样,写了一个80%。最后20%不搞了,实在是没有继续坚持的动力。有一种曾经梦想仗剑走天涯,后来作业太多就放弃了的感觉
2018-12-28 11:45
0
雪    币: 103
活跃值: (376)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
19
很六,我要去尝试一下上下文相关算法,实际写点代码出来分析一下
2019-1-7 14:22
0
雪    币: 11716
活跃值: (133)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
#寻宝大战#祝看雪19岁快乐!
2019-1-11 20:21
0
雪    币: 19
活跃值: (394)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
21
对于表达式求值这种,就是类似计算器这种功能,可以转成逆序波兰式,然后使用调度场算法,很容易实现的,思路也很清晰
2019-1-20 10:46
0
雪    币: 219
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
22
TL,DR
用过 ocamlyacc(LALR) 和 camlp4(自顶向下, 流匹配), 也搜过一些比如 bison, menhir(LR1), 但从未见过有 LL1 基于跳转表, 可用的成熟分析器 ,
估计是因为 LL1 要消除左递归必然要增加新的产生式, 这样就导致了产生式相关联的动作(action) 没法处理, 
LR 分析器在分析带有二义性语法时非常棘手,因此无法处理复杂性高的语言 ,
而 camlp4 要自己处理操作符优先级非常麻烦, 因此写出来的语法定义会难以理解, 

2019-1-24 00:20
0
游客
登录 | 注册 方可回帖
返回
//