55

作者 陈雷
编程语言的虚拟机是一种可以运行中间语言的程序。中间语言是抽象出的指令集,由原生语言编译而成,作为虚拟机执行阶段的输入。很多语言都实现了自己的虚拟机,比如Java、C#和Lua。PHP语言也有自己的虚拟机,称为Zend虚拟机

PHP7完成基本的准备工作后,会启动Zend引擎,加载注册的扩展模块,然后读取对应的脚本文件,Zend引擎会对文件进行词法和语法分析,生成抽象语法树,接着抽象语法树被编译成Opcodes,如果开启了Opcache,编译的环节会被跳过从Opcache中直接读取Opcodes进行执行。

PHP7中词法语法分析,生成抽象语法树,然后编译成Opcodes及被执行均由Zend虚拟机完成。这里将详细阐述抽象语法树编译成Opcodes的过程,以及Opcodes被执行的过程,来阐述Zend虚拟机的实现原理及关键的数据结构。

1 基础知识

Zend虚拟机(称为Zend VM)是PHP语言的核心,承担了语法词法解析、抽象语法树编译以及指令的执行工作,下面我们讨论一下Zend虚拟机的基础架构以及相关的基础知识。

1.1 Zend虚拟机架构

Zend虚拟机主要分为解释层、中间数据层和执行层,下面给出各层包含的内容,如图1所示。

clipboard.png
图1 Zend虚拟机架构图

下面解释下各层的作用。

(1)解释层

这一层主要负责把PHP代码进行词法和语法分析,生成对应的抽象语法树;另一个工作就是把抽象语法树进行编译,生成符号表和指令集;

(2)中间数据层

这一层主要包含了虚拟机的核心部分,执行栈的维护,指令集和符号表的存储,而这三个是执行引擎调度执行的基础;

(3)执行层

这一层是执行指令集的引擎,这一层是最终的执行并生成结果,这一层里面实现了大量的底层函数。

为了更好地理解Zend虚拟机各层的工作,我们先了解一下物理机的一些基础知识,读者可以对照理解虚拟机的原理。

1.2 符号表

符号表是在编译过程中,编译程序用来记录源程序中各种名字的特性信息,所以也称为名字特性表。名字一般包含程序名、过程名、函数名、用户定义类型名、变量名、常量名、枚举值名、标号名等。特性信息指的是名字的种类、类型、维数、参数个数、数值及目标地址(存储单元地址)等。

符号表有什么作用呢?一是协助进行语义检查,比如检查一个名字的引用和之前的声明是否相符,二是协助中间代码生成,最重要的是在目标代码生成阶段,当需要为名字分配地址时,符号表中的信息将是地址分配的主要依据。

clipboard.png
图2 符号表创建示例

符号表一般有三种构造和处理方法,分别是线性查找,二叉树和Hash技术,其中线性查找法是最简单的,按照符号出现的顺序填表,每次查找从第一个开始顺序查找,效率比较低;二叉树实现了对折查找,在一定程度上提高了效率;效率最高的是通过Hash技术实现符号表,相信大家对Hash技术有一定的了解,而PHP7中符号表就是使用的HashTable实现的。

1.3 函数调用栈

为了更清晰地了解虚拟机中函数调用的过程,我们先了解一下物理机的简单原理,主要涉及函数调用栈的概念,而Zend虚拟机参照了物理机的基本原理,做了类似的设计。

下面以一段C代码描述一下系统栈和函数过程调用,代码如下:

  int funcB(int argB1, int argB2)

  {

         int varB1, varB2;

         return argB1+argB2;

  }

  int funcA(int argA1, int argA2)

  {

       int varA1, varA2;

        return argA1+argA2+funcB( 3, 4);

  }    

  int main()

  {

      int varMain;

      return funcA(1, 2);

     }

这段代码运行时,首先main函数会压栈, 首先将局部变量varMain入栈,main函数调用了funcA函数,C语言会从后往前,将函数参数压栈,先压第二个参数argA2=2,再压第一个参数argA1=1,同时对于funcA的返回会产生一个临时变量等待赋值,也会被压栈,这些称为main函数的栈帧;接着将funcA压栈,同样的先将局部变量varA1和varA2压入栈中,因为调用了函数funcB,会将参数argB2=4和argB1=3压入栈中,同时把funcB的返回产生的临时变量压入栈中,这部分称为funcA的栈帧;同样,funcB被压入栈中,如图3所示。

clipboard.png
图3 函数调用压栈过程示意图

funcB函数执行,对argB1和argB2进行相加操作,执行后得到返回值为7,然后funcB的栈帧出栈,funcA中临时变量TempB被赋值为7,继而进行相加操作,得到结果为10,然后funcA出栈,main函数中临时变量TempA被赋值为10,最终main函数返回并出栈,整个函数调用结束。如图4所示。

clipboard.png
图4 函数调用出栈过程示意图

1.4 指令

汇编语句中的指令语句一般格式为:

     [标号:]     [前缀]  指令助记符    [操作数]     [;注释]

其中:

  • 1)标识符字段由各种有效字符组成,一般表示符号地址,具有段基址、偏移量、类型三种属性。通常情况下这部分是可选部分,主要为便于程序的读写方便而使用。
  • 2)助记符,规定指令或伪指令的操作功能,是语句中唯一不可缺少的部分。对于指令,汇编程序会将其翻译成机器语言指令:
MOV   AX, 100  →   B8 00 01
  • 3)操作数,指令语句中提供给指令的操作对象、存放位置。操作数可以是1个、2个或0个,2个时用逗号‘,’分开。比如“RET;”对应的操作数个数是0个,“INC BX;”对应的操作数个数是1,“MOV AX,DATA;”对应的操作数个数是2个。
  • 4)注释,以“ ;”开始,给以编程说明。

    符号表、函数调用栈以及指令基本构成了物理机执行的基本元素,Zend虚拟机也同样实现了符号表,函数调用栈及指令,来运行PHP代码,下面我先讨论一下Zend虚拟机相关的数据结构。

2相关数据结构

Zend虚拟机包含了词法语法分析,抽象语法树的编译,以及Opcodes的执行,本文主要详细介绍抽象语法树和Opcodes的执行过程,在展开介绍之前,先阐述一下用到的基本的数据结构,为后面原理性的介绍奠定基础。

2.1 EG(v)

首先介绍的是全局变量executor_globals,EG(v)是对应的取值宏,executor_globals对应的是结构体_zend_executor_globals,是PHP生命周期中非常核心的数据结构。这个结构体中维护了符号表(symbol_table, function_table,class_table等),执行栈(zend_vm_stack)以及包含执行指令的zend_execute_data,另外还包含了include的文件列表,autoload函数,异常处理handler等重要信息,下面给出_zend_executor_globals的结构图,然后分别阐述其含义,如图5所示。

