编程语言的虚拟机是一种可以运行中间语言的程序。中间语言是抽象出的指令集,由原生语言编译而成,作为虚拟机执行阶段的输入。很多语言都实现了自己的虚拟机,如Java、C#和Lua。PHP语言也有自己的虚拟机,称为Zend虚拟机。
在第7章中,我们了解到PHP 7完成基本的准备工作后,会启动Zend引擎,加载注册的扩展模块,然后读取对应的脚本文件,Zend引擎会对文件进行词法和语法分析,生成AST,接着AST被编译成opcode,如果开启了opcache,编译的环节会被跳过从opcache中直接读取opcode进行执行。
在PHP 7中,进行词法和语法分析,生成AST,然后编译成opcode及被执行均由Zend虚拟机完成。第10章中,我们已经具体介绍了AST的生成过程,本章将详细阐述AST编译成opcode的过程、opcode被执行的过程,以及Zend虚拟机的实现原理及关键数据结构。
11.1 基础知识
Zend虚拟机(称为Zend VM)是PHP语言的核心,承担了语法和词法分析、AST编译以及指令的执行工作,下面我们讨论一下Zend虚拟机的基础架构以及相关的基础知识。
11.1.1 Zend虚拟机架构
Zend虚拟机主要分为解释层、中间数据层和执行层,如图11-1所示。
图11-1 Zend虚拟机架构图
-
解释层
这一层主要负责对PHP代码进行词法和语法分析,生成对应的AST;另一个工作就是对AST进行编译,生成符号表和指令集。
-
中间数据层
这一层主要包含了虚拟机的核心部分——执行栈的维护、指令集和符号表的存储,而这些是执行引擎调度执行的基础。
-
执行层
这一层是执行指令集的引擎,负责最终的执行并生成结果,这一层实现了大量的底层函数。
为了更好地理解Zend虚拟机各层的工作,我们先了解一下物理机的一些基础知识,读者可以对照理解虚拟机的原理。
11.1.2 符号表
是编译程序在编译过程中用来记录源程序中各种名字的特性信息,所以也称为名字特性表。名字一般包含程序名、过程名、函数名、用户定义类型名、变量名、常量名、枚举值名、标号名等。特性信息指的是名字的种类、类型、维数、参数个数、数值及目标地址(存储单元地址)等。
符号表有什么作用呢?一是协助进行语义检查,比如检查一个名字的引用和之前的声明是否相符;二是协助中间代码生成,最重要的是在目标代码生成阶段,当需要为名字分配地址时,符号表中的信息是地址分配的主要依据。
图11-2 符号表创建示例
符号表一般有3种构造和处理方法,分别是线性查找、二叉树和Hash技术。其中,线性查找法是最简单的,按照符号出现的顺序填表,每次查找从第一个位置开始顺序查找,效率比较低;二叉树实现了对半查找,在一定程度上提高了效率;效率最高的方法是通过Hash技术实现符号表,通过对第5章的学习,详细大家对Hash技术有一定的了解,而PHP 7中的符号表就是使用HashTable实现的。
11.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被压入栈中。函数调用压栈过程示意图如图11-3所示。
图11-3 函数调用压栈过程示意图
执行funcB函数,对argB1和argB2进行相加操作,执行后得到返回值为7,然后funcB的栈帧出栈,funcA中的临时变量TempB被赋值为7,继而进行相加操作,得到结果为10,然后funcA出栈,main函数中的临时变量TempA被赋值为10,最终main函数返回并出栈,整个函数调用结束。函数调用出栈过程示意图如图11-4所示。
图11-4 函数调用出栈过程示意图
11.1.4 指令
汇编语句中的指令语句格式一般如下:
[标号:] [前缀] 指令助记符 [操作数] [;注释]
其中:
- 标号字段由各种有效字符组成,一般表示符号地址,具有段基址、偏移量、类型3种属性。通常情况下,这部分是可选部分,主要为便于程序的读写而使用。
- 指令助记符规定指令或伪指令的操作功能,是语句中唯一不可缺少的部分。对于指令,汇编程序会将其翻译成机器语言指令:
MOV AX, 100 → B8 00 01 - 操作数指明指令语句中提供给指令的操作对象、存放位置。操作数可以是1个、2个或0个,2个操作数之间用逗号“, ”分开。比如“RET; ”对应的操作数个数是0个,“INC BX; ”对应的操作数个数是1,“MOV AX,DATA; ”对应的操作数个数是2个。
- 注释以“ ; ”开始,用于对程序进行解释和说明。
符号表、函数调用栈以及指令构成了物理机执行的基本元素,Zend虚拟机也同样实现了符号表、函数调用栈及指令,以运行PHP代码。下面我们先讨论一下Zend虚拟机相关的数据结构。
11.2 相关数据结构
Zend虚拟机包含词法和语法分析、AST的编译,以及opcode的执行,本章主要详细介绍AST和opcode的执行过程,下面先介绍一下用到的基本数据结构,为后面原理性内容的介绍奠定基础。
11.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的结构图,然后分别阐述其含义,如图11-5所示。
图11-5 EG(v)结构图
这个结构体比较复杂,下面介绍几个核心的成员。
- symbol_table:符号表,主要存放全局变量,以及一些魔术变量,如
_POST等。
- function_table:函数表,主要存放函数,包括大量的内部函数以及用户自定义的函数,如zend_version、func_num_args、str系列函数等。
- class_table:类表,主要存放内置的类以及用户自定义的类,如stdclass、throwable、exception等类。
- zend_constants:常量表,存放PHP中的常量,如E_ERROR、E_WARNING等。
- vm_stack:虚拟机的栈,执行时压栈、出栈都在这里操作。
- current_execute_data:对应_zend_execute_data结构体,存放执行时的数据。
11.2.2 符号表
在PHP 7中,符号表分为symbol_table、function_table和class_table等。
- symbol_tablesymbol_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如图11-6所示。
图11-6 symbol_table示意图
从图11-6可以看出,符号表中有我们常见的超全局变量_POST等,还有全局变量$a。在编译过程中会调用zend_attach_symbol_table函数将变量加入symbol_table中。
- 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等。
- class_table
class_table对应的是类表,其也是HashTable:
HashTable *class_table; /* class table */
类表里面也有大量的内置类,如stdclass、traversable、xmlreader等。
符号表存放了执行时需要的数据,比如在symbol_table中,key为_GET的Bucket对应的也是一个HashTable类型的表,里面存放的是$_GET[xxx],执行时会从中取对应的值。
11.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; //返回值的类型
};
zend_op结构图如图11-7所示。
图11-7 zend_op结构图
PHP代码会被编译成一条一条的opline,分解为最基本的操作。举个例子,如果把opcode当成一个计算器,只接受两个操作数op1和op2,执行一个操作handler,比如加、减、乘、除,然后它返回一个结果result,再稍加处理算术溢出的情况,存于extended_value中。下面详细介绍下各个字段。
- 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
- 操作数
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脚本中出现。
- 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的。
- extended_value
extended_value是存的扩展信息,opcode和CPU的指令类似,有一个标示指令字段handler,以及这个opcode所操作的操作数op1和op2,但PHP不像汇编那么底层,在脚本实际执行的时候可能还需要其他更多的信息,extended_value字段就保存了这类信息。
- lineno
lineno对应源代码文件中的行号。
到这里,相信读者对指令opline有了比较深刻的认识,在Zend虚拟机执行时,这些指令被组装在一起,成为指令集,下面我们介绍一下指令集。
11.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];
};
其结构如图11-8所示。
图11-8 zend_op_array结构
这个结构体中有几个关键变量。
- last和opcodes是存放opline的数组,也就是指令集存放的位置,其中last为数组中opline的个数。
- 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);
}
- T为IS_VAR和IS_TMP_VAR类型变量的总数,编译时遇到这种类型,T就会加1,用于后续在执行栈上分配空间。
- static_variables是用于存放静态变量的HashTable。
- literals是用于存放常量(IS_CONST)类型的数组,last_literal为常量的个数。
- run_time_cache用于运行时缓存的操作,本书不展开讨论。
11.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; /* 缓存常量 */
};
下面我们介绍一下各字段。
- opline:对应的是zend_op_array中opcodes数组里面的zend_op,表示正在执行的opline。
- prev_execute_data: op_array上下文切换的时候,这个字段用来保存切换前的op_array,此字段非常重要,能将每个op_array的execute_data按照调用的先后顺序连接成一个单链表,每当一个op_array执行结束要还原到调用前op_array的时候,就通过当前的execute_data中的prev_execute_data字段来得到调用前的执行器数据。
- symbol_table:当前使用的符号表,一般会取EG(symbol_table)。
- literals:常量数组,用来缓存常量。
zend_execute_data是在执行栈上运行的关键数据,可以用EX宏来获取其中的值,代码如下:
#define EX(element) ((execute_data)->element)
11.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;
可以看出,栈的结构比较简单,有3个变量,其中top指向栈顶,end指向栈底,pre是指向上一个栈的指针,也就意味着所有栈在一个单向链表上。
在执行PHP代码时,参数的压栈操作以及出栈调用执行函数都是在栈上进行的,下面介绍栈操作的核心函数。
- 初始化
初始化调用的函数为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)申请内存,申请的大小为16× 1024× zeof(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指向申请内存的最尾部,prev指向null,如图11-9所示。
图11-9 zend_vm_stack初始化后示意图
可以看出,多个vm_stack构成单链表,将多个栈连接起来,栈初始为16×1024个zval的大小,栈顶部占用了一个*zval指针的大小,以及一个结构体_zend_vm_stack的大小。
- 入栈操作
调用的函数为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),如图11-10所示。
图11-10 压栈过程
至此,我们了解了Zend虚拟机中符号表、指令集、执行数据以及执行栈相关的数据结构,下面我们基于这些基本知识来介绍一下指令集生成的过程。
11.3 AST编译过程
AST(抽象语法树)的编译是生成指令集opcode的过程,词法和语法分析后生成的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遍历AST成opline,以及调用pass_two函数设置handler。下面我们一一阐述。
11.3.1 op_array初始化
在遍历AST之前,需要先初始化指令集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)。
11.3.2 AST编译
AST的编译过程是遍历AST生成对应指令集的过程,编译在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;
根据第10章的知识,可以得到的AST如图11-11所示。
图11-11 AST示意图
可以很直观地看出,CG(ast)节点下面有3个子女。
- 第一个子女,其kind是ZEND_AST_ASSIGN,有两个子女,分别是ZEND_AST_VAR和ZEND_AST_ZVAL,对应$a=1。
- 第二个子女,其kind也是ZEND_AST_ASSIGN,有两个子女,分别是ZEND_AST_VAR和ZEND_AST_BINARY_OP,其中ZEND_AST_BINARY_OP对应的是相加操作,对应的是
a+2。
- 第三个子女,其kind是ZEND_AST_STMT_LIST,有一个子女,为ZEND_AST_ECHO,对应的是echo $b。
下面我们来看整棵AST的遍历过程。
-
Assign编译过程
-
根节点kind为ZEND_AST_STMT,会调用函数zend_ast_get_list将其转换为zend_ast_list *,得到children的个数为2,接着递归调用zend_compile_top_stmt,这样就可以把AST根节点的最左子女遍历一遍,以便生成对应的指令。
-
遍历第一个子女节点,对应的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的AST有两个子女,左child为var_ast,右child为expr_ast,分别进行处理。
-
调用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)来获取栈里的信息。
-
对于左子女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+16i,如图11-12所示。
图11-12 左子女var_ast编译示意图此时,对于赋值语句$a=1,左侧表达式$a编译完成,赋值给了znode *result,下面继续对右子女常量1进行编译。
-
对于右子女,调用函数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的生成过程。
-
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操作对应的指令示意图,如图11-13所示。
图11-13 Assign指令示意图从图11-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的值为空。
-
-
Add编译过程
对于“
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中的操作数op1和op2做了同样的操作,而result的不同之处在于,其类型是IS_TMP_VAR,因此Add指令示意图如图11-14所示。
图11-14 Add指令示意图对于“
a+2; ”,相当于把临时变量赋值给$b,与Assign编译过程一致,如图11-15所示。
图11-15 第二条Assign指令示意图 -
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_UNUSEDEcho指令示意图如图11-16所示。
图11-16 Echo指令示意图 -
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。Return指令示意图如图11-17所示。
图11-17 Return指令示意图经过对Assign、Add和Echo编译后,生成的全部oplines如图11-18所示。
图11-18 所有指令集示意图到这里,我们了解了AST编译生成opline指令集的过程,包括op1、op2和result的生成过程,但是此时opline中的handler还是空指针,接下来我们看一下handler的设置过程。
11.3.3 设置指令handler
AST编译后还有一个重要操作,即由函数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数组如图11-19所示。
到此,整个AST就编译完成了,最终的结果为opline指令集,接下来在Zend虚拟机上执行这些指令。
11.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执行。下面我们具体分析一下执行的过程。
11.4.1 执行栈分配
执行栈是通过11.2.6节介绍的zend_vm_stack_push_call_frame完成的,会在EG(vm_stack)上分配一块内存区域,80字节用来存放execute_data,紧接着下面是根据last_var和T的数量分配zval大小的空间,以11.3节编译生成的指令集为例,分配的栈如图11-20所示。
图11-20 执行栈分配示意图
在EG(vm_stack)上分配的空间大小跟op_array中last_var和T的值相关。
11.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;
}
从代码中可以看出,初始化工作主要做了几件事:
- 读取op_array中的第一条指令,赋值给EX(opline),其中EX宏是对execute_data的取值宏;
- 设置EX的返回值;
- 赋值符号表;
- 设置运行时缓存;
- 设置常量数组。
做完这些工作后,执行栈中数据的结果如图11-21所示。
图11-21 初始化execute_data示意图
11.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,下面以11.3节中生成的指令集为例进行详细阐述。
-
对于第一条指令——Assign指令,对应的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获取到栈中对应的位置,然后将常量值赋值到对应的位置,同时将其复制到result对应的位置,如图11-22所示。
图11-22 Assign指令执行示意图完成Assign操作后,会调用ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION宏执行下一条指令,也就是opline+1。
-
第二条指令对应的是相加操作,其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对应的位置,如图11-23所示。
图11-23 Add指令执行示意图 -
第三条指令依然是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();与第一条指令类似,执行过程如图11-24所示。
图11-24 第二条Assign指令示意图 -
第四条指令是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值输出,如图11-25所示。
图11-25 Echo指令执行示意图 -
第五条指令为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循环退出,指令执行结束。
到此,整个执行过程就介绍完了,相信读者通过这5条指令的执行,初步理解了Zend虚拟机的执行过程。
11.4.4 释放execute_data
指令执行完毕后,调用zend_vm_stack_free_call_frame释放execute_data,并回收EG(vm_stack)使用的空间,这部分比较简单。
到此,我们详细阐述了AST被编译成指令集,以及指令集被执行的过程,相信读者对Zend虚拟机有了一定了解。
11.5 其他调度方式
Zend虚拟机默认使用call方式进行调度,另外还支持goto、switch方式,可以在编译PHP时增加参数:
./configure --with-zend-vm=goto
感兴趣的读者可以按照本章分析的方式自行分析调度的方式,相信会有更深刻的理解。
11.6 本章小结
本章主要介绍了Zend虚拟机的实现原理,包括AST编译生成指令集的过程,以及指令集执行的过程;同时介绍了Zend虚拟机运行中用到的数据结构。希望读者学完本章,能够对Zend虚拟机有一定的认识。