编译与执行

 

PHP7与PHP5在编译执行上的区别

 

在PHP7之前的版本,PHP代码在语法解析阶段直接生成了ZendVM指令(也就是opline指令,后面会聊一下opline指令的~),这使得编译器与执行器耦合在一起。这个模式的坏处就是,当我们如果要换一个VM时候,就需要修改语法解析规则,或者如果PHP的语法规则变了,(例如php中访问对象使用->来访问,想换成成java中 .  来访问),我们也需要去修改语法解析规则。

PHP7加入了抽象语法树,首先将PHP代码解析成抽象语法树,然后将抽象语法树编译成为ZendVM指令。抽象语法树的加入,使得PHP的编译器与执行器很好的隔离开来了,编译器不需要关心指令的生成规则,而执行器同样不关心指令的语法规则是什么样子的,执行器根据自己的规则将抽象语法树编译成对应的指令。

编译型语言和解析型语言的区别

编译型语言 C语言

解析性语言 php

 

解析型语言与实际计算机之间多了一层解析器,屏蔽了不同平台之间机器语言的差异,由解析器去处理不同平台之间的差异,实现了跨平台运行。但是带来的代价是运行效率低,与编译性语言直接执行机器指令想比,多了一层解析器的工作。

抽象语法树(AST)

词法解析,语法解析

PHP使用re2c、bison完成这个阶段的工作:

  • re2c: 词法分析器,将输入分割为一个个有意义的词块,称为token
  • yacc: 语法分析器,确定词法分析器分割出的token是如何彼此关联的

词法分析器将上面的语句分解为这些token:$a、=、2、+、3、- 、6,接着语法分析器确定了2+3-6是一个表达式,而这个表达式被赋值给了a

语法分析树

PHP的抽象语法树定义

typedef struct _zend_ast_list {
    zend_ast_kind kind; //语句类型
    zend_ast_attr attr;
    uint32_t lineno;
    uint32_t children;
    zend_ast *child[1];
} zend_ast_list;

PHP抽象语法树拆分逻辑比较难理解,我给出一个例子,我们看下最终生成的语法树。

Zend虚拟机

Zend虚拟机就是PHP语言的解析器,负责PHP代码的解析,执行。

ZendVM对于计算机而言就是普通二进制可执行程序,是编译好的机器指令,而PHP代码被编译成ZendVM可识别的指令,而不是机器指令,然后ZendVM来执行,最终变为机器指令。

ZendVM由以下部分组成:

Opline指令

opline是ZendVM定义的执行指令,每条指令的编码都是opcode。

struct _zend_op {
   const void *handler; //指令执行handler
   znode_op op1;  //操作数1
   znode_op op2;  //操作数2
   znode_op result; //返回值
   uint32_t extended_value;
   uint32_t lineno;
   zend_uchar opcode; //opcode指令
   zend_uchar op1_type;  //操作数1类型
   zend_uchar op2_type;  //操作数2类型
   zend_uchar result_type; //返回值类型
};
 
 
//opcode 定义
#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
#define ZEND_BW_OR                             9
#define ZEND_BW_AND                           10
#define ZEND_BW_XOR                           11


举个例子:例如赋值操作 $a = 123,操作数1用来告诉VM 变量$a的位置,操作数2用来保存变量123的位置,执行的时候,ZendVM从操作数1与2获取信息,然后进行对应动作(此时opcode指令为 ZEND_ADD)opline指令描述其实就是:对什么数据,做什么处理!

Zend_op_array

opline 是编译生成的单条指令,所有的指令组合生成了zend_op_array

struct _zend_op_array {
   /* Common elements */
   .... 以上省略
   uint32_t *refcount;
 
   uint32_t this_var;
 
   uint32_t last;
   zend_op *opcodes; //这里就是指令集合,是一个数组,执行器执行时从该数组的第一条指令开始,直到最后
 
   int last_var;
   uint32_t T;
   zend_string **vars;
   ....以上省略
};


zend_execute_data是执行过程中最核心的一个结构,每次函数的调用、include/require、eval等都会生成一个新的结构,它表示当前的作用域、代码的执行位置以及局部变量的分配等等。Zend_execute_data

#define EX(element)             ((execute_data)->element)
 
//zend_compile.h
struct _zend_execute_data {
    const zend_op       *opline;  //指向当前执行的opcode,初始时指向zend_op_array起始位置
    zend_execute_data   *call;             /* current call                   */
    zval                *return_value;  //返回值指针
    zend_function       *func;          //当前执行的函数(非函数调用时为空)
    zval                 This;          //这个值并不仅仅是面向对象的this,还有另外两个值也通过这个记录:call_info + num_args,分别存在zval.u1.reserved、zval.u2.num_args
    zend_class_entry    *called_scope;  //当前call的类
    zend_execute_data   *prev_execute_data; //函数调用时指向调用位置作用空间
    zend_array          *symbol_table; //全局变量符号表
#if ZEND_EX_USE_RUN_TIME_CACHE
    void               **run_time_cache;   /* cache op_array->run_time_cache */
#endif
#if ZEND_EX_USE_LITERALS
    zval                *literals;  //字面量数组,与func.op_array->literals相同
#endif
};


Zend_execute_data 与Zend_op_array的关系

执行器

zend执行opcode的简略过程

  • step1: 为当前作用域分配一块内存,充当运行栈,zend_execute_data结构、所有局部变量、中间变量等等都在此内存上分配
  • step2: 初始化全局变量符号表,然后将全局执行位置指针EG(current_execute_data)指向step1新分配的zend_execute_data,然后将zend_execute_data.opline指向op_array的起始位置
  • step3: 从EX(opline)开始调用各opcode的C处理handler(即_zend_op.handler),每执行完一条opcode将EX(opline)++继续执行下一条,直到执行完全部opcode,函数/类成员方法调用、if的执行过程:
  •            step3.1: 如果是函数调用,则首先从EG(function_table)中根据function_name取出此function对应的编译完成的zend_op_array,然后像step1一样新分配一个zend_execute_data结构,将EG(current_execute_data)赋值给新结构的prev_execute_data,再将EG(current_execute_data)指向新的zend_execute_data,最后从新的zend_execute_data.opline开始执行,切换到函数内部,函数执行完以后将EG(current_execute_data)重新指向EX(prev_execute_data),释放分配的运行栈,销毁局部变量,继续从原来函数调用的位置执行
  • 全部opcode执行完成后将step1分配的内存释放,这个过程会将所有的局部变量"销毁",执行阶段结束


 

你可能感兴趣的:(读PHP7源码日记)