clipboard.png
图5 EG(v)结构图

这个结构体比较复杂,下面我们介绍几个核心的成员。

  • 1)symbol_table:符号表,这里面主要是存的全局变量,以及一些魔术变量,比如$_GET、$_POST等;
  • 2)function_table:函数表,主要存放函数,包括大量的内部函数,以及用户自定义的函数,比如zend_version,func_num_args,str系列函数,等等;
  • 3)class_table:类表,主要存放内置的类以及用户自定义的类,比如stdclass、throwable、exception等类;
  • 4)zend_constants:常量表,存放PHP中的常量,比如E_ERROR、E_WARNING等;
  • 5)vm_stack:虚拟机的栈,执行时压栈出栈都在这上面操作;
  • 6)current_execute_data:对应_zend_execute_data结构体,存放执行时的数据。

下面针对于符号表、指令集、执行数据和执行栈进行详细介绍。

2.2 符号表

PHP7中符号表分为了symbol_table、function_table和class_table等。

  • (1)symbol_table

symbol_table里面存放了变量信息,其类型是HashTable,下面我们看一下具体的定义:

       //符号表缓存

       zend_array *symtable_cache[SYMTABLE_CACHE_SIZE];

      zend_array **symtable_cache_limit;

      zend_array **symtable_cache_ptr;

       //符号表

       zend_array symbol_table;

symbol_table里面有什么呢,代码”$a=1;”对应的symnol_table,如图6所示。

clipboard.png
图6 symbol_table示意图

从图6中可以看出,符号表中有我们常见的超全局变量$_GET、$_POST等,还有全局变量$a。在编译过程中会调用zend_attach_symbol_table函数将变量加入symbol_table中。

  • (2)function_table

function_table对应的是函数表,其类型也是HashTable,见代码:

       HashTable *function_table;  /* function symbol table */

函数表中存储哪些函数呢?同样以上述代码为例,我们利用GDB印一下function_table的内容:

(gdb) p *executor_globals.function_table

$1 = {gc = {refcount = 1, u = {v = {type = 7 '\a', flags = 0 '\000', gc_info = 0}, type_info = 7}}, u = {v = {

      flags = 25 '\031', nApplyCount = 0 '\000', nIteratorsCount = 0 '\000', consistency = 0 '\000'},

    flags = 25}, nTableMask = 4294966272, arData = 0x12102b0, nNumUsed = 848, nNumOfElements = 848,

  nTableSize = 1024, nInternalPointer = 0, nNextFreeElement = 0, pDestructor = 0x8d0dc3 <zend_function_dtor>}

可以看出,函数表中有大量的函数,上面打印显示有848个之多,这里面主要是内部函数,比如zend_version、func_num_args、cli_get_process_title,等等。

  • (3)class_table

class_table对应的是类表,其也是HashTable:

 HashTable *class_table; /* class table */

类表里面也有大量的内置类,比如stdclass、traversable、xmlreader等。

符号表里面存放了执行时需要的数据,比如在symbol_table中,key为_GET的Bucket对应的又是个HashTable,里面存放的是$_GET[xxx],执行时会从中取对应的值。

2.3 指令

Zend虚拟机的指令称为opline,每条指令对应一个opcode。PHP代码在编译后生成opline,Zend虚拟机根据不同的opline完成PHP代码的执行,opline由操作指令、操作数和返回值组成,与机器指令非常类似,opline对应的结构体为zend_op,见代码:

  struct _zend_op {

      const void *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; //返回值的类型

};

对应的内存占用图如图7所示。

clipboard.png
图7 zend_op结构图

PHP代码会被编译成一条一条的opline,分解为最基本的操作,举个例子,如果把opcode当成一个计算器,只接受两个操作数op1和 op2,执行一个操作handler,比如加减乘除,然后它返回一个结果result,再稍加处理算术溢出的情况存在extended_value中。下面详细介绍下各个字段。

  • (1) Opcode

Opcode有时候被称为所谓的字节码(Bytecode),是被软件解释器解释执行的指令集。这些软件指令集通常会提供一些比对应的硬件指令集更高级的数据类型和操作。

注意:Opcode和Bytecode其实是两个含义不同的词,但经常会把它们当作同一个意思来交互使用。

Zend虚拟机有非常多Opcode,对应可以做非常多事情,并且随着PHP的发展, Opcode也越来越多,意味着PHP可以做越来越多的事情。所有的Opcode都在PHP的源代码文件Zend/zend_vm_opcodes.h中定义。Opcode的名称是自描述的,比如:

  • ZEND_ASSGIN:赋值操作;
  • ZEND_ADD:两个数相加操作;
  • ZEND_JMP:跳转操作。

PHP 7.1.0中有186个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_FETCH_THIS                      184

#define ZEND_ISSET_ISEMPTY_THIS              186

#define ZEND_VM_LAST_OPCODE                  186
  • (2)操作数

op1和op2都是操作数,但不一定全部使用,也就是说每个Opcode对应的hanlder最多可以使用两个操作数(也可以只使用其中一个,或者都不使用)。每个操作数都可以理解为函数的参数,返回值result是hanlder函数对操作数op1和op2计算后的结果。op1、op2和result对应的类型都是znode_op,其定义为一个联合体:

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;

这样其实每个操作数都是uint32类型的数字,一般表示的是变量的位置。操作数有5种不同的类型,具体在Zend/zend_compile.h中定义:

#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 */

这些类型是按位表示的,具体含义如下。

  • 1)IS_CONST:值为1,表示一个常量,都是只读的,值不可改变,比如$a=”hello world” 中的hello world。
  • 2)IS_VAR:值为4,是PHP变量,这个变量并不是PHP代码中声明的变量,常见的是返回的临时变量,比如$a=time(), 函数time返回值的类型就是IS_VAR,这种类型的变量是可以被其他Opcode对应的handler重复使用的。
  • 3)IS_TMP_VAR :值为2,也是PHP变量,跟IS_VAR不同之处是,不能与其他Opcode重复使用,举个例子,$a=”123”.time(); 这里拼接的临时变量”123”.time()的类型就是IS_TMP_VAR,一般用于操作的中间结果。
  • 4)IS_UNUSED :值为8,表示这个操作数没有包含任何有意义的东西。
  • 5)IS_CV :值为16,编译变量(Compiled Variable):这个操作数类型表示一个PHP变量:以$something形式在PHP脚本中出现的变量。
  • (3)handler

