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 虚拟机上执行这些指令。