上篇文章,我们讲解了如何从yaphp
源代码生成AST
。这篇文章,我们来讲解如何从AST
生成opcode
。
一句话总结起来就是,我们对AST
进行深度遍历,然后以每一个子AST
来生成一条opline
,最终得到op_array
。我们来看一下具体如何去实现它。
首先,我们需要定义一下和op_array
相关的数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| #define IS_UNUSED 0 #define IS_CONST (1 << 0) #define IS_TMP_VAR (1 << 1) #define IS_VAR (1 << 2) #define IS_CV (1 << 3)
#define INITIAL_OP_ARRAY_SIZE 64
typedef struct _zend_op_array zend_op_array; typedef struct _zend_oparray_context zend_oparray_context; typedef struct _zend_op zend_op; typedef union _znode_op znode_op; typedef struct _znode znode;
union _znode_op { uint32_t num; uint32_t var; };
struct _znode { char op_type; znode_op op; };
struct _zend_op { znode_op op1; znode_op op2; znode_op result; unsigned char opcode; char op1_type; char op2_type; char result_type; };
struct _zend_op_array { zend_op *opcodes; uint32_t last; uint32_t T; };
struct _zend_oparray_context { uint32_t opcodes_size; };
|
其中,_zend_op_array
是核心结构,其他的结构都是以它为中心展开的。这些结构的具体含义我们可以在很多PHP
源码分析的文章里面找到,所以我们不过多介绍。
这里,我们来看看_zend_op
结构,我们发现,这个结构本质上是一种三地址码格式。有一个指令类型的opcode
,有两个操作数 op1
和op2
和一个结果result
。
现在,让我们来实现一下对AST
的处理流程,在文件zend_language_parser.y
里面:
1 2 3 4 5 6
| op_array = (zend_op_array *) malloc(sizeof(zend_op_array)); init_op_array(op_array, INITIAL_OP_ARRAY_SIZE); CG(active_op_array) = op_array;
zend_oparray_context_begin(); zend_compile_top_stmt(CG(ast));
|
这就是我们的核心了,这里的步骤可以总结为:
1 2 3
| 1. 初始化op_array,并且赋值给CG(active_op_array) 2. 调用zend_oparray_context_begin初始化CG(context) 3. 调用zend_compile_top_stmt生成opcode
|
OK
,以这个为思路,我们来看看具体如何实现的。
首先是函数init_op_array
:
1 2 3 4 5
| void init_op_array(zend_op_array *op_array, int initial_ops_size) { op_array->opcodes = (zend_op *) malloc(initial_ops_size * sizeof(zend_op)); op_array->last = 0; op_array->T = 0; }
|
这个函数非常的简单,首先为op_array
里面保存的opcodes
分配初始化的内存;然后设置last
(即opline
的条数)为0
;然后设置T
(即临时变量的个数为0
,后面,我们的临时变量的名字是按照T
来递增命名的,例如第一个临时变量叫做1
,第二个临时变量叫做2
,依次类推)为0。
然后是zend_oparray_context_begin
:
1 2 3
| void zend_oparray_context_begin() { CG(context).opcodes_size = INITIAL_OP_ARRAY_SIZE; }
|
这个CG(context)
会与我们的CG(active_op_array)
挂钩,例如这里的CG(context).opcodes_size
表示,我们的CG(active_op_array)
总容量。可想而知,当我们编译的opline
条数达到CG(context).opcodes_size
的时候,需要进行CG(active_op_array)
的扩容。
最后就是我们的核心函数zend_compile_top_stmt
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void zend_compile_top_stmt(zend_ast *ast) { if (!ast) { return; }
if (ast->kind == ZEND_AST_STMT_LIST) { zend_ast_list *list = zend_ast_get_list(ast); for (uint32_t i = 0; i < list->children; ++i) { zend_compile_top_stmt(list->child[i]); } return; }
zend_compile_stmt(ast); }
|
这个函数是用来编译我们的ZEND_AST_STMT_LIST
的。然后,每一条语句,我们会调用zend_compile_stmt
来进行编译。
我们来看看zend_compile_stmt
:
1 2 3 4 5 6 7 8 9 10 11 12 13
| void zend_compile_stmt(zend_ast *ast) { if (!ast) { return; }
switch (ast->kind) { case ZEND_AST_ECHO: zend_compile_echo(ast); break; default: break; } }
|
因为我们目前只实现了echo
语句,所以,我们这里只有一个ZEND_AST_ECHO
的类型。我们来看看我们是如何编译echo
语句的:
1 2 3 4 5 6 7 8
| void zend_compile_echo(zend_ast *ast) { zend_ast *expr_ast = ast->child[0];
znode expr_node; zend_compile_expr(&expr_node, expr_ast);
zend_emit_op(ZEND_ECHO, &expr_node, nullptr); }
|
这个函数做了两件事情,第一件事情是编译echo
节点使用的expr ast
,第二件事情为echo ast
(也就是我们的echo
语句)生成一条opline
。
我们来看看我们是如何编译expr_ast
的:
1 2 3 4 5 6 7 8 9 10 11 12 13
| void zend_compile_expr(znode *result, zend_ast *ast) { switch (ast->kind) { case ZEND_AST_LNUM: result->op.num = zend_ast_get_lnum(ast)->lnum; result->op_type = IS_CONST; break; case ZEND_AST_BINARY_OP: zend_compile_binary_op(result, ast); return; default: break; } }
|
可以看到,如果是一个ZEND_AST_LNUM
类型的节点(也就是一个数字),那么我们直接返回它作为编译ast
后的结果;如果是一个ZEND_AST_BINARY_OP
类型的节点(也就是操作符),那么我们需要继续编译。
1 2 3 4 5 6 7 8 9 10 11 12
| 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;
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); }
|
可以看到,对ZEND_AST_BINARY_OP
类型的编译实际上就是一个递归的函数。我们发现,只有在AST
的子节点都是终结符的时候,我们才会调用zend_emit_op_tmp
生成一条opline
。我们看看zend_emit_op_tmp
的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
static zend_op *zend_emit_op(unsigned char opcode, znode *op1, znode *op2) { zend_op *opline = get_next_op(); opline->opcode = opcode;
if (op1 != nullptr) { opline->op1_type = op1->op_type; opline->op1 = op1->op; }
if (op2 != nullptr) { opline->op2_type = op2->op_type; opline->op2 = op2->op; }
return opline; }
static inline uint32_t get_temporary_variable(void) { return ++CG(active_op_array)->T; }
static zend_op *zend_emit_op_tmp(znode *result, unsigned char opcode, znode *op1, znode *op2) { zend_op *opline = zend_emit_op(opcode, op1, op2);
if (result) { zend_make_tmp_result(result, opline); }
return opline; }
|
这几个函数就非常的简单了,设置opline
的op1
和op2
,然后opline
的result
我们都用一个临时变量。因为我们的_zend_op
是一个三地址码,所以,我们一条表达式里面,如果出现了多个操作符和多个操作数,那么我们就需要拆解出来,因此,我们就需要临时变量来作为中间结果了。例如1 + 2 + 3
对应的过程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| +----------------------+ | 1 + 2 + 3 | +----------------------+ | | v +----------------------+ | T1 = 1 + 2 | +----------------------+ | | | v +----------------------+ | T2 = T1 + 3 | +----------------------+
|
这样,我们就实现了AST
到opcode
的转化,我们来编写一个测试脚本:
1 2
| echo 1 + 2 * 3; echo 1 + 2 + 3;
|
执行结果如下:
1 2 3 4 5 6 7
| *********************gennerate opcode*********************
|
可以看到,是符合我们的预期的。