handler为Opcode对应的是实际的处理函数,Zend虚拟机对每个Opcode的工作方式是完全相同的,都有一个handler的函数指针,指向处理函数的地址,这是一个C函数,包含了执行Opcode对应的代码,使用op1,op2做为参数,执行完成后,会返回一个结果result,有时也会附加一段信息extended_value。文件Zend/zend_vm_execute.h包含了所有的handler对应的函数,php-7.1.0中这个文件有62000+行。

注意:Zend/zend_vm_execute.h并非手动编写的,而是由zend_vm_gen.php这个PHP脚本解析zend_vm_def.h和zend_vm_execute.skl后生成,这个很有意思,先有鸡还是先有蛋?没有PHP 哪来的这个php脚本呢?这个是后期产物,早期php版本不用这个。这个类似于GO语言的自举,自己编译自己。

同一个Opcode对应的handler函数会根据操作数的类型而不同,比如ZEND_ASSIGN对应的handler就有多个:

ZEND_ASSIGN_SPEC_VAR_CONST_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_CONST_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_TMP_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_TMP_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_VAR_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_VAR_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_CV_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_CV_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_CV_TMP_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_CV_TMP_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_CV_VAR_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_CV_VAR_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_CV_CV_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_CV_CV_RETVAL_USED_HANDLER,

其函数命名是有如下规则的:

ZEND_[opcode]_SPEC_(操作数1类型)_(操作数2类型)_(返回值类型)_HANDLER

举个例子,对于PHP代码:

$a = 1;

对应的handler为ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,其定义为:

static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS)

{

      USE_OPLINE

 

      zval *value;

      zval *variable_ptr;

 

      SAVE_OPLINE();

        //获取op2对应的值,也就是1

      value = EX_CONSTANT(opline->op2);

        //在execute_data中获取op1的位置,也就是$a

      variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var);

        /*代码省略*/

       //将1赋值给$a

      value = zend_assign_to_variable(variable_ptr, value, IS_CONST);

              

      }

         /*代码省略*/

      ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

}

从代码中可以非常直观的看出,常量1是如何赋值给CV类型的$a的。

  • (4)extended_value

extended_value是存的扩展信息,Opcodes和CPU的指令类似,有一个标示指令字段handler,以及这个Opcode所操作的操作数op1和op2,但PHP不像汇编那么底层, 在脚本实际执行的时候可能还需要其他更多的信息,extended_value字段就保存了这类信息;

  • (5)lineno

lineno对应源代码文件中的行号。

到这里,相信读者对指令opline有了比较深刻的认识,在Zend虚拟机执行时,这些指令被组装在一起,成为指令集,下面我们介绍一下指令集。

2.4 指令集

在介绍指令集前,需要先介绍一个编译过程用到的一个基础的结构体znode,其结构如下:

  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

znode只会在编译过程中使用,其中op_type对应的是变量的类型,u是联合体,u.op是操作数的类型,zval constant用来存常量。znode在后续生成指令集时会使用到。

Zend虚拟机中的指令集对应的结构为zend_op_array,其结构如下:

  struct _zend_op_array {

      /* Common elements */

      /*代码省略common是为了函数能够快速访问Opcodes而设定的*/

      /* END of common elements */

      //这部分是存放opline的数组,last为总个数

      uint32_t last;

      zend_op *opcodes;

     

      int last_var;//变量类型为IS_CV的个数

      uint32_t T;//变量类型为IS_VAR和IS_TMP_VAR的个数

      zend_string **vars;//存放IS_CV类型变量的数组

       /*代码省略*/

      /* static variables support */

      HashTable *static_variables; //静态变量

        /*代码省略*/

      int last_literal;//常量的个数

      zval *literals;//常量数组

 

      int  cache_size;//运行时缓存数组大小

      void **run_time_cache;//运行时缓存

 

      void *reserved[ZEND_MAX_RESERVED_RESOURCES];

};

其结构图如图8所示。

clipboard.png
图8 zend_op_array结构图

这个结构体中有几个关键变量。

  • 1)last和opcodes,这部分是存放Opline的数组,也就是指令集存放的位置,其中last为数组中Opline的个数。
  • 2)last_var代表IS_CV类型变量的个数,这种类型变量存放在vars数组中;在整个编译过程中,每次遇到一个IS_CV类型的变量(类似于$something),就会去遍历vars数组,检查是否已经存在,如果不存在,则插入到vars中,并将last_var的值设置为该变量的操作数;如果存在,则使用之前分配的操作数,见代码:
 result->op_type = IS_CV;

 result->u.op.var = lookup_cv(CG(active_op_array), name);

 

  //lookup_cv:

  static int lookup_cv(zend_op_array *op_array, zend_string* name) /* {{{ */{

      int i = 0;

      zend_ulong hash_value = zend_string_hash_val(name);

 

       //遍历vars

      while (i < op_array->last_var) {

                  //如果存在直接返回

               if (ZSTR_VAL(op_array->vars[i]) == ZSTR_VAL(name) ||

                   (ZSTR_H(op_array->vars[i]) == hash_value &&

                    ZSTR_LEN(op_array->vars[i]) == ZSTR_LEN(name) &&

                    memcmp(ZSTR_VAL(op_array->vars[i]), ZSTR_VAL(name), ZSTR_LEN(name)) == 0)) {

                        zend_string_release(name);

                        return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);

               }

               i++;

      }

       //否则插入到vars中,并将last_var的值设置为该变量的操作数

      i = op_array->last_var;

      op_array->last_var++;

      if (op_array->last_var > CG(context).vars_size) {

               CG(context).vars_size += 16; /* FIXME */

               op_array->vars = erealloc(op_array->vars, CG(context).vars_size * sizeof(zend_string*));

      }

 

      op_array->vars[i] = zend_new_interned_string(name);

      return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);

}
  • 3)T为IS_VAR和IS_TMP_VAR类型变量的总数,编译时遇到这种类型,T就会加一,用于后续在执行栈上分配空间。
  • 4)static_variables是用于存放静态变量的HashTable。
  • 5)literals是用来存放常量(IS_CONST)类型的数组,last_literal为常量的个数。
  • 6)run_time_cache用作运行时缓存的操作,本书不展开讨论。

2.5 执行数据

Zend在栈上执行的数据为zend_execute_data,其结构体为:

struct _zend_execute_data {

      const zend_op       *opline;           /* 要执行的指令 */

      zend_execute_data   *call;             /* current call*/

      zval                *return_value;     /* 返回值 */

      zend_function       *func;             /* 执行函数 */

      zval                 This;             /* this + call_info + num_args */

