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:
输出结果为:
array( [0] => phpparser\node\stmt\echo_ object ( [subnodes:protected] => array ( [exprs] => array ( [0] => phpparser\node\scalar\string object ( [subnodes:protected] => array ( [value] => 1+2 ) [attributes:protected] => array ( [startline] => 1 [endline] => 1 ) ) [1] => phpparser\node\scalar\string object ( [subnodes:protected] => array ( [value] => chongrui ) [attributes:protected] => array ( [startline] => 1 [endline] => 1 ) ) ) ) [attributes:protected] => array ( [startline] => 1 [endline] => 1 ) ))
可以看到,这课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肯定会发挥不小的作用:)~
