转自:http://blog.chinaunix.net/uid-26750235-id-3139100.html
(一)简介
抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,比如说,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现。抽象语法树并不依赖于源语言的语法,也就是说语法分析阶段所采用的上下文无文文法,因为在写文法时,经常会对文法进行等价的转换(消除左递归,回溯,二义性等),这样会给文法分析引入一些多余的成分,对后续阶段造成不利影响,甚至会使合个阶段变得混乱。因些,很多编译器经常要独立地构造语法分析树,为前端,后端建立一个清晰的接口。
抽象语法树在很多领域有广泛的应用,比如浏览器,智能编辑器,编译器。
(二)抽象语法树实例
(1)四则运算表达式
表达式: 1+3*(4-1)+2
抽象语法树为:
(2)xml
代码2.1:
- <letter>
- <address>
- <city>ShiChuang</city>
- </address>
- <people>
- <id>12478</id>
- <name>Nosic</name>
- </people>
- </letter>
抽象语法树
(3)程序1
代码2.2
- while b != 0
- {
- if a > b
- a = a-b
- else
- b = b-a
- }
- return a
抽象语法树
(4)程序2
代码2.3
- sum=0
- for i in range(0,100)
- sum=sum+i
- end
抽象语法树
(三)为什么需要抽象语法树
当在源程序语法分析工作时,是在相应程序设计语言的语法规则指导下进行的。语法规则描述了该语言的各种语法成分的组成结构,通常可以用所谓的前后文无关文法或与之等价的Backus-Naur范式(BNF)将一个程序设计语言的语法规则确切的描述出来。前后文无关文法有分为这么几类:LL(1),LR(0),LR(1), LR(k) ,LALR(1)等。每一种文法都有不同的要求,如LL(1)要求文法无二义性和不存在左递归。当把一个文法改为LL(1)文法时,需要引入一些隔外的文法符号与产生式。
例如,四则运算表达式的文法为:
文法1.1
- E->T|EAT
- T->F|TMF
- F->(E)|i
- A->+|-
- M->*|/
改为LL(1)后为:
文法1.2
- E->TE'
- E'->ATE'|e_symbol
- T->FT'
- T'->MFT'|e_symbol
- F->(E)|i
- A->+|-
- M->*|/
例如,当在开发语言时,可能在开始的时候,选择LL(1)文法来描述语言的语法规则,编译器前端生成LL(1)语法树,编译器后端对LL(1)语法树进行处理,生成字节码或者是汇编代码。但是随着工程的开发,在语言中加入了更多的特性,用LL(1)文法描述时,感觉限制很大,并且编写文法时很吃力,所以这个时候决定采用LR(1)文法来描述语言的语法规则,把编译器前端改生成LR(1)语法树,但在这个时候,你会发现很糟糕,因为以前编译器后端是对LL(1)语树进行处理,不得不同时也修改后端的代码。
抽象语法树的第一个特点为:不依赖于具体的文法。无论是LL(1)文法,还是LR(1),或者还是其它的方法,都要求在语法分析时候,构造出相同的语法树,这样可以给编译器后端提供了清晰,统一的接口。即使是前端采用了不同的文法,都只需要改变前端代码,而不用连累到后端。即减少了工作量,也提高的编译器的可维护性。
抽象语法树的第二个特点为:不依赖于语言的细节。在编译器家族中,大名鼎鼎的gcc算得上是一个老大哥了,它可以编译多种语言,例如c,c++,java,ADA,Object C, FORTRAN, PASCAL, COBOL等等。在前端gcc对不同的语言进行词法,语法分析和语义分析后,产生抽象语法树形成中间代码作为输出,供后端处理。要做到这一点,就必须在构造语法树时,不依赖于语言的细节,例如在不同的语言中,类似于if-condition-then这样的语句有不同的表示方法
在c中为:
- if(condition)
- {
- do_something();
- }
在fortran中为:
- If condition then
- do_somthing()
- end if
在构造if-condition-then语句的抽象语法树时,只需要用两个分支节点来表于,一个为condition,一个为if_body。如下图:
在源程序中出现的括号,或者是关键字,都会被丢掉
PHP7抽象语法树
0、前言
最近项目的流程逐渐清晰,但是很多关键性的技术没有掌握,也只能一步一步摸索。
由于要做基于数据流分析的静态代码分析,所以前端的工作如:词法分析、语法分析必不可少。Yacc和Lex什么的就不再考虑了,查了一天的资料,发现两款比较适合,一款是Java下的ANTLR,另一款是专门做PHP AST生成的PHP-Parser。
ANTLR是编译原理领域比较著名的工具了,相对于Yacc和Lex,更加实用。但是对PHP的语法文件只有一个,折腾了半天才生成调通,发现不太适合,对于”$a=1”生成tokens竟然是[$,a,=,1],无法识别assignment,做得过于粗糙,令人无比失望。
相比之下,PHP-Parser更加专业一些,毕竟专注PHP的词法、语法分析工作。
1、介绍
PHP-Parser的项目主页是https://github.com/nikic/PHP-Parser。可以对多版本的PHP进行完美解析,生成一颗抽象语法树。
对于词法分析,PHP有个内置函数token_get_all()可以用来获取TOKENS,作为语法分析的输入,这个开源项目也是用的token_get_all()生成的token流。
2、安装
安装也很简单,这里我是使用的PHP中的包管理工具composer添加的,在项目目录中执行以下命令即可:
php composer.phar require nikic/php-parser
如果没有下载Composer,应该先执行下面的命令:
Curl -s http://getcomposer.org/installer | php
3、生成AST
使用composer添加php-parser之后,就可以方便使用。
首先介绍一下PHP-Parser中定义的一些节点类型:
(1)PhpParser\Node\Stmt是语句节点,不带任何返回信息(return)的结构,如赋值语句”$a = $b” ;
(2)PhpParser\Node\Expr是表达式节点,可以返回一个值的语言结构,如$var和func()。
(3)PhpParser\Node\Scalar是常量节点,可以用来表示任何常量值。如’string’,0,以及常量表达式。
(4)还有一些节点没有包括进去,如参数节点(PhpParser\Node\Arg)。
一些节点类的名称使用了下划线,这是为了避免和PHP关键字冲突。
PHP-parser的HelloWorld程序如下,该代码片段会生成AST:
输出结果为:
可以看到,这课AST只有一个节点Echo_,此节点有一个子节点exprs,可以使用$stmts[0]->exprs进行访问。
对于节点中的attributes信息是用来存储startLine和endLine以及comments的。可以使用getAttributes(),getAttribute(‘startLine’),setAttribute(),hasAttribute()方法进行访问。
开始行号startLine可以通过getLine()/setLine()方法进行访问(也可以getAttribute(‘startLine’))。注释信息可以使用getDocComment()获取。
访问节点上的值:如访问值“chongrui”,使用$stmts[0]->exprs[1]->value;即可。
4、节点遍历
对抽象语法树的遍历非常方便,使用PhpParser\NodeTraverser类即可。同时,支持自定义的Visitor对象。因为在实际应用中,对PHP源码进行分析,往往是不知道AST的具体结构,这时需要动态的去判断每个节点的类型信息。
这些判断统一写到MyNodeVisitor中,该类继承了一个父类NodeVisitorAbstract,这个类中有一些方法:
(1)beforeTraverse()方法用于遍历之前,通常用来在遍历前对值进行重置。
(2)afterTraverse()方法和(1)相同,唯一不同的地方是遍历之后才触发。
(3)enterNode()和leaveNode()方法在对每个节点访问时触发。
enterNode在进入节点时触发,比如在访问节点的子节点之前。这个方法可以返回NodeTraverser::DONT_TRAVERSER_CHILDREN,用来跳过该节点的孩子节点。
leaveNode在遍历节点完成之后触发。它可以返回
NodeTraverser::REMOVE_NODE,这种情况下,当前节点会被删除。如果返回一个节点的集合,那么这些节点会并入到父节点的array中,比如array(A,B,C),B节点被array(X,Y,Z)替换,变成array(A,X,Y,Z,C) .
下面的代码片段对$code进行解析,生成AST,并且在遍历时,当发现遍历节点时String类型时,就进行输出。
结果会输出1,2。
5、其他AST表示
有时候会将AST进行文本化持久保存,这个功能PHP-Parser也支持。
(1)简单的进行序列化
使用serialize()和unserialize()进行序列化和反序列化操作,可以对AST进行持久保存。
(2)易于阅读的保存形式
分别是完美打印和XML持久存储,在这里不做详细介绍,有需要的时候可以看项目的文档:
https://github.com/nikic/PHP-Parser/blob/master/doc/3_Other_node_tree_representations.markdown
6、总结
至少在PHP静态分析方面,PHP-Parser在功能方面大大优于ANTLR。如何构建一个PHP自动化审计系统,这个PHP-Parser肯定会发挥不小的作用:)~