      zend_execute_data   *prev_execute_data;

      zend_array          *symbol_table; /*符号表*/

      void               **run_time_cache;   /* 执行时缓存 */

      zval                *literals;         /* 缓存常量 */

};

下面我们介绍下各字段。

  • 1)opline对应的是zend_op_array中Opcodes数组里面的zend_op,表示正在执行的opline。
  • 2)prev_execute_data: op_array上下文切换的时候,这个字段用来保存切换前的op_array,此字段非常重要,能将每个op_array的execute_data按照调用的先后顺序连接成一个单链表,每当一个op_array执行结束要还原到调用前op_array的时候,就通过当前的execute_data中的prev_execute_data字段来得到调用前的执行器数据。
  • 3)symbol_table,当前使用的符号表,一般会取EG(symbol_table)。
  • 4)literals,常量数组,用来缓存常量。

zend_execute_data是在执行栈上运行的关键数据,可以用EX宏来取其中的值,见代码:

#define EX(element) ((execute_data)->element)

了解完执行数据,下面接下来讨论一下执行栈。

2.6 执行栈

Zend虚拟机中有个类似函数调用栈的结构体,叫_zend_vm_stack。EG里面的vm_stack也是这种类型的。其定义如下:

struct _zend_vm_stack {

      zval *top; //栈顶位置

      zval *end; //栈底位置

      zend_vm_stack prev;

};

typedef struct _zend_vm_stack *zend_vm_stack;

可以看出,栈的结构比较简单,有三个变量top指向栈使用到的位置,end指向栈底,pre是指向上一个栈的指针,也就意味着所有栈在一个单向链表上。
PHP代码在执行时,参数的压栈操作,以及出栈调用执行函数都是在栈上进行的,下面介绍下栈操作的核心函数。

  • 1)初始化

初始化调用的函数为zend_vm_stack_init,主要进行内存申请,以及对_zend_vm_stack成员变量的初始化,见代码:

ZEND_API void zend_vm_stack_init(void)

{

      EG(vm_stack) = zend_vm_stack_new_page(ZEND_VM_STACK_PAGE_SIZE(0 /* main stack */), NULL);

      EG(vm_stack)->top++;

      EG(vm_stack_top) = EG(vm_stack)->top;

      EG(vm_stack_end) = EG(vm_stack)->end;

}

该函数调首先调用zend_vm_stack_new_page为EG(vm_stack)申请内存,申请的大小为161024 sizeof(zval),见代码:

static zend_always_inline zend_vm_stack zend_vm_stack_new_page(size_t size, zend_vm_stack prev) {

      zend_vm_stack page = (zend_vm_stack)emalloc(size);

 

      page->top = ZEND_VM_STACK_ELEMENTS(page);

      page->end = (zval*)((char*)page + size);

      page->prev = prev;

      return page;

}

然后将zend_vm_stack的top指向zend_vm_stack的结束的位置,其中 zend_vm_stack占用24个字节,end指向申请内存的最尾部,pre指向null,如图9所示。

clipboard.png
图9 zend_vm_stack初始化后示意图

可以看出,多个vm_stack构成单链表,将多个栈连接起来,栈初始的大小为16×1024个zval的大小,栈顶部占用了一个*zval和struct _zend_vm_stack的大小,

  • (2)入栈操作

调用的函数为zend_vm_stack_push_call_frame,见代码:

static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame(uint32_t call_info, zend_function *func, uint32_t num_args, zend_class_entry *called_scope, zend_object *object)

{

      uint32_t used_stack = zend_vm_calc_used_stack(num_args, func);

 

      return zend_vm_stack_push_call_frame_ex(used_stack, call_info,

               func, num_args, called_scope, object);

}

该函数会分配一块用于当前作用域的内存空间,并返回zend_execute_data的起始位置。首先调用zend_vm_calc_used_stack计算栈需要的空间,见代码:

 static zend_always_inline uint32_t zend_vm_calc_used_stack(uint32_t num_args, zend_function *func)

{

      uint32_t used_stack = ZEND_CALL_FRAME_SLOT + num_args;

 

      if (EXPECTED(ZEND_USER_CODE(func->type))) {

               used_stack += func->op_array.last_var + func->op_array.T - MIN(func->op_array.num_args, num_args);

      }

      return used_stack * sizeof(zval);

}

这段代码是按照zval的大小对齐,我们知道zval为16字节,那么对于zend_execute_data,大小为80,那么对应5个zval;同时对应IS_CV类型变量个数(last_var)以及变量类型为IS_VAR和IS_TMP_VAR的个数(T),如图10所示。

clipboard.png
图10 压栈过程

到此,我们了解了Zend虚拟机中符号表、指令集、执行数据以及执行栈相关的数据结构,下面我们基于这些基本知识,来介绍一下指令集生成的过程。

3 抽象语法树编译过程

抽象语法树(AST)的编译是生成指令集Opcodes的过程,词法语法分析后生成的AST会保存在CG(ast)中,然后Zend虚拟机会将AST进一步转换为zend_op_array,以便在虚拟机中执行。下面我们讨论一下AST的编译过程。

编译过程在zend_compile函数中,在该函数里,首先调用zendparse做了词法和语法分析的工作,然后开始对CG(ast)的遍历,根据节点不同的类型编译为不同指令opline,代码如下:

static zend_op_array *zend_compile(int type)

{

      /**代码省略**/

      if (!zendparse()) { //词法语法分析

               /**代码省略**/    

 

                 //初始化zend_op_array       

                 init_op_array(op_array, type, INITIAL_OP_ARRAY_SIZE);

               /**代码省略**/

 

                 //遍历ast生成opline

               zend_compile_top_stmt(CG(ast));

               /**代码省略**/

 

                //设置handler

               pass_two(op_array);

                /**代码省略**/

      }

       /**代码省略**/

      return op_array;

}

从上面的过程中可以看出,编译的主要过程是op_array的初始化,调用zend_compile_top_stmt遍历抽象语法树成opline,以及调用pass_two函数设置handler。下面我们一一阐述。

3.1 op_array初始化

在遍历抽象语法树之前,需要先初始化指令集op_array,用来存放指令。op_array的初始化工作,调用的函数为init_op_array,该函数会将op_array进行初始化,代码如下:

op_array = emalloc(sizeof(zend_op_array));

init_op_array(op_array, type, INITIAL_OP_ARRAY_SIZE);

void init_op_array(zend_op_array *op_array, zend_uchar type, int initial_ops_size)

{

      op_array->type = type;

      op_array->arg_flags[0] = 0;

      op_array->arg_flags[1] = 0;

      op_array->arg_flags[2] = 0;

      /**代码省略**/

}

