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
。下面我们一一阐述。
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)
。
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所示。

可以很直观地看出,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
对应的是相加操作,对应的是$b=$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所示。Figure 2. 图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.constant
中,同时将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所示。Figure 3. 图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编译过程
对于 “$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
中的操作数 op1
和 op2
做了同样的操作,而 result
的不同之处在于,其类型是 IS_TMP_VAR
,因此 Add
指令示意图如图11-14所示。

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

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
Echo
指令示意图如图11-16所示。

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所示。

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

到这里,我们了解了 AST
编译生成 opline
指令集的过程,包括 op1
、op2
和 result
的生成过程,但是此时 opline
中的 handler
还是空指针,接下来我们看一下 handler
的设置过程。
设置指令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
虚拟机上执行这些指令。