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脚本的功能。

代码执行的流程如下图

image-20220426181008718

指令

不得不提到的一个概念,简单介绍一下,摘自《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; /* Needs to be signed */
#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		/* Unused operand */
#define IS_CONST (1<<0)
#define IS_TMP_VAR (1<<1)
#define IS_VAR (1<<2)
#define IS_CV (1<<3) /* Compiled variable */

这些类型是按位表示的,具体含义如下

  • 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.hzend_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

比如

1
$a = 1;

对应的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();
//获取op2对应的值,也就是1
value = RT_CONSTANT(opline, opline->op2);
//在execute_data中获取op1的位置,也就是$a
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 {
//将1赋值给$a
value = zend_assign_to_variable(variable_ptr, value, IS_CONST);
if (UNEXPECTED(0)) {
ZVAL_COPY(EX_VAR(opline->result.var), value);
}

/* zend_assign_to_variable() always takes care of op2, never free it! */
}

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));
//初始化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);
//遍历AST生成Opline
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;
//设置handler
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编译
  • 赋值handler

初始化

在遍历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);
}
/* break omitted intentionally */
case ZEND_JMP:
ZEND_PASS_TWO_UPDATE_JMP_TARGET(op_array, opline, opline->op1);
break;
case ZEND_JMPZNZ:
/* absolute index to relative offset */
opline->extended_value = ZEND_OPLINE_NUM_TO_OFFSET(op_array, opline, opline->extended_value);
/* break omitted intentionally */
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:
{
/* If result of assert is unused, result of check is unused as well */
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:
/* absolute index to relative offset */
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:
{
/* absolute indexes to relative offsets */
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语句对每个操作进行设置

这里存在一个宏定义

image-20220426194048955

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";
}
?>

脚本代码执行阶段

进入执行函数

image-20220426194332067

zend_compile_file就是编译的入口,也是返回一个待调用的op_array

image-20220426194433964

继续跟踪,其实这里存在对pahr文件的解析,不知道有没有其他操作,先不看

image-20220426194522921

开始调用zend_compile函数

image-20220426194620790

初始化op_array

image-20220426194703046

然后开始解析AST树,进入zend_compile_foreach函数

image-20220426194845562

对opline进行赋值,其中可以看到opcode时ZEND_FE_RESET_RW或者ZEND_FE_RESET_R

image-20220426194833898

此处的opcode是77,只使用了op1,类型是8

image-20220426200333341

所以对用的handler应该是

ZEND_FE_RESET_R_SPEC_CV_HANDLER

image-20220426201051219

开始执行过程

image-20220426201142015

image-20220426201201937

这个函数里面就是对之前的handler进行循环调用

image-20220426201242203

最终会调用到刚才设置的方法

image-20220426201607082

最后执行完成

image-20220426201831657

0x04 end