CG(active_op_array) = op_array;

首先通过emalloc申请内存,大小为sizeof(zend_op_array)=208,然后初始化op_array的所有成员变量,把op_array赋值给CG(active_op_array)。

3.2 抽象语法树编译

抽象语法树的编译过程,是遍历抽象语法树,生成对应指令集的过程,编译是在 zend_compile_top_stmt() 中完成,这个函数是总入口,会被多次递归调用。其中传入的参数为CG(ast),这个AST是通过词法和语法分析得到的。下面我们看一下zend_compile_top_stmt的代码:

void zend_compile_top_stmt(zend_ast *ast) /* {{{ */

{

      if (!ast) {

               return;

      }

       //对于kind为ZEND_AST_STMT_LIST的节点,转换为zend_ast_list

      if (ast->kind == ZEND_AST_STMT_LIST) {

               zend_ast_list *list = zend_ast_get_list(ast);

               uint32_t i;

               //根据children的个数进行递归调用

               for (i = 0; i < list->children; ++i) {

                        zend_compile_top_stmt(list->child[i]);

               }

               return;

      }

       //其他kind的节点调用zend_compile_stmt

      zend_compile_stmt(ast);

 

      if (ast->kind != ZEND_AST_NAMESPACE && ast->kind != ZEND_AST_HALT_COMPILER) {

               zend_verify_namespace();

      }

      if (ast->kind == ZEND_AST_FUNC_DECL || ast->kind == ZEND_AST_CLASS) {

               CG(zend_lineno) = ((zend_ast_decl *) ast)->end_lineno;

               zend_do_early_binding();

      }

}

从代码中可以看到,对于zend_compile_top_stmt,会对AST节点的kind进行判断,然后走不同的逻辑,实际上是对AST的深度遍历,我们以下面的代码为例,看一下对AST的遍历过程。

<?php

$a = 1;

$b = $a + 2;

echo $b;

可以得到的AST如图11所示。

clipboard.png
图11 抽象语法树示意图

通过这课抽象语法树。可以很直观的看出,CG(ast)节点下面有三个子女:

  • 1)第一个子女,其kind是ZEND_AST_ASSIGN,有两个子女,分别是ZEND_AST_VAR和ZEND_AST_ZVAL,对应$a=1。
  • 2)第二个子女,其kind也是ZEND_AST_ASSIGN,有两个子女分别是ZEND_AST_VAR和ZEND_AST_BINARY_OP,其中ZEND_AST_BINARY_OP对应的是相加操作,对应的是$b=$a+2。
  • 3)第三个子女,其kind是ZEND_AST_STMT_LIST,有一个子女,为ZEND_AST_ECHO,对应的是echo $b。

下面我们看整棵树的遍历过程。

  • 1. Assign编译过程
  • 1)首先根节点kind为ZEND_AST_STMT,会调用函数zend_ast_get_list将其转换为zend_ast_list *,得到children的个数为2,接着递归调用zend_compile_top_stmt,这样就可以把抽象语法树根节点的最左子女遍历一遍,以便生成对应的指令;
  • 2)遍历第一个子女节点,对应的kind为ZEND_AST_ASSIGN,编译过程是调用函数zend_compile_stmt,继而调用zend_compile_expr函数,见代码:
void zend_compile_stmt(zend_ast *ast) /* {{{ */

{

      /*…代码省略…*/

      switch (ast->kind) {

               /*…代码省略…*/

                 default:

               {

                        znode result;

                        zend_compile_expr(&result, ast);

                        zend_do_free(&result);

               }

                  /*…代码省略…*/

}    

void zend_compile_expr(znode *result, zend_ast *ast) /* {{{ */

{

      /*…代码省略…*/

      switch (ast->kind) {

               /*…代码省略…*/

               case ZEND_AST_ASSIGN:

                        zend_compile_assign(result, ast);

                        return; 

         /*…代码省略…*/

}

最终调用的函数为zend_compile_assign,对ZEND_AST_ASSIGN节点进行编译:

void zend_compile_assign(znode *result, zend_ast *ast) /* {{{ */

{

      zend_ast *var_ast = ast->child[0];

      zend_ast *expr_ast = ast->child[1];

 

      znode var_node, expr_node;

      zend_op *opline;

      uint32_t offset;

       /*…代码省略…*/

      

      switch (var_ast->kind) {

               case ZEND_AST_VAR:

               case ZEND_AST_STATIC_PROP:

                        offset = zend_delayed_compile_begin();

                        zend_delayed_compile_var(&var_node, var_ast, BP_VAR_W);

                        zend_compile_expr(&expr_node, expr_ast);

                        zend_delayed_compile_end(offset);

                        zend_emit_op(result, ZEND_ASSIGN, &var_node, &expr_node);

                        return;

         /*…代码省略…*/

}

从代码中可以看出,kind为ZEND_AST_ASSIGN的抽象语法树有两个子女,左child为var_ast,右child为expr_ast,分别进行处理。

  • 3)调用zend_delayed_compile_begin:
static inline uint32_t zend_delayed_compile_begin(void) /* {{{ */

{

      return zend_stack_count (&CG(delayed_oplines_stack));

}

该函数会获取CG的delayed_oplines_stack栈顶的位置,其中delayed_oplines_stack是对于依赖后续编译动作存储信息的栈。等expr_ast编译后使用,调用zend_delayed_compile_end(offset)来获取栈里的信息。

  • 4)对于左子女var_ast调用zend_delayed_compile_var:
void zend_delayed_compile_var(znode *result, zend_ast *ast, uint32_t type) /* {{{ */

{

      zend_op *opline;

      switch (ast->kind) {

               case ZEND_AST_VAR:

                        zend_compile_simple_var(result, ast, type, 1);

   /**代码省略**/

}

其中kind为ZEND_AST_VAR,继而调用zend_compile_simple_var函数:

static void zend_compile_simple_var(znode *result, zend_ast *ast, uint32_t type, int delayed) /* {{{ */

{

      zend_op *opline;

 

      /*…代码省略…*/

      } else if (zend_try_compile_cv(result, ast) == FAILURE) {

               /*…代码省略…*/

      }

}

继而调用zend_try_compile_cv函数:

static int zend_try_compile_cv(znode *result, zend_ast *ast) /* {{{ */

{

      zend_ast *name_ast = ast->child[0];

      if (name_ast->kind == ZEND_AST_ZVAL) {

               /*…代码省略…*/

 

               result->op_type = IS_CV;

               result->u.op.var = lookup_cv(CG(active_op_array), name);

 

      /*…代码省略…*/

}

核心函数是lookup_cv,在这里面组装了操作数,见代码:

static int lookup_cv(zend_op_array *op_array, zend_string* name) /* {{{ */{

      int i = 0;

      zend_ulong hash_value = zend_string_hash_val(name);

       //判断变量是否在vars中存在,若存在直接返回对应的位置

      while (i < op_array->last_var) {

               if (ZSTR_VAL(op_array->vars[i]) == ZSTR_VAL(name) ||

                   (ZSTR_H(op_array->vars[i]) == hash_value &&

                    ZSTR_LEN(op_array->vars[i]) == ZSTR_LEN(name) &&

                    memcmp(ZSTR_VAL(op_array->vars[i]), ZSTR_VAL(name), ZSTR_LEN(name)) == 0)) {

                        zend_string_release(name);

                        return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);

               }

               i++;

      }

       //若不存在,则写入vars中,返回新插入的位置

      i = op_array->last_var;

      op_array->last_var++;

      /*…代码省略…*/

 

      op_array->vars[i] = zend_new_interned_string(name);

 

      return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);

}

