共计 5328 个字符,预计需要花费 14 分钟才能阅读完成。
grape
全部视频:https://segmentfault.com/a/11…
原视频地址:http://replay.xesv5.com/ll/24…
流程回顾
上节课我们把 $a= 1 这个过程编译梳理了一遍,我们了解到 op1,op2,result,opcode 的生成过程,下面我们把整个过程来回顾一下。
static zend_op_array *zend_compile(int type)
{
zend_op_array *op_array = NULL;
zend_bool original_in_compilation = CG(in_compilation);
CG(in_compilation) = 1;
CG(ast) = NULL;
CG(ast_arena) = zend_arena_create(1024 * 32); // 首先会分配内存
if (!zendparse()) {//zendparse(就是 yyparse)(zend_language_parse.y) ==> 通过 parser 调用 lexer,生成抽象语法树 ast_list,存到 CG(ast);yyparse 是通过 bison 编译 zend_language_parser.y 生成
int last_lineno = CG(zend_lineno);
zend_file_context original_file_context;
zend_oparray_context original_oparray_context;
zend_op_array *original_active_op_array = CG(active_op_array);
op_array = emalloc(sizeof(zend_op_array));
init_op_array(op_array, type, INITIAL_OP_ARRAY_SIZE); // 初始化 oparray
CG(active_op_array) = op_array;
if (zend_ast_process) {zend_ast_process(CG(ast));
}
zend_file_context_begin(&original_file_context);
zend_oparray_context_begin(&original_oparray_context);
zend_compile_top_stmt(CG(ast)); // 编译 ast 生成 oparray
CG(zend_lineno) = last_lineno;
zend_emit_final_return(type == ZEND_USER_FUNCTION); //PHP 中会加 return 1,在此进行处理
op_array->line_start = 1;
op_array->line_end = last_lineno;
pass_two(op_array); // 对于 handler 的处理
zend_oparray_context_end(&original_oparray_context);
zend_file_context_end(&original_file_context);
CG(active_op_array) = original_active_op_array;
}
zend_ast_destroy(CG(ast));
zend_arena_destroy(CG(ast_arena));
CG(in_compilation) = original_in_compilation;
return op_array;
}
大体流程为:词法分析 -> 语法分析 -> 编译 ast 生成 op_array-> 处理 return 1-> 对于 handler 做处理
以上处理 return 1 环节之前的文章中我们都已经提到过,如果有不太理解的请翻阅之前的文章。接下来我们 gdb 程序到环节 return 1。代码:
<?php
$a = 2;
$b = 3;
我们来看一看到编译 ast 生成 op_array 处的结果:
我们来看这个结果,vars 是存我们的变量的,在这存的是 a 和 b,并且 last_Var= 2 只有两个;T 是 temporary,T= 2 说明有两个临时变量。然后 literals 是存我们的字面量,再这里存的是 2,3,last_literal= 2 表示现在有两个字面量,接下来我们打印一下看是否和我们所解释的一致。
结果和我们设想的一致。另外,对于 opcode 的值又是如何呢?
我们发现,$a=2 op1 是 80,$b=3 op1 为 96,这是为什么呢?这之前我们说过这个问题,因为在栈中我们是分配一个大小为 16 的内存,所以需要增加 16. 第二个,我们知道 result.constant 的 0 和 1 代表字面量偏移量分别为 0 和 1.
到这里都是之前学习过的内容,接下来继续学习。
return 1 的做了什么?
继续执行代码:
我们发现在执行完 zend_emit_final_return 这句之后我们的 op_array 发生了变化。那么为什么会发生这样的变化呢?我们在文章开头有些到这个函数的作用是增加 return 1 结尾,那么具体其中是怎么来操作呢?我们来看代码:
void zend_emit_final_return(int return_one) /* {{{ */
{
znode zn;
zend_op *ret;
zend_bool returns_reference = (CG(active_op_array)->fn_flags & ZEND_ACC_RETURN_REFERENCE) != 0;
if (CG(active_op_array)->fn_flags & ZEND_ACC_HAS_RETURN_TYPE
&& !(CG(active_op_array)->fn_flags & ZEND_ACC_GENERATOR)) {zend_emit_return_type_check(NULL, CG(active_op_array)->arg_info - 1, 1);
}
zn.op_type = IS_CONST;
if (return_one) {ZVAL_LONG(&zn.u.constant, 1); // 在 gdb 过程中会走到这一步,把 1 赋值给 zn.u.constant
} else {ZVAL_NULL(&zn.u.constant);
}
ret = zend_emit_op(NULL, returns_reference ? ZEND_RETURN_BY_REF : ZEND_RETURN, &zn, NULL);// 在此会像字面量中添加一个新的元素 1
ret->extended_value = -1;
}
static zend_op *zend_emit_op(znode *result, zend_uchar opcode, znode *op1, znode *op2) /* {{{ */
{zend_op *opline = get_next_op(CG(active_op_array));
opline->opcode = opcode;
if (op1 == NULL) {SET_UNUSED(opline->op1);
} else {SET_NODE(opline->op1, op1);
}
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;
}
#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)
我们发现,gdb 过程在这个函数中像 literals 里边又新增 1 个元素,我们打印 opcodes:
我们发现,新增了一条指令,在代码中就是 return 1。
好的,到此,我们发现,有三条指令,两个变量,三个字面量。$a 和 $b 的位置已经有了,字面量也有了,我们发现 handler 还是个空指针,接下来我们看 handler 的生成。
pass_two 设置 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,我们拿 IS_CONST 来举例,如果 op1,op2 的 type 为 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).zv = CT_CONSTANT_EX(op_array, (node).constant); \
} while (0)
# define CT_CONSTANT_EX(op_array, num) \
((op_array)->literals + (num))
我们知道,对于 is_constr 的变量的字面量是存在与 Literals 里边的,而 constant 是相对的下标,因此我们可以通过对于首地址偏移 constant 来进行转换为真实的偏移量。对于 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);
}
static const void *zend_vm_get_opcode_handler(zend_uchar opcode, const zend_op* op)
{return zend_vm_get_opcode_handler_ex(zend_spec_handlers[opcode], op);
}
其中 opcode 和 handler 之前的对应关系在 Zend/zend_vm_execute.h 中定义的。opline 数组经过一次遍历后,handler 也就设置完毕,设置后的 opline 数组如图所示:
结尾
最后我们打印下生成 handler 后的 op_array:
我们发现,handler 已经被赋值。
至此,整个抽象语法树就编译完成了,最终的结果为 opline 指令集,接下来就是在 Zend 虚拟机上执行这些指令。
参考资料:
【PHP7 源码分析】PHP7 源码研究之浅谈 Zend 虚拟机