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源码不变,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
比如上面的代码,就可以得到:
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使用
评论前必须登录!