从代码中可以看出,变量是存放到op_array->vars中的,而返回的是一个int型的地址,这个是什么呢?我们看一下宏ZEND_CALL_VAR_NUM的定义:

#define ZEND_CALL_VAR_NUM(call, n) \

      (((zval*)(call)) + (ZEND_CALL_FRAME_SLOT + ((int)(n))))

#define ZEND_CALL_FRAME_SLOT \

      ((int)((ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval)) - 1) / ZEND_MM_ALIGNED_SIZE(sizeof(zval))))

可以看出,这个值都是sizeof(zval)的整数倍,在笔者的机器上,zval的大小为16,而zend_execute_data大小为80,所以返回的是每个变量的偏移值,即80+16*i,如图12所示。

clipboard.png
图12 左子女var_ast编译示意图

此时,就将赋值语句$a=1中,左侧表达式$a编译完成,赋值给了znode* result,下面继续对右子女常量1的编译。

  • 5)对于右子女,调用函数zend_compile_expr进行编译,见代码:
void zend_compile_expr(znode *result, zend_ast *ast) /* {{{ */

{

    /* CG(zend_lineno) = ast->lineno; */

    CG(zend_lineno) = zend_ast_get_lineno(ast);

 

     switch (ast->kind) {

         case ZEND_AST_ZVAL:

            ZVAL_COPY(&result->u.constant, zend_ast_get_zval(ast));

            result->op_type = IS_CONST;

            return;

从代码中可以看出,对于常量1,通过ZVAL_COPY,将值复制到result->u.constan中,同时将result->op_type赋值为IS_CONST。这样,对于assign操作的两个操作数都编译完成了,下面我们看一下对应指令opline的生成过程。

  • 6)opline生成调用函数zend_emit_op,见代码:
static zend_op *zend_emit_op(znode *result, zend_uchar opcode, znode *op1, znode *op2) /* {{{ */

{

       //分配和获取opline,并设置其opcode

      zend_op *opline = get_next_op(CG(active_op_array));

      opline->opcode = opcode;

        //设置操作数1

      if (op1 == NULL) {

               SET_UNUSED(opline->op1);

      } else {

               SET_NODE(opline->op1, op1);

      }

        //设置操作数2

      if (op2 == NULL) {

               SET_UNUSED(opline->op2);

      } else {

               SET_NODE(opline->op2, op2);

      }

 

      zend_check_live_ranges(opline);

 

      if (result) {

                  //设置返回值

               zend_make_var_result(result, opline);

      }

      return opline;

}

其中对操作数得到设置,对应的是宏SET_NODE,见代码:

  #define SET_NODE(target, src) do { \

               target ## _type = (src)->op_type; \

               if ((src)->op_type == IS_CONST) { \

                        target.constant = zend_add_literal(CG(active_op_array), &(src)->u.constant); \

               } else { \

                        target = (src)->u.op; \

               } \

      } while (0)

 

int zend_add_literal(zend_op_array *op_array, zval *zv) /* {{{ */

{

      int i = op_array->last_literal;

      op_array->last_literal++;

      if (i >= CG(context).literals_size) {

               while (i >= CG(context).literals_size) {

                        CG(context).literals_size += 16; /* FIXME */

               }

               op_array->literals = (zval*)erealloc(op_array->literals, CG(context).literals_size * sizeof(zval));

      }

      zend_insert_literal(op_array, zv, i);

      return i;

}

从代码中可以看出,对于操作数1,会将编译过程中临时的结构znode传递给zend_op中,对于操作数2,因为是常量(IS_CONST),会调用zend_add_literal将其插入到op_array->literals中。

从返回值的设置,调用的是zend_make_var_result,其代码如下:

  static inline void zend_make_var_result(znode *result, zend_op *opline) /* {{{ */

{

        //返回值的类型设置为IS_VAR

      opline->result_type = IS_VAR;

        //这个是返回值的编号,对应T位置

      opline->result.var = get_temporary_variable(CG(active_op_array));

      GET_NODE(result, opline->result);

}

static uint32_t get_temporary_variable(zend_op_array *op_array) /* {{{ */

{

      return (uint32_t)op_array->T++;

}

返回值的类型为IS_VAR,result.var为T的值,下面我们给出Assign操作对应的指令图,如图13所示。

clipboard.png
图13 Assign指令示意图

从图13中可以看出,生成的opline中opcode等于38;op1的类型为IS_CV,op1.var对应的是vm_stack上的偏移量;op2的类型为IS_CONST,op2.constant对应的是op_array中literals数组的下标;result的类型为IS_VAR,result.var对应的是T的值;此时handler的值为空。

  • 2. Add编译过程

对于“$b =$a+2;”语句,首先是add语句,也就是$a+1,跟assign语句类型类似,不同是调用了函数zend_compile_binary_op,见代码:

void zend_compile_binary_op(znode *result, zend_ast *ast) /* {{{ */

{

      zend_ast *left_ast = ast->child[0];

      zend_ast *right_ast = ast->child[1];

      uint32_t opcode = ast->attr;//通过attr区分加减乘除等等操作

 

      znode left_node, right_node;

      zend_compile_expr(&left_node, left_ast);

      zend_compile_expr(&right_node, right_ast);

      /*…代码省略…*/

       zend_emit_op_tmp(result, opcode, &left_node, &right_node);

      /*…代码省略…*/

}

