baiyan
全部视频:https://segmentfault.com/a/11...
原视频地址:http://replay.xesv5.com/ll/24...
引入
- 先看上一节笔记中展示的AST示例:
<?php
$a = 1;
$b = $a + 2;
- 在PHP中,构造出来的抽象语法树如图所示:
- 那么,这个AST后面能够用来做什么呢?因为我们最终需要执行这段PHP代码,所以需要将其转化为可执行的指令,让虚拟机最终来解释执行
- 指令的几个要素:
操作数:参与指令操作的变量或常量等,只需OP1和OP2最多两个操作数就够了,因为多元运算可以转化成二元运算(op1/op2)
指令操作:用来描述具体的赋值/加减乘除等指令操作(opcode)
返回值:用来存储中间运算结果(result)
处理函数:用来具体实现加减乘除等指令的操作逻辑(handler)
- 这些指令要素是我们自己定义的,而寄存器是无法理解这些自定义指令的,这就需要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; //下面oplines数组大小
zend_op *opcodes; //oplines数组,存放所有指令
int last_var;//操作数类型为IS_CV的个数
uint32_t T;//操作数类型为IS_VAR和IS_TMP_VAR的个数之和
zend_string **vars;//存放IS_CV类型操作数的数组
...
int last_literal;//下面常量数组大小
zval *literals;//存放IS_CONST类型操作数的数组
};
- 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/临时变量这些都是分配在栈上的(后面会讲)。通过计算,我们才能得出最终操作数在栈桢中的位置
- 在PHP7中,操作数有5种类型可选,如下:
#define IS_CONST (1<<0)
#define IS_TMP_VAR (1<<1)
#define IS_VAR (1<<2)
#define IS_UNUSED (1<<3) /* Unused variable */
#define IS_CV (1<<4) /* Compiled variable */
IS_CONST类型:值为1,表示常量,如$a = 1中的1或者$a = "hello world"中的hello world
IS_TMP_VAR类型:值为2,表示临时变量,如$a=”123”.time(); 这里拼接的临时变量”123”.time()的类型就是IS_TMP_VAR,一般用于操作的中间结果
IS_VAR类型:值为4,表示变量,但是这个变量并不是PHP中常见的声明变量,而是返回的临时变量,如$a = time()中的time()
IS_UNUSED:值为8,表示没有使用的操作数
IS_CV:值为16,表示形如$a这样的变量
- 既然如何我们知道了如何存储指令,那么下面讲一下如何遍历这棵抽象语法树,来得到这些指令集:
遍历抽象语法树
编译根节点(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; /* 存储变量的值 */
union {
struct {
ZEND_ENDIAN_LOHI_4( //大小端问题,详情看"PHP内存管理3笔记”
zend_uchar type, //注意这里就是存放变量类型的地方,char类型
zend_uchar type_flags, //类型标记
zend_uchar const_flags, //是否是常量
zend_uchar reserved) //保留字段
} v;
uint32_t type_info;
} u1;
union {
uint32_t next; /* 数组模拟链表,链地址法解决哈希冲突时使用 */
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)这种语法,不加括号可以吗?
为什么要do{}while(0),反正都是只执行一次这个宏的代码,可以去掉do{}while(0)吗?
- 针对第一个问题,是出于安全性的考虑,看下面一个例子:
#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存储中间结果
- 具体图解请参考如下两篇参考文献
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。