这里发现
一只程序猿O(∩_∩)O
渴望用Hello World改变世界,喜欢电影,喜欢跑步,略带文艺的逗比程序猿一只!

PHP之opcode及VLD使用

OpCode,即Operation Code,操作码。通常是指计算机指令中的一部分,用于指定要执行的操作, 指令的格式和规范由处理器的指令规范指定。

通常opcode还有另一种称谓:字节码(byte codes)。例如Java编译后生成的class文件。

PHP中的opcode则属于后者,PHP构建在Zend虚拟机(Zend VM)之上。PHP的opcode就是Zend虚拟机中的指令。当Zend 引擎完成对脚本代码的编译后,便将它们生成可以直接运行的中间代码,即OpCode。

比如写了如下PHP代码:

<?php
    echo "Hello World!";
    $a = 1 +1;
    echo $a;
?>

PHP执行这段代码会经过如下4个步骤(确切的来说,应该是PHP的语言引擎Zend):

1. Scanning(Lexing) ,Zend Engine(Zend引擎),调用词法分析器(Lex生成的,源文件在Zend/zend_language_sanner.l),将我们要执行的PHP源文件,去掉空格 ,注释,分割成一个一个的语言片段(Tokens)。

2. Parsing, Zend引擎会将得到的token forward给语法分析器(yacc生成, 源文件在 Zend/zend_language_parser.y),将Tokens转换成简单而有意义的表达式。

3. Compilation, 将表达式编译成Opocdes,opcode一般会以op array的形式存在。

4. Execution, Zend引擎调用zend_executor来顺次执行Opcodes,每次一条,从而实现PHP脚本的功能。

php-life-cycle-without-opcode-cache

每一次请求到来的时候,都是这样一个过程,但是很明显每次都需要编译,是非常低效的。因为PHP源码不变,OpCode就不会改变,所以前面的步骤就不需要每次都进行,这就是所谓的OpCode缓存,就是将第三步生成的OpCode缓存下来,这样下一次请求来的时候就不需要前三步,直接执行OpCode就可以了,从而可以大大的提升执行速度。

关于Opcode缓存

详细的opcode编译过程可以看鸟哥的博客《深入理解PHP原理之OpCode》。

那如何可以看到我们的PHP脚本,最终被编译成什么样的呢? 也就是说,OpCode长的什么样子呢? 在PECL中已经有这样的模块,可以让我们直接使用了,那就是由 Derick Rethans开发的VLD (Vulcan Logic Dissassembler)模块。只需要下载这个模块,并把他作为PHP扩展载入,就可以通过简单的设置,来得到脚本编译的结果了。

1. 安装VLD

下载地址:http://pecl.php.net/package/vld

windows直接下载dll拷贝到php的ext目录中,然后在php.ini中加上extension=php_vld.dll就可以了。

Linux下载编译安装,然后同样php.ini加上extension=vld.so即可。

2. 使用VLD

php -dvld.active=1 test.php

比如上面的代码,就可以得到:

vld

PHP构建在Zend 引擎之上。PHP的opcode就是Zend虚拟机中的指令,在PHP源码({PHPSRC}/Zend/zend_vm_opcodes.c)中可以发现,一共有187条指令。顺便说一下,目前PHP源码托管到github上了,源码地址:https://github.com/php/php-src/

在PHP7之前的源码中,opcode({PHPSRC}/Zend/zend_compile.h)是这样的:

struct _zend_op {
    opcode_handler_t handler; // 执行该opcode时调用的处理函数
    znode result;
    znode op1;
    znode op2;
    ulong extended_value;
    uint lineno;
    zend_uchar opcode;  // opcode代码
};

最新的PHP7中的是这样,可以看到增加了三个字段,表示操作数的数据类型,这大概是为PHP7可以支持强类型修改的吧(猜的)。

struct _zend_op {
    const void *handler;
    znode_op op1;
    znode_op op2;
    znode_op result;
    uint32_t extended_value;
    uint32_t lineno;
    zend_uchar opcode;
    zend_uchar op1_type;
    zend_uchar op2_type;
    zend_uchar result_type;
};

各个字段的含义:

(1)handler:op的执行句柄。

