前言 读了读编译原理,用一句简单的 PHP 代码来理一遍 PHP 的编译执行过程。
简单的测试脚本
总览 PHP 的编译执行大概是这么一个流程:
读入代码字符串,词法分析得到 token 和 value。
语法分析 token,建立 AST 树。
遍历分析 AST 树,将数据写入内存,并生成 op_array。
遍历 op_array,根据 opcode 调用不同的 handler 执行操作。
入口 直接从 zend.c 的 zend_execute_scripts 函数开始看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ZEND_API int zend_execute_scripts (int type, zval *retval, int file_count, ...) { ... for (i = 0 ; i < file_count; i++) { file_handle = va_arg(files, zend_file_handle *); if (!file_handle) { continue ; } op_array = zend_compile_file(file_handle, type); ... if (op_array) { zend_execute(op_array, retval); zend_exception_restore(); ... destroy_op_array(op_array); efree_size(op_array, sizeof (zend_op_array)); } ... } ... }
一般来说这里会编译执行 3 个文件,分别是 auto_prepend_file、要执行的 PHP 文件 和 auto_append_file。
这里的 zend_compile_file 是个全局函数,指向的是 compile_file 函数:
1 zend_compile_file = compile_file;
compile_file 先调用 open_file_for_scanning 做好词法分析前的准备(比如设置词法分析的 state 为 INITIAL),再调用 zend_compile 开始编译:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ZEND_API zend_op_array *compile_file (zend_file_handle *file_handle, int type) { zend_lex_state original_lex_state; zend_op_array *op_array = NULL ; zend_save_lexical_state(&original_lex_state); if (open_file_for_scanning(file_handle)==FAILURE) { ... } else { op_array = zend_compile(ZEND_USER_FUNCTION); } zend_restore_lexical_state(&original_lex_state); return op_array; }
zend_compile 函数里面就是总览里面提到的 1-3 的处理过程了,首先是调用 zendparse 开始词法分析。
词法分析 PHP 的编译采用的是 re2c 词法分析 + Bison 语法分析。在默认情况下,调用 yyparse 开始语法分析,而 yyparse 则不断调用 yylex 从代码字符串中获取 token 和 value 再加以分析,而在 PHP 中这两个函数则分别被替换成了 zendparse 和 zendlex。所以在调用 zendparse 开始语法分析之后,Bison 会调用 zendlex 开始词法分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int ZEND_FASTCALL zendlex (zend_parser_stack_elem *elem) { zval zv; int ret; if (CG(increment_lineno)) { CG(zend_lineno)++; CG(increment_lineno) = 0 ; } ret = lex_scan(&zv, elem); ZEND_ASSERT(!EG(exception) || ret == T_ERROR); return ret; }
这里的 zend_parser_stack_elem 结构体实际上是用来存放 value 的,因为后面语法分析才会用到所以这里暂且不提。
zendlex 调用 lex_scan,后面就是词法分析的语法了,很长这里就不贴了,如果想要自己看最好看 .l 的 re2c 规则文件(匹配规则使用正则表达式,还是挺好看的,简单来说规则就是:<state> 正则表达式 { 匹配后返回 token 的处理代码 } ),如果看 re2c 编译后的 c 文件,里面的代码逻辑简直不能看的。
结合测试代码和词法规则,我们可以得出分析的过程:
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 <INITIAL>"<?php" ([ \t]|{NEWLINE}) { HANDLE_NEWLINE(yytext[yyleng-1 ]); BEGIN(ST_IN_SCRIPTING); RETURN_OR_SKIP_TOKEN(T_OPEN_TAG); } <ST_IN_SCRIPTING,ST_DOUBLE_QUOTES,ST_HEREDOC,ST_BACKQUOTE,ST_VAR_OFFSET>"$" {LABEL} { RETURN_TOKEN_WITH_STR(T_VARIABLE, 1 ); } <ST_IN_SCRIPTING>{TOKENS} { RETURN_TOKEN(yytext[0 ]); } <ST_IN_SCRIPTING>{LNUM} { ... RETURN_TOKEN_WITH_VAL(T_LNUMBER); } <ST_IN_SCRIPTING>{TOKENS} { RETURN_TOKEN(yytext[0 ]); }
匹配到 = 和 ; 会直接返回字符的,没有什么其他操作。
在匹配到 <?php\n 的 open tag 之后,会将 state 改写为 ST_IN_SCRIPTING 用于下一次的匹配,然后返回一个代表 open tag 的 token(一般是一个整型数)。
然后开始匹配变量 $Twings,这里符合的匹配规则是 “$”{LABEL}(LABEL 其实就是 PHP 的合法变量名构成),这个规则除了返回 token,还会用变量名生成一个 AST 节点存储在前面提到的 zend_parser_stack_elem 里面,在后面的语法分析部分会作为子节点组装成一个完整的操作节点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #define RETURN_TOKEN_WITH_STR(_token, _offset) do { \ token = _token; \ offset = _offset; \ goto emit_token_with_str; \ } while (0) ... emit_token_with_str: zend_copy_value(zendlval, (yytext + offset), (yyleng - offset)); emit_token_with_val: if (PARSER_MODE()) { ZEND_ASSERT(Z_TYPE_P(zendlval) != IS_UNDEF); elem->ast = zend_ast_create_zval_with_lineno(zendlval, start_line); }
elem -> ast 的生成代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_zval_with_lineno (zval *zv, uint32_t lineno) { return zend_ast_create_zval_int(zv, 0 , lineno); }static zend_always_inline zend_ast * zend_ast_create_zval_int (zval *zv, uint32_t attr, uint32_t lineno) { zend_ast_zval *ast; ast = zend_ast_alloc(sizeof (zend_ast_zval)); ast->kind = ZEND_AST_ZVAL; ast->attr = attr; ZVAL_COPY_VALUE(&ast->val, zv); Z_LINENO(ast->val) = lineno; return (zend_ast *) ast; }
可以看到本来是个存放着 zval 的 zend_ast_zval 结构体,返回的时候为了适应 AST 树节点结构体而转换为了 zend_ast 类型的指针。
后半 LNUM 的操作里面有八进制和下划线的一些处理,想知道的可以自行阅读源码。
词法分析的过程大致如上,获取到 token 之后,接下来就是语法分析了。
语法分析 同样地,想要阅读源码建议看 zend_language_parser.y 里面 Bison 编译前的代码,更加人类可读一点。
在开头部分可以看到很多用 %token 声明的 token,比如说前面解析出的 T_VARIABLE:
1 %token <ast> T_VARIABLE "variable (T_VARIABLE)"
<ast> 指 T_VARIABLE 的类型为 ast,其实就是前面提到的用 union 声明的 _zend_parser_stack_elem 结构体里面的 ast 成员,这个结构体里面声明的就是语法分析里面 token 要用的类型:
1 2 3 4 5 6 typedef union _zend_parser_stack_elem { zend_ast *ast; zend_string *str; zend_ulong num; unsigned char *ptr; } zend_parser_stack_elem;
zend_ast 的定义如下,主要的就是操作码和子节点两个成员:
1 2 3 4 5 6 struct _zend_ast { zend_ast_kind kind; zend_ast_attr attr; uint32_t lineno; zend_ast *child[1 ]; };
双引号里面的是这个 token 人类可读的字面表示。
语法规则跟词法分析里面的相似,同样是匹配 + 处理,不同的是语法分析里面还有套娃匹配。
阅读规则,我们可以得出分析的过程:
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 44 45 46 47 48 49 start: top_statement_list { CG(ast) = $1 ; } ; top_statement_list: top_statement_list top_statement { $$ = zend_ast_list_add($1 , $2 ); } | { $$ = zend_ast_create_list(0 , ZEND_AST_STMT_LIST); } ; top_statement: statement { $$ = $1 ; } | ... ; statement: ... | expr ';' { $$ = $1 ; } | ... ; expr: ... | variable '=' expr { $$ = zend_ast_create(ZEND_AST_ASSIGN, $1 , $3 ); } | scalar { $$ = $1 ; } | ... ; variable: callable_variable { $$ = $1 ; } | ... ; callable_variable: simple_variable { $$ = zend_ast_create(ZEND_AST_VAR, $1 ); } | ... ; simple_variable: T_VARIABLE { $$ = $1 ; } | .... ; scalar: T_LNUMBER { $$ = $1 ; } | ;
$$ 代表本次处理完成后返回给上一级的值,$1 代表匹配规则中第一个符号的值。
这样看来就比较直观了,递归套娃,expr 中调用 zend_ast_create_list,用之前词法分析生成的两个 AST 节点组装成一个 ASSIGN 操作的节点,因为 ASSIGN 操作需要 2 个操作数,所以最后调用的其实是 zend_ast_create_2 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_2 (zend_ast_kind kind, zend_ast *child1, zend_ast *child2) { zend_ast *ast; uint32_t lineno; ZEND_ASSERT(kind >> ZEND_AST_NUM_CHILDREN_SHIFT == 2 ); ast = zend_ast_alloc(zend_ast_size(2 )); ast->kind = kind; ast->attr = 0 ; ast->child[0 ] = child1; ast->child[1 ] = child2; if (child1) { lineno = zend_ast_get_lineno(child1); } else if (child2) { lineno = zend_ast_get_lineno(child2); } else { lineno = CG(zend_lineno); } ast->lineno = lineno; return ast; }
简单来说就是将操作码和两个子节点放进一个新的节点里。完成一整部分代码(top_statement)的分析之后,会将这一部分生成的节点挂到前面(top_statement_list)生成的 AST 树的子节点上,后面通过遍历这棵树来依次处理这些 AST 节点。
遍历 AST 树 zend_compile 在调用 zendparse 完成语法分析之后,会调用 zend_compile_top_stmt 开始处理 AST 树:
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 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); uint32_t i; for (i = 0 ; i < list ->children; ++i) { zend_compile_top_stmt(list ->child[i]); } return ; } if (ast->kind == ZEND_AST_FUNC_DECL) { ... } else if (ast->kind == ZEND_AST_CLASS) { ... } else { zend_compile_stmt(ast); } if (ast->kind != ZEND_AST_NAMESPACE && ast->kind != ZEND_AST_HALT_COMPILER) { zend_verify_namespace(); } }
如果是根节点,则遍历它的子节点重新调用本函数。
如果是 ASSIGN 类型的节点,则经过 zend_compile_stmt 和 zend_compile_expr 函数的 switch 之后会调用 zend_compile_assign:
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 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; if (is_this_fetch(var_ast)) { zend_error_noreturn(E_COMPILE_ERROR, "Cannot re-assign $this" ); } zend_ensure_writable_variable(var_ast); switch (var_ast->kind) { case ZEND_AST_VAR: offset = zend_delayed_compile_begin(); zend_delayed_compile_var(&var_node, var_ast, BP_VAR_W, 0 ); zend_compile_expr(&expr_node, expr_ast); zend_delayed_compile_end(offset); zend_emit_op(result, ZEND_ASSIGN, &var_node, &expr_node); return ; case ZEND_AST_STATIC_PROP: ... case ZEND_AST_DIM: ... case ZEND_AST_PROP: ... case ZEND_AST_ARRAY: ... EMPTY_SWITCH_DEFAULT_CASE(); } }
主要看 case ZEND_AST_VAR 里面的处理,这里调用 zend_emit_op 将 AST 节点处理成了 opcode,其结构体为 zend_op,里面存放了该 opcode 的处理函数(用于最后执行相应的操作)、操作码、操作数的值和类型等数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 struct _zend_op { const void *handler; znode_op op1; znode_op op2; znode_op result; uint32_t extended_value; uint32_t lineno; zend_uchar opcode; zend_uchar op1_type; zend_uchar op2_type; zend_uchar result_type; };
顺带一提的是这里 var_node 的类型,即 op1_type 表示的是 cv(编译期缓存),还有就是这个时候,opcode 中的处理函数还是空的,赋值操作还在后面。然后将这个 opcode 放入 opcode 数组中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static zend_op *zend_emit_op (znode *result, zend_uchar opcode, znode *op1, znode *op2) { zend_op *opline = get_next_op(); opline->opcode = opcode; if (op1 != NULL ) { SET_NODE(opline->op1, op1); } if (op2 != NULL ) { SET_NODE(opline->op2, op2); } if (result) { zend_make_var_result(result, opline); } return opline; }
opcode 数组的结构体比较长,这里就不贴了。
zend_compile_top_stmt 遍历完 AST 树生成 opcode 之后,会调用 pass_two 函数为每个 opcode 加上处理函数,因为查找的方式比较复杂,这里就不提了,想要研究的可以自行阅读 zend_vm_execute.h 的 zend_vm_get_opcode_handler_ex 函数。
执行 这时回到 zend_execute_scripts 函数中,生成 op_array,即 opcode 数组之后,会调用 zend_execute 开始执行,因为这个函数代码量过大,这里只简单叙述一下。
opcode 的执行过程是一个死循环,每次调用该 opcode 的处理函数之后会对返回值 ret 进行判断,如果 <= 0 就会终止循环,而 zend_compile 函数在调用 zend_compile_top_stmt 处理完 AST 树之后,还会调用 zend_emit_final_return 在末尾加上一个 opcode 用于终止循环。
在测试脚本中只有一个 opcode,相应的处理函数是 ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER:
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 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(); value = RT_CONSTANT(opline, opline->op2); variable_ptr = EX_VAR(opline->op1.var); if (IS_CV == IS_VAR && UNEXPECTED(Z_ISERROR_P(variable_ptr))) { if (UNEXPECTED(0 )) { ZVAL_NULL(EX_VAR(opline->result.var)); } } else { value = zend_assign_to_variable(variable_ptr, value, IS_CONST, EX_USES_STRICT_TYPES()); if (UNEXPECTED(0 )) { ZVAL_COPY(EX_VAR(opline->result.var), value); } } ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); }
在执行结束之后,会调用 ZEND_VM_NEXT_OPCODE_EX 移动 opcode 指针指向下一条 opcode。
后记 简单理了一遍过程,以后有时间再看看其他感兴趣的东西,比如变量哈希表如何存放,函数定义与执行,函数递归等等。
参考:
https://www.php.cn/php-weizijiaocheng-357239.html
https://www.kancloud.cn/nickbai/php7/363255
https://www.bookstack.cn/read/php-internals/0.md