源码分析必备:用vld查看opcode信息

 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 -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,然后大括号输出函数的名字就可以了,具体的代码如下:

 代码具体的原理就不介绍了。在修改好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到底是怎么在执行了。

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
初识Visual Leak Detector   灵活自由是C/C++语言的一大特色,而这也为C/C++程序员出了一个难题。当程序越来越复杂时,内存的管理也会变得越加复杂,稍有不慎就会出现内存问题。内存泄漏是最常见的内存问题之一。内存泄漏如果不是很严重,在短时间内对程序不会有太大的影响,这也使得内存泄漏问题有很强的隐蔽性,不容易被发现。然而不管内存泄漏多么轻微,当程序长时间运行时,其破坏力是惊人的,从性能下降到内存耗尽,甚至会影响到其他程序的正常运行。另外内存问题的一个共同特点是,内存问题本身并不会有很明显的现象,当有异常现象出现时已时过境迁,其现场已非出现问题时的现场了,这给调试内存问题带来了很大的难度。   Visual Leak Detector是一款用于Visual C++的免费的内存泄露检测工具。相比较其它的内存泄露检测工具,它在检测到内存泄漏的同时,还具有如下特点:   1、 可以得到内存泄漏点的调用堆栈,如果可以的话,还可以得到其所在文件及行号;   2、 可以得到泄露内存的完整数据;   3、 可以设置内存泄露报告的级别;   4、 它是一个已经打包的lib,使用时无须编译它的源代码。而对于使用者自己的代码,也只需要做很小的改动;   5、 他的源代码使用GNU许可发布,并有详尽的文档及注释。对于想深入了解堆内存管理的读者,是一个不错的选择。   可见,从使用角度来讲,Visual Leak Detector简单易用,对于使用者自己的代码,唯一的修改是#include Visual Leak Detector的头文件后正常运行自己的程序,就可以发现内存问题。从研究的角度来讲,如果深入Visual Leak Detector源代码,可以学习到堆内存分配与释放的原理、内存泄漏检测的原理及内存操作的常用技巧等。   本文首先将介绍Visual Leak Detector的使用方法与步骤,然后再和读者一起初步的研究Visual Leak Detector的源代码,去了解Visual Leak Detector的工作原理。   使用Visual Leak Detector(1.0)   下面让我们来介绍如何使用这个小巧的工具。   首先从网站上下载zip包,解压之后得到vld.h, vldapi.h, vld.lib, vldmt.lib, vldmtdll.lib, dbghelp.dll等文件。将.h文件拷贝到Visual C++的默认include目录下,将.lib文件拷贝到Visual C++的默认lib目录下,便安装完成了。因为版本问题,如果使用windows 2000或者以前的版本,需要将dbghelp.dll拷贝到你的程序的运行目录下,或其他可以引用到的目录。   接下来需要将其加入到自己的代码中。方法很简单,只要在包含入口函数的.cpp文件中包含vld.h就可以。如果这个cpp文件包含了stdafx.h,则将包含vld.h的语句放在stdafx.h的包含语句之后,否则放在最前面。如下是一个示例程序:   #include   void main()   {   …   }   接下来让我们来演示如何使用Visual Leak Detector检测内存泄漏。下面是一个简单的程序,用new分配了一个int大小的堆内存,并没有释放。其申请的内存地址用printf输出到屏幕上。   #include   #include   #include   void f()   {   int *p = new int(0x12345678);   printf("p=%08x, ", p);   }   void main()   {   f();   }   编译运行后,在标准输出窗口得到:   p=003a89c0   在Visual C++的Output窗口得到:   WARNING: Visual Leak Detector detected memory leaks!   ---------- Block 57 at 0x003A89C0: 4 bytes ---------- --57号块0x003A89C0地址泄漏了4个字节   Call Stack: --下面是调用堆栈   d:\test\testvldconsole\testvldconsole\main.cpp (7): f --表示在main.cpp第7行的f()函数   d:\test\testvldconsole\testvldconsole\main.cpp (14): main –双击以引导至对应代码处   f:\rtm\vctools\crt_bld\self_x8
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值