(2)op1, op2, result:这三个字段是op的操作数和操作结果载体,当然并不是每个op都需要使用这三个字段,根据op的功能不同,会使用其中某些字段。比如类型为ZEND_ECHO的op只需要使用op1,功能就是将op1中的相应的值输出。


void zend_do_echo(const znode *arg TSRMLS_DC) {
    zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);
    opline->opcode = ZEND_ECHO;
    opline->op1 = *arg;
    SET_UNUSED(opline->op2);
}

PHP7之前的代码是znode类型,而php7中是znode_op类型。

PHP7中znode_op和znode的定义:

typedef union _znode_op {
    uint32_t constant;
    uint32_t var;
    uint32_t num;
    uint32_t opline_num; /* Needs to be signed */
    #if ZEND_USE_ABS_JMP_ADDR
    zend_op *jmp_addr;
    #else
    uint32_t jmp_offset;
    #endif
    #if ZEND_USE_ABS_CONST_ADDR
    zval *zv;
    #endif
} znode_op;
typedef struct _znode { /* used only during compilation */
    zend_uchar op_type;
    zend_uchar flag;
    union {
        znode_op op;
        zval constant; /* replaced by literal/zv */
    } u;
} znode;

PHP7之前版本znode:

typedef struct _znode {
    int op_type;
    union {
        zval constant;
        zend_uint var;
        zend_uint opline_num; /*  Needs to be signed */
        zend_op_array *op_array;
        zend_op *jmp_addr;
        struct {
            zend_uint var;  /* dummy */
            zend_uint type;
        } EA;
    } u;
} znode;

具体变化的原因由于并没有仔细的看源码,所以还不太明白,有空研究一下源码。

其中op_type这个int类型的字段定义操作数的类型,这些类型一共有五种:

#define IS_CONST    (1<<0)
#define IS_TMP_VAR  (1<<1)
#define IS_VAR      (1<<2)
#define IS_UNUSED   (1<<3)    /* Unused variable */
#define IS_CV       (1<<4)    /* Compiled variable */

IS_CONST

表示常量,例如$a = 123; $b = “hello”;这些代码生成OP后,123和”hello”都是以常量类型操作数存在。

IS_TMP_VAR:表示临时变量,临时变量一般在前面加~来表示,这是一些OP执行过程中需要用到的中间变量,例如初始化一个数组的时候,就需要一个临时变量来暂时存储数组zval,然后将数组赋值给变量。

IS_VAR:一般意义上的变量,以$开发表示,此种变量本人目前研究的较少,暂不介绍

IS_UNUSED :暂时不介绍,从名字来看应该是标识为不使用

IS_CV:这种类型的操作数比较重要,此类型是在PHP后来的版本中(大概5.1)中才出现,CV的意思是compiled variable,即编译后的变量,变量都是保存在一个符号表中,这个符号表是一个哈希表,试想如果每次读写变量的时候都需要到哈希表中去检索,势必会对效率有一定的影响,因此在执行上下文环境中,会将一些编译期间生成的变量缓存起来,此过程以后再详细介绍。此类型操作数一般以!开头表示,比如变量$a=123;$b=”hello”这段代码,$a和$b对应的操作数可能就是!0和!1, 0和1相当于一个索引号,通过索引号从缓存中取得相应的值。

(3)opcode:与CPU的指令类似,有一个标示指令的opcode字段。此字段保存的整形值即为op的编号,用来区分不同的op类型,opcode的可取值都被定义成了宏,可以在{PHPSRC}/Zend/zend_vm_opcodes.h中看到这些宏的定义,类似如({PHPSRC}/Zend/zend_vm_opcodes.h):

#define ZEND_NOP 0
#define ZEND_ADD 1
#define ZEND_SUB 2
#define ZEND_MUL 3
#define ZEND_DIV 4
#define ZEND_MOD 5
#define ZEND_SL 6
#define ZEND_SR 7
#define ZEND_CONCAT 8

(4)lineno:对应PHP源码中的行号。

(5)extended_value:PHP不像汇编那么底层,在脚本实际执行的时候可能还需要其他更多的信息,保存在extended_value字段。

转载请注明出处fullstackdevel.com:SEAN是一只程序猿 » PHP之opcode及VLD使用

分享到:更多 ()

Comment 抢沙发

评论前必须登录!