0x00 前言 看的有点乱,简单介绍一下
0x01 生命周期 PHP
的生命周期共有五个阶段
php_module_startup 模块初始化阶段
本阶段主要进行php
框架、Zend
引擎的初始化操作,比如启动zend
引擎、解析php.ini
、将php.ini
中配置的扩展加载到php
中,注册php.ini
中禁用的函数、类:disable_functions、disable_classes
等。
php_request_startup 请求初始化阶段
该阶段是在请求处理前每个请求都会经历的一个阶段,对于Fpm
而言,实在worker
进程中accept
一个请求并读取、解析完请求数据后的一个阶段。
主要完成以下几个操作:
激活输出:php_output_activate
。
初始化编译器、执行器、重置垃圾回收器、初始化全局变量符号表。
php_execute_script 执行脚本阶段
php_request_shutdown 请求关闭阶段
与请求初始化阶段相反,此阶段将flush
输出内容、发送header
头、清理全局变量、关闭编译器、关闭执行器等。
php_module_shutdown 模块关闭阶段
与模块初始化阶段相反,次阶段将清理资源、各php
模块关闭等操作。
0x02 脚本执行阶段 其他的四个阶段,探索生命周期 中描述的还是很清楚的,不说了
我这里遇到的情况主要是要搞清楚脚本执行阶段
此阶段会经历如下四个步骤
Scanning(Lexing) ,将PHP代码转换为语言片段(Tokens)
Parsing, 将Tokens转换成简单而有意义的表达式
Compilation,将表达式编译成Opcodes
Execution,顺次执行Opcodes,每次一条,从而实现PHP脚本的功能。
代码执行的流程如下图
指令 不得不提到的一个概念,简单介绍一下,摘自《PHP底层设计于源码实现》
Zend虚拟机的指令成为opline,每条指令对应一个opcode(
PHP代码在编译后生成opline,然后Zend虚拟机根据不同的opline完成PHP代码的执行
其中的关键参数就是handler,opline对应的结构体如下
1 2 3 4 5 6 7 8 9 10 11 12 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; };
PHP代码会被编译成一条一条的opline,分解为最基本的操作。举个例子,如果把opcode当成一个计算器,直接收两个操作数op1和op2,执行一个操作handler,比如加、减、乘、除,然后它返回一个结果result,再稍加处理算术溢出的情况,存于extended_value中。下面详细介绍下各个字段。
opcode opcode有时候被称为所谓的字节码(bytecode),是被软件解释器解释执行的指令集。这些软件指令集通常会提供一些比对应的硬件指令集更高级的数据类型和操作。
Zend虚拟机有很多opcode,对应可以做非常多事情,所有的opcode都在PHP的源代码文件Zend/zend_vm_opcodes.h
中定义,opcode的名称是自描述的,例如
ZEND_ASSGIN:赋值操作
ZEND_ADD:两个数相加操作
ZEND_JMP:跳转操作
在我测试的PHP7.3.4中定义了,将近200个操作
1 2 3 4 5 6 7 8 9 10 11 #define ZEND_NOP 0 #define ZEND_ADD 1 #define ZEND_SUB 2 #define ZEND_MUL 3 #define ZEND_DIV 4 #define ZEND_MOD 5 #define ZEND_SL 6 ... #define ZEND_ISSET_ISEMPTY_CV 197 #define ZEND_FETCH_LIST_W 198 #define ZEND_VM_LAST_OPCODE 198
操作数 op1和op2都是操作数,但不一定全部使用,也就是说,每个opcode对应的handler最多可以使用两个操作数(也可以用一个,或者都不使用)。每个操作数都可以理解为函数的参数,返回值result是handler函数对操作数op1和op2计算后的结果。op1、op2和result对应的类型都是znode_op,其定义为一个联合体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef union _znode_op { uint32_t constant; uint32_t var; uint32_t num; uint32_t opline_num; #if ZEND_USE_ABS_JMP_ADDR zend_op *jmp_addr; #else uint32_t jmp_offset; #endif #if ZEND_USE_ABS_CONST_ADDR zval *zv; #endif } znode_op;
这样其实每个操作数都是uint32类型的数字,一半表示变量的位置。操作数有5中不同的类型,具体在Zend/zend_compile.h
中定义
1 2 3 4 5 #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)
这些类型是按位表示的,具体含义如下
IS_CONST:值为1,表示一个常量,都是只读的,值不可改变,如$a='123'
中的123
IS_VAR:值为4,是PHP变量,这个变量并不是PHP代码中声明的变量,常见的是返回的临时变量,如$a=time()
,函数time返回值的类型就是IS_VAR,这种类型的变量是可以被其他opcode对应的handler重复使用的
IS_TMP_VAR:值为2,也是PHP变量,跟IS_VAR不同之处是,不能与其他opcode重复使用,如$a='123'.time()
,这里拼接的临时变量的类型就是IS_TMP_VAR,一半用于操作的中间结果
IS_UNUSED:值为8,表示这个操作数没包含任何有意义的东西
IS_CV:值为16,编译变量(compiled variable),这个操作数类型表示一个PHP变量,以$something
形式在PHP脚本中出现
handler handler对应的是opcode的实际处理函数,Zend虚拟机对每个opcode的工作方式是完全相同的,都有一个handler函数指针,执行处理函数的地址。
处理函数是一个C函数,包含了执行opcode对应的代码,以op1、op2为参数,执行完成后,会返回一个结果result,有时也会附加一段信息。
文件Zend/zend_vm_execute.h
(这个文件是通过zend_vm_gen.php
解析zend_vm_def.h
和zend_vm_execute.skl
生成的)包含所有的handler对应的函数
同一个opcode对应的handler函数会根据操作数的类型而不同,比如ZEND_ASSIGN对应的handler就有多个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ZEND_ASSIGN_SPEC_VAR_CONST_RETVAL_UNUSED_HANDLER, ZEND_ASSIGN_SPEC_VAR_CONST_RETVAL_USED_HANDLER, ZEND_ASSIGN_SPEC_VAR_TMP_RETVAL_UNUSED_HANDLER, ZEND_ASSIGN_SPEC_VAR_TMP_RETVAL_USED_HANDLER, ZEND_ASSIGN_SPEC_VAR_VAR_RETVAL_UNUSED_HANDLER, ZEND_ASSIGN_SPEC_VAR_VAR_RETVAL_USED_HANDLER, ZEND_ASSIGN_SPEC_VAR_CV_RETVAL_UNUSED_HANDLER, ZEND_ASSIGN_SPEC_VAR_CV_RETVAL_USED_HANDLER, ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER, ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_USED_HANDLER, ZEND_ASSIGN_SPEC_CV_TMP_RETVAL_UNUSED_HANDLER, ZEND_ASSIGN_SPEC_CV_TMP_RETVAL_USED_HANDLER, ZEND_ASSIGN_SPEC_CV_VAR_RETVAL_UNUSED_HANDLER, ZEND_ASSIGN_SPEC_CV_VAR_RETVAL_USED_HANDLER, ZEND_ASSIGN_SPEC_CV_CV_RETVAL_UNUSED_HANDLER, ZEND_ASSIGN_SPEC_CV_CV_RETVAL_USED_HANDLER,
函数的命名规则如下
ZEND_[opcode]_SPEC_(操作数1类型)_(操作数2类型)_HANDLER
比如
对应的handler为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 28 29 30 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); if (UNEXPECTED(0 )) { ZVAL_COPY(EX_VAR(opline->result.var), value); } } ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); }
这么说来,在PHP代码写好的那一刻就确定了我们写的代码,将会调用哪些handler
主要就是看我们写了什么操作
AST编译 这里再看一下编译的过程
AST的编译时生成指令集opcode的过程,词法和语法分析后生成AST会保存在CG中,然后Zend虚拟机会将AST进一步转黄成zend_op_array,以便在虚拟机中执行
编译过程在zend_compile函数中进行,代码如下
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 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()) { 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); 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)); CG(zend_lineno) = last_lineno; zend_emit_final_return(type == ZEND_USER_FUNCTION); op_array->line_start = 1 ; op_array->line_end = last_lineno; pass_two(op_array); 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,用来存放指令,主要函数init_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 44 45 46 47 48 49 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 ; op_array->refcount = (uint32_t *) emalloc(sizeof (uint32_t )); *op_array->refcount = 1 ; op_array->last = 0 ; op_array->opcodes = emalloc(initial_ops_size * sizeof (zend_op)); op_array->last_var = 0 ; op_array->vars = NULL ; op_array->T = 0 ; op_array->function_name = NULL ; op_array->filename = zend_get_compiled_filename(); op_array->doc_comment = NULL ; op_array->arg_info = NULL ; op_array->num_args = 0 ; op_array->required_num_args = 0 ; op_array->scope = NULL ; op_array->prototype = NULL ; op_array->live_range = NULL ; op_array->try_catch_array = NULL ; op_array->last_live_range = 0 ; op_array->static_variables = NULL ; op_array->last_try_catch = 0 ; op_array->fn_flags = 0 ; op_array->last_literal = 0 ; op_array->literals = NULL ; op_array->run_time_cache = NULL ; op_array->cache_size = 0 ; memset (op_array->reserved, 0 , ZEND_MAX_RESERVED_RESOURCES * sizeof (void *)); if (zend_extension_flags & ZEND_EXTENSIONS_HAVE_OP_ARRAY_CTOR) { zend_llist_apply_with_argument(&zend_extensions, (llist_apply_with_arg_func_t ) zend_extension_op_array_ctor_handler, op_array); } }
首先通过emalloc申请内存,然后初始化op_array的所有成员变量,把op_array赋值给CG
AST编译 编译在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 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 ; } 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_stmt
中完成的,可以看到对应的每个操作
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 void zend_compile_stmt (zend_ast *ast) { if (!ast) { return ; } CG(zend_lineno) = ast->lineno; if ((CG(compiler_options) & ZEND_COMPILE_EXTENDED_INFO) && !zend_is_unticked_stmt(ast)) { zend_do_extended_info(); } switch (ast->kind) { case ZEND_AST_STMT_LIST: zend_compile_stmt_list(ast); break ; case ZEND_AST_GLOBAL: zend_compile_global_var(ast); break ; case ZEND_AST_STATIC: zend_compile_static_var(ast); break ; case ZEND_AST_UNSET: zend_compile_unset(ast); break ; case ZEND_AST_RETURN: zend_compile_return(ast); break ; case ZEND_AST_ECHO: zend_compile_echo(ast); break ; case ZEND_AST_THROW: zend_compile_throw(ast); break ; case ZEND_AST_BREAK: case ZEND_AST_CONTINUE: zend_compile_break_continue(ast); break ; case ZEND_AST_GOTO: zend_compile_goto(ast); break ; case ZEND_AST_LABEL: zend_compile_label(ast); break ; case ZEND_AST_WHILE: zend_compile_while(ast); break ; case ZEND_AST_DO_WHILE: zend_compile_do_while(ast); break ; case ZEND_AST_FOR: zend_compile_for(ast); break ; case ZEND_AST_FOREACH: zend_compile_foreach(ast); break ; case ZEND_AST_IF: zend_compile_if(ast); break ; case ZEND_AST_SWITCH: zend_compile_switch(ast); break ; case ZEND_AST_TRY: zend_compile_try(ast); break ; case ZEND_AST_DECLARE: zend_compile_declare(ast); break ; case ZEND_AST_FUNC_DECL: case ZEND_AST_METHOD: zend_compile_func_decl(NULL , ast); break ; case ZEND_AST_PROP_DECL: zend_compile_prop_decl(ast); break ; case ZEND_AST_CLASS_CONST_DECL: zend_compile_class_const_decl(ast); break ; case ZEND_AST_USE_TRAIT: zend_compile_use_trait(ast); break ; case ZEND_AST_CLASS: zend_compile_class_decl(ast); break ; case ZEND_AST_GROUP_USE: zend_compile_group_use(ast); break ; case ZEND_AST_USE: zend_compile_use(ast); break ; case ZEND_AST_CONST_DECL: zend_compile_const_decl(ast); break ; case ZEND_AST_NAMESPACE: zend_compile_namespace(ast); break ; case ZEND_AST_HALT_COMPILER: zend_compile_halt_compiler(ast); break ; default : { znode result; zend_compile_expr(&result, ast); zend_do_free(&result); } } if (FC(declarables).ticks && !zend_is_unticked_stmt(ast)) { zend_emit_tick(); } }
赋值handler 通过pass_two
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 ZEND_API int pass_two (zend_op_array *op_array) { opline = op_array->opcodes; end = opline + op_array->last; while (opline < end) { switch (opline->opcode) { case ZEND_RECV_INIT: { zval *val = CT_CONSTANT(opline->op2); if (Z_TYPE_P(val) == IS_CONSTANT_AST) { uint32_t slot = ZEND_MM_ALIGNED_SIZE_EX(op_array->cache_size, 8 ); Z_CACHE_SLOT_P(val) = slot; op_array->cache_size += sizeof (zval); } } break ; case ZEND_FAST_CALL: opline->op1.opline_num = op_array->try_catch_array[opline->op1.num].finally_op; ZEND_PASS_TWO_UPDATE_JMP_TARGET(op_array, opline, opline->op1); break ; case ZEND_BRK: case ZEND_CONT: { uint32_t jmp_target = zend_get_brk_cont_target(op_array, opline); if (op_array->fn_flags & ZEND_ACC_HAS_FINALLY_BLOCK) { zend_check_finally_breakout(op_array, opline - op_array->opcodes, jmp_target); } opline->opcode = ZEND_JMP; opline->op1.opline_num = jmp_target; opline->op2.num = 0 ; ZEND_PASS_TWO_UPDATE_JMP_TARGET(op_array, opline, opline->op1); } break ; case ZEND_GOTO: zend_resolve_goto_label(op_array, opline); if (op_array->fn_flags & ZEND_ACC_HAS_FINALLY_BLOCK) { zend_check_finally_breakout(op_array, opline - op_array->opcodes, opline->op1.opline_num); } case ZEND_JMP: ZEND_PASS_TWO_UPDATE_JMP_TARGET(op_array, opline, opline->op1); break ; case ZEND_JMPZNZ: opline->extended_value = ZEND_OPLINE_NUM_TO_OFFSET(op_array, opline, opline->extended_value); case ZEND_JMPZ: case ZEND_JMPNZ: case ZEND_JMPZ_EX: case ZEND_JMPNZ_EX: case ZEND_JMP_SET: case ZEND_COALESCE: case ZEND_FE_RESET_R: case ZEND_FE_RESET_RW: ZEND_PASS_TWO_UPDATE_JMP_TARGET(op_array, opline, opline->op2); break ; case ZEND_ASSERT_CHECK: { zend_op *call = &op_array->opcodes[opline->op2.opline_num - 1 ]; if (call->opcode == ZEND_EXT_FCALL_END) { call--; } if (call->result_type == IS_UNUSED) { opline->result_type = IS_UNUSED; } ZEND_PASS_TWO_UPDATE_JMP_TARGET(op_array, opline, opline->op2); break ; } case ZEND_DECLARE_ANON_CLASS: case ZEND_DECLARE_ANON_INHERITED_CLASS: case ZEND_FE_FETCH_R: case ZEND_FE_FETCH_RW: opline->extended_value = ZEND_OPLINE_NUM_TO_OFFSET(op_array, opline, opline->extended_value); break ; case ZEND_CATCH: if (!(opline->extended_value & ZEND_LAST_CATCH)) { ZEND_PASS_TWO_UPDATE_JMP_TARGET(op_array, opline, opline->op2); } break ; case ZEND_RETURN: case ZEND_RETURN_BY_REF: if (op_array->fn_flags & ZEND_ACC_GENERATOR) { opline->opcode = ZEND_GENERATOR_RETURN; } break ; case ZEND_SWITCH_LONG: case ZEND_SWITCH_STRING: { HashTable *jumptable = Z_ARRVAL_P(CT_CONSTANT(opline->op2)); zval *zv; ZEND_HASH_FOREACH_VAL(jumptable, zv) { Z_LVAL_P(zv) = ZEND_OPLINE_NUM_TO_OFFSET(op_array, opline, Z_LVAL_P(zv)); } ZEND_HASH_FOREACH_END(); opline->extended_value = ZEND_OPLINE_NUM_TO_OFFSET(op_array, opline, opline->extended_value); break ; } } if (opline->op1_type == IS_CONST) { ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline, 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, 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++; } }
从这里可以看的出来,是通过分析的AST有多少个操作,通过while语句对每个操作进行设置
这里存在一个宏定义
0x03 流程跟踪 这里以一个foreach进行跟踪
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php class A extends ArrayIterator { protected $call ; public function __construct ($data , $callback ) { parent ::__construct($data ); $this ->call = $callback ; } public function current ( ) { $value = "whoami" ; $value = call_user_func($this ->call,$value ); var_dump($value ); return $value ; } } $a = new A(array ('whoami' ),'system' );foreach ($a as $b ){ echo ":aaa" ; } ?>
脚本代码执行阶段 进入执行函数
zend_compile_file就是编译的入口,也是返回一个待调用的op_array
继续跟踪,其实这里存在对pahr文件的解析,不知道有没有其他操作,先不看
开始调用zend_compile函数
初始化op_array
然后开始解析AST树,进入zend_compile_foreach
函数
对opline进行赋值,其中可以看到opcode时ZEND_FE_RESET_RW
或者ZEND_FE_RESET_R
此处的opcode是77,只使用了op1,类型是8
所以对用的handler应该是
ZEND_FE_RESET_R_SPEC_CV_HANDLER
开始执行过程
这个函数里面就是对之前的handler进行循环调用
最终会调用到刚才设置的方法
最后执行完成
0x04 end …