对于加减乘除等操作,kind都是ZEND_AST_BINARY_OP,具体操作通过AST中的attr区分的,因为$a+1会生成临时变量,因此与Assign操作不同,调用的函数是zend_emit_op_tmp:

 static zend_op *zend_emit_op_tmp(znode *result, zend_uchar opcode, znode *op1, znode *op2) /* {{{ */

{

      /*…代码与zend_emit_op一样…*/

      if (result) {

               zend_make_tmp_result(result, opline);

      }

 

      return opline;

}

zend_emit_op_tmp函数与zend_emit_op类似,opline中的操作数1和操作数2做了同样的操作,而result不同之处在于,其类型是IS_TMP_VAR,因此opline如图14所示。

clipboard.png
图14 Add指令示意图

对于“$b=$a+2;”相当于把临时变量赋值给$b,与Assign编译过程一致,生成opline如图15所示。

clipboard.png
图15 第2条Assign指令示意图

  • 3. Echo编译过程
    对于“echo $b;”,编译过程类似于Assign和Add的编译,不同处是调用函数zend_compile_echo。
void zend_compile_echo(zend_ast *ast) /* {{{ */

{

      zend_op *opline;

      zend_ast *expr_ast = ast->child[0];

 

      znode expr_node;

      zend_compile_expr(&expr_node, expr_ast);

 

      opline = zend_emit_op(NULL, ZEND_ECHO, &expr_node, NULL);

      opline->extended_value = 0;

}

Echo对应的指令只有一个操作数,对于操作数2,SET_UNUSED宏设置为IS_UNUSED。

#define SET_UNUSED(op)  op ## _type = IS_UNUSED

生成的opline如图16所示。

clipboard.png
图16 Echo指令示意图

  • 4. Return编译过程

上面对于AST编译并没有结束,PHP代码中虽然没有return操作,但是默认会生成一条ZEND_RETURN指令,通过zend_emit_final_return含设置,代码如下:

void zend_emit_final_return(int return_one) /* {{{ */

{

      znode zn;

      zend_op *ret;

      /**代码省略**/

 

      zn.op_type = IS_CONST;

      if (return_one) {

               ZVAL_LONG(&zn.u.constant, 1);

      } else {

               ZVAL_NULL(&zn.u.constant);

      }

 

      ret = zend_emit_op(NULL, returns_reference ? ZEND_RETURN_BY_REF : ZEND_RETURN, &zn, NULL);

      ret->extended_value = -1;

}

同样通过zend_emit_op设置opline,设置以后的opline如图17所示。

clipboard.png
图17 Return指令示意图

经过对Assign、Add和Echo的编译后,生成的全部oplines如图18所示。

clipboard.png
图18 所有指令集示意图

到这里,我们了解了AST编译生成opline指令集的过程,包括op1、op2和result的生成过程,但是此时opline中的handler还都是空指针,接下来我们看一下handler设置的过程。

3.3 设置指令handler

抽象语法树编译后还有一个重要操作,函数叫pass_two,这个函数中,对opline指令集做了进一步的加工,最主要的工作是设置指令的handler,代码如下:

ZEND_API int pass_two(zend_op_array *op_array)

{

     /**代码省略**/

   while (opline < end) {//遍历opline数组

      if (opline->op1_type == IS_CONST) {

               ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline->op1);

      } else if (opline->op1_type & (IS_VAR|IS_TMP_VAR)) {

       opline->op1.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->op1.var);

          }

     

      if (opline->op2_type == IS_CONST) {

          ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline->op2);

      } else if (opline->op2_type & (IS_VAR|IS_TMP_VAR)) {

          opline->op2.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->op2.var);

               }

    if (opline->result_type & (IS_VAR|IS_TMP_VAR)) {

       opline->result.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->result.var);

     }

     ZEND_VM_SET_OPCODE_HANDLER(opline);

     /**代码省略**/

}

从代码中可以看出,该函数会对opline指令数组进行遍历,对每一条opline指令进行操作,对于op1和op2如果是IS_CONST类型,调用ZEND_PASS_TWO_UPDATE_CONSTANT,见代码:

 /* convert constant from compile-time to run-time */

# define ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, node) do { \

               (node).constant *= sizeof(zval); \

      } while (0)

根据上一节的知识我们知道,对于IS_CONST类型的变量,其值是存在op_array->literals数组中,因此,可以直接使用数组下标乘以sizeof(zval)转换为偏移量。

对于op1和op2如果是IS_VAR或者IS_TMP_VAR类型的变量,跟上一节一样,通过ZEND_CALL_VAR_NUM计算偏移量。

另外一个非常重要的工作是通过ZEND_VM_SET_OPCODE_HANDLER(opline),设置opline对应的hanlder,代码如下:

ZEND_API void zend_vm_set_opcode_handler(zend_op* op)

{

      op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op);

}

其中opcode和handler之前的对应关系在Zend/zend_vm_execute.h中定义的。opline数组经过一次遍历后,handler也就设置完毕,设置后的opline数组如图19所示。

clipboard.png
图19 设置handler后的指令集

到此,整个抽象语法树就编译完成了,最终的结果为opline指令集,接下来就是在Zend虚拟机上执行这些指令。

4执行过程

执行的入口函数为zend_execute,在该函数中会针对上一节生成的opline指令集进行调度执行。首先会在EG(vm_stack)上分配空间,然后每一条指令依次压栈并调用对应的handler。代码如下:

ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value)

{

      zend_execute_data *execute_data;

      /**代码省略**/

      //压栈生成execute_data

      execute_data = zend_vm_stack_push_call_frame(ZEND_CALL_TOP_CODE | ZEND_CALL_HAS_SYMBOL_TABLE,

               (zend_function*)op_array, 0, zend_get_called_scope(EG(current_execute_data)), zend_get_this_object(EG(current_execute_data)));

       //设置symbol_table

      if (EG(current_execute_data)) {

               execute_data->symbol_table = zend_rebuild_symbol_table();

      } else {

               execute_data->symbol_table = &EG(symbol_table);

      }

      EX(prev_execute_data) = EG(current_execute_data);

       //初始化execute_data

      i_init_execute_data(execute_data, op_array, return_value);

       //执行

      zend_execute_ex(execute_data);

       //释放execute_data

      zend_vm_stack_free_call_frame(execute_data);

}

这个代码中首先根据op_array中的指令生成对应的execute_data,然后初始化后调用handler执行。下面我们具体分析一下执行的过程。

4.1 执行栈分配

执行栈是通过2.6节介绍的zend_vm_stack_push_call_frame完成的,会在EG(vm_stack)上分配一块内存区域,80个字节用来存放execute_data,紧接着下面是根据last_var和T的数量分配zval大小的空间,以3节编译生成的指令集为例,分配的栈如图20所示。

