vld介绍
vld是PECL(PHP 扩展和应用仓库)的一个PHP扩展,现在最新版本是 0.14.0(2016-12-18),它的作用是:显示转储PHP脚本(opcode)的内部表示(来自PECL的vld简介)。简单来说,可以查看PHP程序的opcode。
vld扩展的安装
1、下载官方插件安装压缩包
官方网址:http://pecl.php.net/package/vld
下载命令:# wget http://pecl.php.net/get/vld-0.14.0.tgz
注:下载的URL是在相对的版本链接上,点击右键,复制链接即可
2、解包
解包命令:# tar zxvf vld-0.14.0.tgz
3、编译和安装
进入解压后的vld目录:# cd vld-0.14.0/
扩展php扩展模块:# phpize
使用locate找php-config路径:# locate php-config
注:locate命令没有的话可以使用命令:【# yum -y install mlocate 】 安装后使用 【# updatedb】 更新数据后可以直接使用
找到的php-config如下:
配置编译vld的php-config路径:# ./configure –with-php-config=/usr/bin/php-config –enable-vld
编译安装:# make && make install
4、重启服务器
重启Apache:# systemctl restart httpd.service
重启Nginx和PHP-fpm(如果有Nginx的话重启):
# systemctl restart nginx.service
# systemctl restart php-fpm.service
注:systemctl是新版本的centos系统有的,没有该命令的可以使用下面三个命令代替
# service apachectl restart
# service nginx restart
# service php-fpm restart
vld扩展的测试
使用phpinfo()函数测试是否已成功安装,测试代码:
<?php
phpinfo();
?>
1、使用网络访问结果如下(显示enabled):
2、使用命令测试(显示enabled):
命令是:# php test.php | grep “vld”
那么,vld怎么用捏?
vld不能单独使用,它需要和PHP命令一起。它主要有两个参数分别是 -dvld.active (等于1证明是使用vld扩展)和 -dvld.execute (等于1证明是需要执该PHP文件,默认是1)。
测试命令:# php -dvld.active=1 -dvld.execute=0 test.php
(就是指运行test.php的时候,使用vld插件,不执行,只显示opcode)
VLD(Vulcan Logic Dumper)是一个在Zend引擎中,以挂钩的方式实现的用于输出PHP脚本生成的中间代码(执行单元)的扩展。 它可以在一定程序上查看Zend引擎内部的一些实现原理,是我们学习PHP源码的必备良器。它的作者是Derick Rethans, 除了VLD扩展,我们常用的XDebug扩展的也有该牛人的身影。
VLD扩展是一个开源的项目,在这里可以下载到最新的版本,虽然最新版本的更新也是一年前的事了。 作者没有提供编译好的扩展,Win下使用VC6.0编译生成dll文件,可以看我之前写过的一篇文章(使用VC6.0生成VLD扩展)。 *nix系统下直接configue,make,make install生成。如果遇到问题,请自行Google之。
看一个简单的例子,假如存在t.php文件,其内容如下:
$a = 10;
echo $a;
在命令行下使用VLD扩展显示信息。
php -dvld.active=1 t.php
-dvld.active=1表示激活VLD扩展,使用VLD扩展输出中间代码,此命令在CMD中输出信息为:
Branch analysis from position: 0
Return found
filename: D:\work\xampp\xampp\php\t.php
function name: (null)
number of ops: 5
compiled vars: !0 = $a
line # * op fetch ext return operands
———————————————————————————
2 0 > EXT_STMT
1 ASSIGN !0, 10
3 2 EXT_STMT
3 ECHO !0
4 4 > RETURN 1
branch: # 0; line: 2- 4; sop: 0; eop: 4
path #1: 0,
10
如上为VLD输出的PHP代码生成的中间代码的信息,说明如下:
Branch analysis from position 这条信息多在分析数组时使用。
Return found 是否返回,这个基本上有都有。
filename 分析的文件名
function name 函数名,针对每个函数VLD都会生成一段如上的独立的信息,这里显示当前函数的名称
number of ops 生成的操作数
compiled vars 编译期间的变量,这些变量是在PHP5后添加的,它是一个缓存优化。这样的变量在PHP源码中以IS_CV标记。
op list 生成的中间代码的变量列表
使用-dvld.active参数输出的是VLD默认设置,如果想看更加详细的内容。可以使用-dvld.verbosity参数。
php -dvld.active=1 -dvld.verbosity=3 t.php
-dvld.verbosity=3或更大的值的效果都是一样的,它们是VLD在当前版本可以显示的最详细的信息了,包括各个中间代码的操作数等。显示结果如下:
Finding entry points
Branch analysis from position: 0
Add 0
Add 1
Add 2
Add 3
Add 4
Return found
filename: D:\work\xampp\xampp\php\t.php
function name: (null)
number of ops: 5
compiled vars: !0 = $a
line # * op fetch ext return operands
——————————————————————————–
-
2 0 > EXT_STMT RES[ IS_UNUSED ] OP1[ IS_UNUSED ] OP2[ IS_UNUSED ]
1 ASSIGN OP1[IS_CV !0 ] OP2[ , IS_CONST (0) 10 ]
3 2 EXT_STMT RES[ IS_UNUSED ] OP1[ IS_UNUSED ] OP2[ IS_UNUSED ]
3 ECHO OP1[IS_CV !0 ]
4 > RETURN OP1[IS_CONST (0) 1 ]
branch: # 0; line: 2- 3; sop: 0; eop: 4
path #1: 0,
10
以上的信息与没有加-dvld.verbosity=3的输出相比,多了Add 字段,还有中间代码的操作数的类型,如IS_CV,IS_CONST等。 PHP代码中的$a = 10; 其中10的类型为IS_CONST, $a作为一个编译期间的一个缓存变量存在,其类型为IS_CV。
如果我们只是想要看输出的中间代码,并不想执行这段PHP代码,可以使用-dvld.execute=0来禁用代码的执行。
php -dvld.active=1 -dvld.execute=0 t.php
运行这个命令,你会发现这与最开始的输出有一点点不同,它没有输出10。 除了直接在屏幕上输出以外,VLD扩展还支持输出.dot文件,如下的命令:
php -dvld.active=1 -dvld.save_dir=’D:\tmp’ -dvld.save_paths=1 -dvld.dump_paths=1 t.php
以上的命令的意思是将生成的中间代码的一些信息输出在D:/tmp/paths.dot文件中。 -dvld.save_dir指定文件输出的路径,-dvld.save_paths控制是否输出文件,-dvld.dump_paths控制输出的内容,现在只有0和1两种情况。 输出的文件名已经在程序中硬编码为paths.dot。这三个参数是相互依赖的关系,一般都会同时出现。
总结一下,VLD扩展的参数列表:
-dvld.active 是否在执行PHP时激活VLD挂钩,默认为0,表示禁用。可以使用-dvld.active=1启用。
-dvld.skip_prepend 是否跳过php.ini配置文件中auto_prepend_file指定的文件, 默认为0,即不跳过包含的文件,显示这些包含的文件中的代码所生成的中间代码。此参数生效有一个前提条件:-dvld.execute=0
-dvld.skip_append 是否跳过php.ini配置文件中auto_append_file指定的文件, 默认为0,即不跳过包含的文件,显示这些包含的文件中的代码所生成的中间代码。此参数生效有一个前提条件:-dvld.execute=0
-dvld.execute 是否执行这段PHP脚本,默认值为1,表示执行。可以使用-dvld.execute=0,表示只显示中间代码,不执行生成的中间代码。
-dvld.format 是否以自定义的格式显示,默认为0,表示否。可以使用-dvld.format=1,表示以自己定义的格式显示。这里自定义的格式输出是以-dvld.col_sep指定的参数间隔
-dvld.col_sep 在-dvld.format参数启用时此函数才会有效,默认为 “\t”。
-dvld.verbosity 是否显示更详细的信息,默认为1,其值可以为0,1,2,3 其实比0小的也可以,只是效果和0一样,比如0.1之类,但是负数除外,负数和效果和3的效果一样 比3大的值也是可以的,只是效果和3一样。
-dvld.save_dir 指定文件输出的路径,默认路径为/tmp。
-dvld.save_paths 控制是否输出文件,默认为0,表示不输出文件
-dvld.dump_paths 控制输出的内容,现在只有0和1两种情况,默认为1,输出内容
VLD(Vulcan Logic Dumper)的简介如下:
The Vulcan Logic Dumper hooks into the Zend Engine and dumps all the opcodes (execution units) of a script. It can be used to see what is going on in the Zend Engine.
之前的文章 PHP解释器引擎执行流程 结尾处提到了VLD的原理,此扩展利用PHP对扩展模块提供的请求初始化钩子函数(PHP_RINIT_FUNCTION),在每此请求到来的时候将默认的编译函数指针zend_compile_file和执行函数指针zend_execute指向自己定义的vld_compile_file函数和vld_execute函数,这两个函数中,对原函数进行了封装,原编译函数能返回一个op_array的指针,所以在新的编译函数中可以截获这个op_array的指针,然后输出相关opcode信息。
关于PHP扩展模块的安装这里就不介绍了,网络上很多相关资料。
那么让我们看看这个扩展安装后的实际效果,以下为一个非常简单的PHP脚本,test.php:
[php] view plain copy
<?php
$a = “Hello world”;
echo $a;
?>
在命令行下执行该脚本:
php -dvld.active=1 test.php
于是可以看到vld输出的内容:
希望看到跟详细的内容可以用以下方式:
php -dvld.active=1 -dvld.verbosity=3 test.php
这里简单的说说输出内容的含义:
这段代码一共有3个op分别是:
1:ASSIGN // #define ZEND_ASSIGN 38
2:ECHO // #define ZEND_ECHO 40
3:RETURN // #define ZEND_RETURN 62
第1个op ASSIGN的操作句柄是将OP2的值赋值给OP1,对应的就是$a = “Hello world”这句代码,那么OP2就是”Hello world”的,OP1应该就是$a,但是实际上输出的内容中显示的是!0,实际上$a属于编译后的变量,!0就代表了$a,可以在输出op list的上一行看到
compiled vars: !0 = $a
这样的优化可以避免每次查找变量$a都在变量符号表中去检索,起到一定的缓存的作用。在这条op执行结束之后,!0的值就等于”Hello world”了。
第2个op ECHO的操作句柄是将 OP1的内容送到标准输出,对应的就是echo $a这句代码,这样就把”Hello world”输出到终端了
第3个op RETURN 是在每个PHP文件结尾都会自动加上的,它的操作句柄是将OP1的常量值返回
这样我们就能很清晰的知道一段PHP代码会得到什么样的OP code,vld真的是一个不错的分析工具。
也许有人会问,你怎么知道每个op对应的执行句柄是什么呢,vld能输出这些信息吗?非常可惜,vld不能帮助我们输出OP对应的执行句柄信息。在默认以CALL方式执行op的模式下,每个op对应的handler都是一个函数,vld中截获的op中有这些handler的指针,但是无法通过这些指针知道相应的函数名,c语言没有一些更高级的语言那样的反射特性。所以如果想知道每个op对应的handler,就需要另外想办法了,目前为止,我只发现了两种方法可以得到这些信息。下面简单的介绍这两种方法。
方法一:
在之前的文章 PHP代码如何执行?中介绍过,op的handler都定义在{PHPSRC}/Zend/zend_vm_execute.h中,这是一个由PHP生成的极大的c源文件,其中有每个handler的函数定义以及op映射到handler的算法,在zend_init_opcodes_handlers函数中,初始化一个 static const opcode_handler_t labels[]数组,这个 labels数组就是handlers的一张表,这个表有近4000个项,每个项都是一个handler的函数指针,当然有大量的NULL指针,还有一些重复的指针。如果我们能有一个跟labels数组对应的数组handler_names,数组中的每一个项对应的是labels中相应项中函数指针的函数名,那么我们就可以通过现有的op到handler的映射算法从handler_names中得到该op的handler的函数名。但是事情没有想象的那么容易,我们如何正确生成这个拥有4000个项的数组handler_names,答案就在{PHPSRC}/Zend/zend_vm_gen.php,这个PHP文件是用来生成{PHPSRC}/Zend/zend_vm_execute.h,可以在其中找到生成labels数组的部分,只要添加相关代码通过类似方式生成handler_names数组就可以了。有兴趣的读者可以尝试生成这个handler_names数组文件,然后编译到vld扩展中,在输出op list的时候把每个op执行的句柄函数名也一并输出。
方法二:
此方法是我目前经常用到的,相对来说比较方便,还是在{PHPSRC}/Zend/zend_vm_gen.php这个文件里面想办法。这个文件会生成每个op的handler,所以如果想办法在每个handler函数的代码中输出该handler名字,那么就知道哪些handler被调用。这个并不太难,在zend_vm_gen.php第380行左右可以看到类似以下PHP代码:
if (0 && strpos($code, ‘{‘) === 0) {
...
}
实际上这个条件中的代码就是在每个handler开始的一行中输出内容,但是因为条件永远无法满足,所以实际条件中的代码无法执行,可以将if中的条件改成true,然后大括号输出函数的名字就可以了,具体的代码如下:
[php] view plain copy
if (1) {
$name = $name.($spec?”_SPEC”:””).$prefix[$op1].$prefix[$op2].”_HANDLER”;
$code = “{/n/tfprintf(stderr, /”$name//n/”);/n” . substr($code, 1);
}
代码具体的原理就不介绍了。在修改好zend_vm_gen.php之后,在命令行下执行该脚本,就会生成一个新的zend_vm_execute.h( 同时会生成zend_vm_opcodes.h),打开zend_vm_execute.h文件,可以看到很多函数开头都多出了这么一句:
fprintf(stderr, “ZEND_***/n”);
这样每个函数开始执行的时候就会把自己的名字输出到标准错误。下面的工作,就是重新编译Zend/zend_execute.lo,然后重新链接sapi/cli/php,如果你不知道如何单独完成这些操作,那么也可以更暴力一点重新安装整个PHP,需要注意的是修改后的PHP千万不要用在正式环境,因为会输出一大量不需要的信息,自己单独为试验安装一个PHP吧。 另外这个方法也会输出一些非直接的hanlder的函数名,有可能一个handler会调用另外一个函数,这样可能会输出这个handler的名字和那个被调用的函数的名字,所以实际输出的函数名字会多于op的数量。
我们用方法二来查看前面的test.php的op handler的名字,直接用修改后的php 执行test.php得到以下内容:
ZEND_ASSIGN_SPEC_CV_CONST_HANDLER
ZEND_ECHO_SPEC_CV_HANDLER
Hello worldZEND_RETURN_SPEC_CONST_HANDLER
zend_leave_helper_SPEC_HANDLER
可以看到一共输出了4个函数的名字,其中ZEND_ASSIGN_SPEC_CV_CONST_HANDLER函数就是ASSIGN的handler,ZEND_ECHO_SPEC_CV_HANDLER就是ECHO的handler,ZEND_RETURN_SPEC_CONST_HANDLER是RETURN的handler,这个handler会调用zend_leave_helper_SPEC_HANDLER函数,所以会输出4个函数的名字,知道了这些函数的名字,我们就能在zend_vm_execute.h中去找到其具体定义,这样就知道每个op到底是怎么在执行了。