内容简介:baiyan全部视频:原视频地址:
【PHP源码学习】2019-03-22 AST的遍历-1
baiyan
全部视频: https://segmentfault.com/a/11...
原视频地址: http://replay.xesv5.com/ll/24...
引入
- 先看上一节笔记中展示的AST示例:
<?php $a = 1; $b = $a + 2;
- 在 PHP 中,构造出来的抽象语法树如图所示:
- 那么,这个AST后面能够用来做什么呢?因为我们最终需要执行这段PHP代码,所以需要将其转化为可执行的指令,让虚拟机最终来解释执行
- 指令的几个要素:
操作数(OP1、OP2,两个就够了,因为多元运算可以转化成二元运算)
指令操作(handler)
运算的中间结果 返回值 (result)
- 这些指令要素是我们自己定义的,而寄存器是无法理解这些自定义指令的,这就需要 zend虚拟机(zendvm) 去进行指令转换工作并且真正的执行这些指令。
举例:$a = 1 + 2; 这行代码中,$a/1/2是操作数,1+2计算的中间结果3是返回值,加法和赋值是指令做的具体操作,加法对应加法的opcode与相应的handler,赋值对应赋值的opcode与相应的handler
- 接下来讲一下这些指令在PHP7中是如何存储的:
指令的基本概念及存储结构
- 在PHP的zend虚拟机中,每条指令都是一个 opline ,每个opline由操作数、指令操作、返回值组成。每个指令操作都对应一个 opcode (如ZEND_ASSIGN/ZEND_ADD等等),而每个opcode又对应一个 handler 处理函数。这样,zend虚拟机就可以根据生成的指令,找到对应的指令处理函数,把操作数作为参数传入,即可完成指令的执行
基本概念总结
opline:在zend虚拟机中,每条指令都是一个 opline ,每个opline由操作数、指令操作、返回值组成
opcode:每个指令操作都对应一个 opcode (如ZEND_ASSIGN/ZEND_ADD等等),在PHP7中,有100多种指令操作,所有的指令集被称作opcodes
handler:每个opcode指令操作都对应一个 handler 指令处理函数,处理函数中有具体的指令操作执行逻辑
- 下面看一下PHP内部的具体实现,回想昨天调试的zend_compile断点处,它是编译的入口,并返回生成结果op_array:
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宏可以取得compile_globals结构体中的字段 CG(ast) = NULL;//一开始的AST为NULL CG(ast_arena) = zend_arena_create(1024 * 32);//给AST分配空间 if (!zendparse()) { //进行词法和语法分析 ....... //初始化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; ...... //对AST进行遍历并生成指令,并存储到zend_op_array中 zend_compile_top_stmt(CG(ast)); ...... //设置handler pass_two(op_array); } return op_array; }
- 重点关注zend_op_array这个类型,它是一个数组,用来存储所有的指令集(即所有的opline)。来看看它的结构:
struct _zend_op_array { ... uint32_t last; //数组的长度 zend_op *opcodes; //存储所有opline的值 ... };
- zend_op_array是指令的集合,那么每条指令在zendvm中是一个opline。它由 指令操作、操作数、返回值、以及指令操作对应的handler 组成。每一条指令opline对应的结构体是zend_op:
struct _zend_op { const void *handler; //操作 znode_op op1; //操作数1 znode_op op2; //操作数2 znode_op result; //操作结果 uint32_t extended_value; uint32_t lineno; //行号 zend_uchar opcode; //opcode值 zend_uchar op1_type; //操作数1类型 zend_uchar op2_type; //操作数2类型 zend_uchar result_type; //返回值类型 };
- 每一条指令opline,对应一个zend_op,存放指令集的zend_op_array由多条zend_op所构成
- 现在我们知道了指令中的具体操作是由opcode和handler来表示和处理,那么继续看一下 操作数 和 返回值 具体是如何表示的,如zend_op结构体中所示,它们的类型均为znode_op类型:
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;
- 可以看到,constant、var、num都是uint32类型的,这个uint32类型并不足以表示所有操作数。这里存储的是 相对于执行栈帧首地址的偏移量 。因为CV/临时变量这些都是分配在栈上的(后面会讲)。通过计算,我们才能得出最终操作数在栈桢中的位置
- 既然如何我们知道了如何存储指令,那么下面讲一下如何遍历这棵抽象语法树,来得到这些指令集:
遍历抽象语法树
编译根节点(LIST)
我们现在仅仅关注$a = 1这一行代码并对其AST进行遍历
- 关注zend_compile中的zend_compile_top_stmt(CG(ast))这个函数调用,它会对这棵AST进行遍历:
void zend_compile_top_stmt(zend_ast *ast) { if (!ast) { return; } if (ast->kind == ZEND_AST_STMT_LIST) { //如果是这个AST是LIST类型 zend_ast_list *list = zend_ast_get_list(ast);//将其转换成ZEND_AST_LIST类型即可(上一篇笔记是在gdb下直接强转的) uint32_t i; for (i = 0; i < list->children; ++i) { zend_compile_top_stmt(list->child[i]); //递归调用,进行深度遍历 } return; } zend_compile_stmt(ast); //编译的入口。递归调用的时候,如果往下走的时候并非LIST型结点,会调用这个函数 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(); } }
- 内联函数(inline):普通的函数调用是需要压栈的,而内联函数直接将函数体代码嵌入到调用位置,提高代码执行效率
-
具体代码执行过程(按照最开始的AST图来讲):
- 根节点进来,判断是ZEND_AST_LIST类型(132),故将其强转成ZEND_AST_LIST类型
- 递归调用函数本身,参数传入其第一个子结点(517,赋值运算符)
- 赋值运算符不是LIST类型,往下走到zend_compile_stmt(ast)中
- 接下来看下编译入口zend_compile_stmt()的具体实现:
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; ...... default: //最终会走到这里,因为所有的case中,没有ZEND_AST_ASSIGN类型与之匹配 { znode result; //声明了一个znode类型变量,存储返回值(像$a = 1 + 2)这种需要存储中间结果3的表达式才需要使用这个result zend_compile_expr(&result, ast); //调用这个函数处理当前表达式,下面会展开 zend_do_free(&result); } } ...... }
- 代码最终会走到default分支。会首先声明一个znode类型的变量result,看一下znode类型的结构:
typedef struct _znode { zend_uchar op_type; zend_uchar flag; union { znode_op op; //操作数变量的位置 zval constant; //常量 } u; } znode;
- 重点关注这个联合体u中的op以及constant字段,在后面可以用来存储编译过程中的中间值,先记下这个结构体
- 接下来会调用zend_compile_expr函数,继续跟进zend_compile_expr(&result, ast),注意这里的ast是517位根节点的子树而非最开始的ast。看一下这个函数的具体实现:
void zend_compile_expr(znode *result, zend_ast *ast) { ...... switch (ast->kind) { ...... case ZEND_AST_ASSIGN: zend_compile_assign(result, ast); //代码走到这里,调用这个函数,下面继续跟进 return; ...... } }
编译赋值(ASSIGN)等号
- 最终上面代码会走到ZEND_AST_ASSIGN的case中,并调用zend_compile_assign(result, ast)函数,继续跟进这个函数:
void zend_compile_assign(znode *result, zend_ast *ast) { zend_ast *var_ast = ast->child[0]; //517的第一个孩子256 zend_ast *expr_ast = ast->child[1]; //517的第二个孩子64 znode var_node, expr_node; //存储编译后的中间结果 zend_op *opline; uint32_t offset; ... switch (var_ast->kind) { case ZEND_AST_VAR: case ZEND_AST_STATIC_PROP: offset = zend_delayed_compile_begin(); zend_delayed_compile_var(&var_node, var_ast, BP_VAR_W); //编译$a,中间结果放到var_node这个znode上 zend_compile_expr(&expr_node, expr_ast); //编译1,中间结果放到expr_node这个znode上 zend_delayed_compile_end(offset); zend_emit_op(result, ZEND_ASSIGN, &var_node, &expr_node); //生成opline return; ...... } }
- 首先取出赋值等号(517)的第一个子结点,其类型是ZEND_AST_VAR(256),然后取出第二个子结点,其类型是ZEND_AST_ZVAL(64),switch之后来到ZEND_AST_STATIC_PROP这个case,直接看第二行这个函数zend_delayed_compile_var,它的功能是编译左边的$a
编译赋值等号左边的$a
- 接下来调用zend_delayed_compile_var(&var_node, var_ast, BP_VAR_W); 这个函数被用来编译左边的$a,它会将编译产生的中间结果放到var_node这个znode中:
void zend_delayed_compile_var(znode *result, zend_ast *ast, uint32_t type) { zend_op *opline; switch (ast->kind) { case ZEND_AST_VAR: zend_compile_simple_var(result, ast, type, 1); return; ... } }
- 代码会执行到ZEND_AST_VAR这个case中,然后调用zend_compile_simple_var(result, ast, type, 1),继续跟进:
static void zend_compile_simple_var(znode *result, zend_ast *ast, uint32_t type, int delayed) { zend_op *opline; if (is_this_fetch(ast)) { ...... } else if (zend_try_compile_cv(result, ast) == FAILURE) { ...... } }
- 首先第一个if中is_this_fetch(ast)是判断是否和this(对象)相关,我们这里不是,那么走到下一个else if分支,调用zend_try_compile_cv(result, ast)函数,继续跟进:
static int zend_try_compile_cv(znode *result, zend_ast *ast) { zend_ast *name_ast = ast->child[0]; if (name_ast->kind == ZEND_AST_ZVAL) { zend_string *name = zval_get_string(zend_ast_get_zval(name_ast)); if (zend_is_auto_global(name)) { zend_string_release(name); return FAILURE; } result->op_type = IS_CV; //将其类型标记为CV,CV变量在运行时是存在栈上的 result->u.op.var = lookup_cv(CG(active_op_array), name); //返回这个CV变量在运行时栈上的偏移量 name = CG(active_op_array)->vars[EX_VAR_TO_NUM(result->u.op.var)]; return SUCCESS; } return FAILURE; }
- 这个函数首先取它的第一个孩子结点,因为当前传过来的ast是256(ZEND_AST_VAR类型),它的孩子结点只有一个,且类型为64(ZEND_AST_ZVAL类型),所以第一个if判断为true,并且调用了zval_get_string(zend_ast_get_zval(name_ast))这个函数。我们由内往外看,首先对这个64的ast结点进行zend_ast_get_zval()函数调用,它会将ZEND_AST类型转化成ZEND_AST_ZVAL类型,和之前在gdb中调试的效果一样。接下来外部对这个ast结点调用zval_get_string()函数,看下它的内部实现:
static zend_always_inline zend_string *_zval_get_string(zval *op) { return Z_TYPE_P(op) == IS_STRING ? zend_string_copy(Z_STR_P(op)) : _zval_get_string_func(op); }
- 首先,Z_TYPE_P这个宏取得zval中的u1.v.type字段,如果是IS_STRING的话,调用zend_string_copy(z_str_p(op))函数,首先调用了Z_STR_P这个宏,它会取得zend_value中的str字段,就是指向zend_string的指针,我们可以看到以下上面宏的定义:
/* we should never set just Z_TYPE, we should set Z_TYPE_INFO */ #define Z_TYPE(zval) zval_get_type(&(zval)) #define Z_TYPE_P(zval_p) Z_TYPE(*(zval_p)) static zend_always_inline zend_uchar zval_get_type(const zval* pz) { return pz->u1.v.type; } ... #define Z_STR(zval) (zval).value.str #define Z_STR_P(zval_p) Z_STR(*(zval_p))
- 我们继续看一下外层zend_string_copy()的实现:
static zend_always_inline zend_string *zend_string_copy(zend_string *s) { if (!ZSTR_IS_INTERNED(s)) { GC_REFCOUNT(s)++; } return s; }
- 我们看到仅仅是做了一个对zend_string中的refcount字段++的操作,并没有真正去做具体的拷贝
- 回到zend_try_compile_cv()这个函数,我们在调用完之后将结果赋值给name变量。接下来因为变量不是全局的,所以不进这个if。接下来对result变量的两个字段进行了赋值操作:
result->op_type = IS_CV; result->u.op.var = lookup_cv(CG(active_op_array), name);
- 首先为它赋值IS_CV。那么什么是CV型变量呢?CV,即compiled variable,是PHP编译过程中产生的一种变量类型,以类似于缓存的方式,提高某些变量的存储速度。这里$a = 1中的$a,就是CV型变量,CV型变量在运行时是存储在zend_execute_data(后面会讲)虚拟机上的栈中的
- 第二行,给result中的u.op.var字段赋值,而result我们讲过,是一个znode。重点关注右边lookup_cv()函数,它返回一个int类型的地址,是sizeof(zval)的整数倍,通过它可以得到每个变量的偏移量(80(后面会讲) + 16 * i),i是变量的编号。这样就规定了运行时在栈上相对于zend_execute_data的偏移量,从而在栈上方便地存储了$a这个变量(下一篇笔记会详细讲)。而$a在zend_op_array的vars数组上也冗余存了一份,这样如果后面又用到了$a的话,直接去zend_op_array的vars数组中查找找,如果存在,那么直接使用之前的编号i,如果不存在则按序分配一个编号,然后再插入zend_op_array的vars数组,节省了分配编号的时间
static int lookup_cv(zend_op_array *op_array, zend_string* name) /* {{{ */{ int i = 0; zend_ulong hash_value = zend_string_hash_val(name); while (i < op_array->last_var) { if (ZSTR_VAL(op_array->vars[i]) == ZSTR_VAL(name) || (ZSTR_H(op_array->vars[i]) == hash_value && ZSTR_LEN(op_array->vars[i]) == ZSTR_LEN(name) && memcmp(ZSTR_VAL(op_array->vars[i]), ZSTR_VAL(name), ZSTR_LEN(name)) == 0)) { zend_string_release(name); return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i); } i++; } i = op_array->last_var; op_array->last_var++; if (op_array->last_var > CG(context).vars_size) { CG(context).vars_size += 16; /* FIXME */ op_array->vars = erealloc(op_array->vars, CG(context).vars_size * sizeof(zend_string*)); } op_array->vars[i] = zend_new_interned_string(name); return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i); }
编译赋值等号右边的值1
- 接下来回到外部zend_compile_assign函数,继续往下执行zend_compile_expr(&expr_node, expr_ast)函数,处理等号右边的值1,它会将编译产生的中间结果放到expr_node这个znode中:
- 接下来会走到zend_compile_expr函数的ZEND_AST_ZVAL这个case,看下这个case:
case ZEND_AST_ZVAL: ZVAL_COPY(&result->u.constant, zend_ast_get_zval(ast)); result->op_type = IS_CONST; return;
- 它调用了一个ZVAL_COPY宏,将这个ZEND_AST_ZVAL类型的结点的zval字段中存储的值(即1对应的zval),拷贝到之前声明的znode类型变量result的u.constant字段中,这样操作数就存放完毕了
- 看一下这个zend_ast_get_zval(ast)的具体实现:
static zend_always_inline zval *zend_ast_get_zval(zend_ast *ast) { ZEND_ASSERT(ast->kind == ZEND_AST_ZVAL); return &((zend_ast_zval *) ast)->val; }
- 先忽略断言,它直接利用强转并取出val字段的值,就是1对应的zval,并返回了它的地址
- 接下来再看一下ZVAL_COPY这个宏,它的功能是把一个zval(v)拷贝到另外一个zval(z)中
- 我们首先回顾一下zval的结构:
struct _zval_struct { zend_value value; /* value */ union { struct { ZEND_ENDIAN_LOHI_4( zend_uchar type, /* active type */ zend_uchar type_flags, zend_uchar const_flags, zend_uchar reserved) /* call info for EX(This) */ } v; uint32_t type_info; } u1; union { uint32_t next; /* hash collision chain */ uint32_t cache_slot; /* literal cache slot */ uint32_t lineno; /* line number (for ast nodes) */ uint32_t num_args; /* arguments number for EX(This) */ uint32_t fe_pos; /* foreach position */ uint32_t fe_iter_idx; /* foreach iterator index */ uint32_t access_flags; /* class constant access flags */ uint32_t property_guard; /* single property guard */ uint32_t extra; /* not further specified */ } u2; };
- 由zval结构,我们可以知道:复制zval,就是将老zval中的 value/u1/u2 三个字段拷贝到新zval中即可。
- 那么,源码中是怎么实现的呢:
#define ZVAL_COPY(z, v) \ do { \ zval *_z1 = (z); \ const zval *_z2 = (v); \ zend_refcounted *_gc = Z_COUNTED_P(_z2); \ uint32_t _t = Z_TYPE_INFO_P(_z2); \ ZVAL_COPY_VALUE_EX(_z1, _z2, _gc, _t); \ if ((_t & (IS_TYPE_REFCOUNTED << Z_TYPE_FLAGS_SHIFT)) != 0) { \ GC_REFCOUNT(_gc)++; \ } \ } while (0)
- 首先将z赋值给_z1,它是一个地址,然后将v赋值给_z2,也是一个地址,注意这个地址的值是常量,表示不能够修改v指向的zval的值(因为它是被赋值的zval,所以没必要修改它的值)
- 然后通过Z_COUNTED_P(_z2)这个宏取出_z2(v)这个zval中的refcounted字段,看下这个宏的具体实现:
#define Z_COUNTED(zval) (zval).value.counted #define Z_COUNTED_P(zval_p) Z_COUNTED(*(zval_p))
- 可以看到,它取出了zval中的zend_value字段中的counted字段的值,那么,为什么要取counted这个字段呢?我们回顾一下zend_value的结构:
typedef union _zend_value { zend_long lval; //整型 double dval; //浮点 zend_refcounted *counted; //引用计数,这里取出这个字段的值 zend_string *str; //字符串 zend_array *arr; //数组 zend_object *obj; //对象 zend_resource *res; //资源 zend_reference *ref; //引用 zend_ast_ref *ast; //抽象语法树 zval *zv; //内部使用 void *ptr; //不确定类型,取出来之后强转 zend_class_entry *ce; //类 zend_function *func;//函数 struct { uint32_t w1; uint32_t w2; } ww; //这个union一共8B,这个结构体每个字段都是4B,因为所有联合体字段共用一块内存,故相当于取了一半的union } zend_value;
- 这里一定要注意zend_value是一个 联合体 。由于其内部所有字段共用一块内存空间,源码中取counted字段的值,和取lval/dval/str/arr这些字段值的效果是 完全一样 的,其本质上就是取到了zval中的zend_value这个字段的值。
- 那么现在我们完成了从老的zval中取到zend_value这个字段的值,还剩下u1/u2两个字段需要我们去拿
- 接下来它使用了Z_TYPE_INFO_P(_z2);这个宏,看下它的实现:
#define Z_TYPE_INFO(zval) (zval).u1.type_info #define Z_TYPE_INFO_P(zval_p) Z_TYPE_INFO(*(zval_p))
- 它直接取到了zval中u1的type_info字段。由于u1也是一个联合体,实际上就是取得了v这个结构体中的type/type_flags/const_flags/reserved这四个字段的值,理由同上。这样,u1也拿到了,那么现在还剩下u2没有去取
- 接下来又去调用了ZVAL_COPY_VALUE_EX(_z1, _z2, _gc, _t)这个宏,我们看下它的实现:
# define ZVAL_COPY_VALUE_EX(z, v, gc, t) \ do { \ Z_COUNTED_P(z) = gc; \ Z_TYPE_INFO_P(z) = t; \ } while (0) #else
- 它就是直接将gc.counted字段,即zend_value的值)、以及t(即u1的值)直接拷贝到z这个zval中,这样就完成了zend_value以及u1的复制,那么u2为什么没有拷贝呢?因为u2这个联合体中的字段并不重要,不对其进行复制不会对代码逻辑有任何影响
- 下面的if我们可以先忽略,由于在ZVAL_COPY_VALUE_EX宏中完成了复制,可以不去考虑下面的逻辑
- 那么现在针对这个宏中出现的语法,提两个问题:
为什么会出现(z)这种语法,不加括号可以吗?
- 针对第一个问题,是出于安全性的考虑,看下面一个例子:
#define X(a, b) \ a = b * 3;
- 如果我们这样调用宏:X(a, 1+2);那么宏展开的结果为 a = 1 + 2 3 ,即a = 7 ,而我们预期的结果是a = (1+2) 3 = 9,出现了运算符优先级的问题,所以当传入的参数是一个表达式的时候,不加括号会出现 运算符优先级 不符合预期的问题,所以加上括号能够更加安全
- 下面看第二个为什么要加上do-while(0)的问题,扩展一下上面的宏X:
#define X(a, b) \ a = b * 3; a = a + 1;
- 那么我们如果在代码中编写:
if (true) X(a, b)
- 那么它的效果等同于:
if (true) a = b * 3; a = a +1;
- 可以看到,如果if不加{}大括号的话, *只会执行第一条语句a = b 3*,并不符合预期
- 如果加上do{}while(0),其效果等同于:
if (true) do { a = b * 3; a = a +1; } while(0)
- 由于do-while语句的存在,两个表达式变成了 一个整体 ,所以宏体中两个表达式都会被执行。所以,这样做能够保证 宏体里所有语句都会被完整的执行 。
- 目前为止,变量$a和表达式1的信息,均已存储在两个不同的znode中,均已编译完成,下面就开始生成指令了:
指令的生成
- 接下来回到外部zend_compile_assign函数,继续往下执行zend_emit_op(result, ZEND_ASSIGN, &var_node, &expr_node);这个函数,它负责整合之前编译过程中的中间结果,并生成相应的指令:
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; }
- 首先观察传递进去的参数,注意这里的result并非之前给result赋值的那个result(那个result是var_node和expr_node这两个临时znode),这个result在之前外层的default分支声明还未赋值,为NULL。第二个就是指令所要执行的操作opcode,即ASSIGN;第三个参数和第四个参数是操作数,我们这里就是$a和1,也将其存储编译期间信息的znode传递进去
- 有了操作数$a、指令操作(ASSIGN)、操作数(1),我们现在就可以生成指令了
- 一条指令是一个opline,且opline是存储在zend_op_array上的。那么我们首先在zend_op_array上分配一个opline出来用于存储即将生成的指令。随后将opcode的信息也赋值到这个opline上去。那么现在opline上只有一个opcode,还没有操作数$a和操作数(1)的信息,也没有result的信息。因为op1不是空,调用SET_NODE宏:
#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)
- 从SET_NODE(opline->op1, op1)的两个参数可以看出,它将操作数$a的信息拷贝到opline上
- op2也不为空,也与op1同理,将值1的znode信息拷贝到opline上
- 下面zend_check_live_ranges这个函数先忽略,那么现在还剩下一个返回值没有设置,由于我们这里result的值还是空,所以不进这个if。因为我们$a = 1这个简单的赋值表达式,是没有返回值这一说法的。但是类似$a = 1 + 2;这样的表达式,返回的中间值3的信息可以存在result这个znode中,然后同样拷贝到opline上,这个时候才会用到result。这个函数的细节不再展开
- 最后全部编译完之后,看下这几个znode中的值:
- 由于CV型变量是存储在zend_execute_data栈桢上的,故图中的80即是$a在执行栈桢上的偏移量,通过计算能够找到$a。1是等号右侧表达式1的值,而result中的值没有意义,在这里没有使用result存储中间结果
- 具体图解请参考如下两篇参考文献
参考资料
以上所述就是小编给大家介绍的《【PHP源码学习】2019-03-22 AST的遍历-1》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 数组常见的遍历循环方法、数组的循环遍历的效率对比
- C++拾趣——STL容器的插入、删除、遍历和查找操作性能对比(Windows VirtualStudio)——遍历和删除
- Js遍历数组总结
- 遍历
- 遍历 DOM 注意点
- MySQL 实现树形的遍历
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
XML 基础教程
(美)雅可布斯 / 许劲松 等 / 人民邮电出版社 / 2007-7 / 49.00元
《XML 基础教程:入门、DOM、Ajax与Flash》全面讲述了XML及其在Web开发领域中的作用,同时介绍了一些特定的XML词汇以及相关的XML推荐标准。书中首先解释了XML并介绍了XML文档的不同组成部分;其次讲解了XML应用程序客户端的处理方法,如何使用CSS和 XSLT对XML文档进行显示和转换,如何使用JavaScript操作XML文档等内容;然后介绍了如何在服务器端处理XML;最后深......一起来看看 《XML 基础教程》 这本书的介绍吧!