clipboard.png
图20 执行栈分配示意图

从图20中看出,在EG(vm_stack)上分配空间,空间的大小跟op_array中last_var和T的值相关。

4.2 初始化execute_data

在执行栈上分配空间后,会调用函数i_init_execute_data对执行数据进行初始化,见代码:

    static zend_always_inline void i_init_execute_data(zend_execute_data *execute_data, zend_op_array *op_array, zval *return_value) /* {{{ */

{

         ZEND_ASSERT(EX(func) == (zend_function*)op_array);

 

         EX(opline) = op_array->opcodes;//读取第一条指令

         EX(call) = NULL;

         EX(return_value) = return_value;//设置返回值

 

         if (EX_CALL_INFO() & ZEND_CALL_HAS_SYMBOL_TABLE) {

                    //赋值符号表

                  zend_attach_symbol_table(execute_data);

          /**代码省略**/

 

          //运行时缓存

          if (!op_array->run_time_cache) {

                  if (op_array->function_name) {

                           op_array->run_time_cache = zend_arena_alloc(&CG(arena), op_array->cache_size);

                  } else {

                           op_array->run_time_cache = emalloc(op_array->cache_size);

                  }

                  memset(op_array->run_time_cache, 0, op_array->cache_size);

         }

         EX_LOAD_RUN_TIME_CACHE(op_array);

         EX_LOAD_LITERALS(op_array);//设置常量数组

 

         EG(current_execute_data) = execute_data;

}

从代码中可以看出,初始化工作主要做了几件事:

  • 1)读取op_array中的第一条指令,赋值给EX(opline),其中EX宏是对execute_data的取值宏;
  • 2)设置EX的返回值;
  • 3)赋值符号表;
  • 4)设置运行时缓存;
  • 5)设置常量数组。

做完这些工作后,执行栈中数据的结果如图21所示。

clipboard.png
图21 初始化execute_data示意图

4.3 调用hanlder函数执行

接下来是调用execute_ex进行指令的执行,见代码:

ZEND_API void execute_ex(zend_execute_data *ex)

{

         ZEND_VM_LOOP_INTERRUPT_CHECK();

 

         while (1) { //循环

                  int ret;

                  if (UNEXPECTED((ret = ((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) != 0))

       {

                  if (EXPECTED(ret > 0)) {

                           execute_data = EG(current_execute_data);

                           ZEND_VM_LOOP_INTERRUPT_CHECK();

                  } else {

                           return;

                  }

}

}

从代码中可以看出,整个执行最外层是while循环,直到结束才退出。调用的是opline中对应的handler,下面以3节中生成的指令集进行详细的阐述。

  • 1)对于第一个指令,对应的handler为:
//ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER

//通过op2获取到常量数组里面的值

value = EX_CONSTANT(opline->op2);

//获取到op1对应的位置

variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var);

//将常量赋值给对应位置的指针

value = zend_assign_to_variable(variable_ptr, value, IS_CONST);

//将结果复制到result

ZVAL_COPY(EX_VAR(opline->result.var), value);

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

首先通过op2.constant值获取到常量表中的zval值,通过op1.var获取到栈中对应的位置,然后将常量值赋值到对应的位置,同时将其copy到result对应的位置,如图22所示。

clipboard.png
图22 Assign指令执行示意图

完成assign操作后,会调用ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION宏进行下一条指令的执行,也就是opline+1。

  • 2)第二条指令对应的是相加操作,其handler为:
//ZEND_ADD_SPEC_CV_CONST_HANDLER

zval *op1, *op2, *result;

//获取op1对应的位置

op1 = _get_zval_ptr_cv_undef(execute_data, opline->op1.var);

//获取op2对应的值

op2 = EX_CONSTANT(opline->op2);

/**代码省略**/

//执行相加操作,赋值给result

add_function(EX_VAR(opline->result.var), op1, op2);

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

首先根据op1.var获取对应的位置,然后根据op2.constant值获取到常量表中的zval值,最后进行相加操作,赋值给result对应的位置,如图23所示。

clipboard.png
图23 Add指令执行示意图

  • 3)第三条指令依然是assign,但是因为类型与第一条指令不同,因此对应的handler也不同:
//ZEND_ASSIGN_SPEC_CV_TMP_RETVAL_UNUSED_HANDLER

zval *value;

zval *variable_ptr;

//根据op2.var获取临时变量的位置

value = _get_zval_ptr_tmp(opline->op2.var, execute_data, &free_op2);

//根据op1.var获取操作数1 的位置

variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var);

//将临时变量赋值给操作数1对应的位置

value = zend_assign_to_variable(variable_ptr, value, IS_TMP_VAR);

//同时拷贝到result对应的位置

ZVAL_COPY(EX_VAR(opline->result.var), value);

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

与第一条指令类似,执行过程如图24所示。

clipboard.png
图24 第2条Assign指令示意图

  • 4)第四条指令是echo操作,对应的handler为:
// ZEND_ECHO_SPEC_CV_HANDLER

zval *z;

//根据op1.var获取对应位置的值

z = _get_zval_ptr_cv_undef(execute_data, opline->op1.var);

//调用zend_write输出

zend_write(ZSTR_VAL(str), ZSTR_LEN(str));

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

这条指令会根据op1.var获取到对应的位置,取出zval值输出,如图25所示。

clipboard.png
图25 Echo指令执行示意图

  • 5)第五条指令为return,对应的handler为:
//ZEND_RETURN_SPEC_CONST_HANDLER

zval *retval_ptr;

zval *return_value;

retval_ptr = EX_CONSTANT(opline->op1);

return_value = EX(return_value);

//调用zend_leave_helper_SPEC函数,返回

ZEND_VM_TAIL_CALL(zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));

这条指令没有做实质性的操作,核心是返回-1,让while循环退出,指令执行结束。

到此,整个的执行过程就阐述完成了,相信读者通过这五条指令的执行,初步理解了Zend虚拟机的执行过程。

4.4 释放execute_data

指令执行完毕后,调用zend_vm_stack_free_call_frame释放execute_data,并回收EG(vm_stack)上使用的空间,这部分比较简单。

5小结

本文主要介绍了Zend虚拟机的实现原理,包括抽象语法树编译生成指令集的过程,以及指令集执行的过程。同时介绍了Zend虚拟机运行中用到的数据结构。希望读者读完本文,能够对Zend虚拟机有一定的认识。


AI及LNMPRG研究
7.2k 声望12.8k 粉丝

一群热爱代码的人 研究Nginx PHP Redis Memcache Beanstalk 等源码 以及一群热爱前端的人