资讯专栏INFORMATION COLUMN

【PHP源码学习】抽象语法树编译过程的验证

antyiwei / 2992人阅读

摘要:引入阅读前请先查看源码学习的遍历了解基本概念。此篇文章是针对于进行了实战调试,以验证源码学习的遍历中的一些论证。到此,编译阶段告一段落。参考资料源码分析源码研究之浅谈虚拟机

grape

引入

阅读前请先查看【PHP源码学习】2019-03-22 AST的遍历了解基本概念。

此篇文章是针对于$a=1进行了gdb实战调试,以验证 【PHP源码学习】2019-03-22 AST的遍历 中的一些论证。

实验代码:


基本流程:
-   zend_compile_top_stmt(CG(ast))
-   zend_compile_stmt(ast)
-   zend_compile_expr(&result, ast);
-   zend_compile_assign(result, ast);
-   zend_delayed_compile_begin();
-   zend_delayed_compile_var(&var_node, var_ast, BP_VAR_W);
-   zend_compile_expr(&expr_node, expr_ast);
-   zend_delayed_compile_end(offset);
-   zend_emit_op(result, ZEND_ASSIGN, &var_node, &expr_node);
GDB过程

我们来gdb一下整个过程,首先,在zend_compile_top_stmt入口处打断点:

gdb下来我们进入:

在zend_compile_stmt我们进入default的zend_compile_expr函数:


因为我们是赋值运算,我们此时走到了assign函数,接下来就是整个编译过程中的重点部分:

打印var_ast->kind:

可以看出我们接下来要走的就是上图的几个函数,那么,这几个函数又是怎么走的呢?我们接着看,首先,进入zend_delayed_compile_begin():

这个函数的作用是取栈顶,然后在函数结束后赋值给offest,那我们看看这个offest是什么?

等号左边$a的编译

接下来进入$a的处理函数:

在这里记录下我们处理$a过程中调用的函数:

zend_delayed_compile_var (result=0x7fffffffa580, ast=0x7ffff5e7f060, type=1)
zend_compile_simple_var (result=0x7fffffffa580, ast=0x7ffff5e7f060, type=1, delayed=1)
zend_try_compile_cv (result=0x7fffffffa580, ast=0x7ffff5e7f060)
lookup_cv (op_array=0x7ffff5e75460, name=0x7ffff5e5e4e0)

lookup_cv函数的作用是什么呢?首先我们先看lookcv的源代码:

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);
}

我们发现,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数组,节省了分配编号的时间。
另外,在zend_try_compile_cv这个函数中对于result进行赋值,那么我们打印下result的值:

我们发现,op_type=16(IS_CV),u.op.var=80
到此我们总结一下$a这个过程,核心函数lookupcv,在lookupcv中我们将变量存储在op_array->vars中,并且返回一个int型整数,代表着偏移量。随后在zend_try_compile_cv中将op_type和u.op.var赋值给znode *result,具体编译示例图如下图所示:

到此,$a即左子节点就结束了。

等号右边1的编译

接下来我们来进行右子树的处理,gdb如图:

右子树的处理比较简单,其调用函数为:

zend_compile_expr
ZVAL_COPY(z, v)    

重点函数在于ZVAL_COPY这个宏,首先我们看gdb中到达了这个宏:

然后我们继续分析这个宏的作用,老规矩,先贴源码:

#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)

它的功能是把一个zval(v)拷贝到另外一个zval(z)中,具体的一些分析请查看上一篇文章:【PHP源码学习】2019-03-22 AST的遍历-1
在进行复制完之后我们对于result进行打印:

我们可以看到已经完成了赋值。
最后进行了 result->op_type = IS_CONST,op_type的赋值:

重新打印result即最终的结果:

至此,$a和1都分别存在了两个znode中。下边开始生成指令。

根据assign以及op1,op2生成opline

我们进入到zend_emit_op函数中,这个函数中会生成opcode:

首先我们看这个函数所执行的所有指令:

其中,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)

从代码中可以看出,对于操作数1,会将编译过程中临时的结构znode传递给zend_op中,对于操作数2,因为是常量(IS_CONST),会调用zend_add_literal将其插入到op_array->literals中。
接下来我们进行返回值的设置,此时会调用zend_make_var_result这个函数:

static inline void zend_make_var_result(znode *result, zend_op *opline) /* {{{ */
{
    opline->result_type = IS_VAR; //返回值的类型设置为IS_VAR
    opline->result.var = get_temporary_variable(CG(active_op_array));  //这个是返回值的编号,对应T位置
    GET_NODE(result, opline->result);
}
static uint32_t get_temporary_variable(zend_op_array *op_array) /* {{{ */
{
      return (uint32_t)op_array->T++;
}

返回值的类型为IS_VAR,result.var为T的值
最后打印opline看一下最终的结果:

,下面我们给出Assign操作对应的指令图,如图所示:

从图中可以看出,生成的opline中opcode等于38;op1的类型为IS_CV,op1.var对应的是vm_stack上的偏移量;op2的类型为IS_CONST,op2.constant对应的是op_array中literals数组的下标;result的类型为IS_VAR,result.var对应的是T的值;此时handler的值为空。
到此,编译阶段告一段落。

参考资料:

【PHP7源码分析】PHP7源码研究之浅谈Zend虚拟机

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/31488.html

相关文章

  • PHP7源码分析】PHP7语言执行原理

    摘要:在中,源代码首先将进行词法分析,将源代码切割为多个字符串单元,分割后的字符串称之为。图以为例解释型语言的执行示意图第步源码通过词法分析得到第步基于语法分析器生成抽象语法树第步抽象语法树转换为指令集合,解释执行。 顺风车运营研发团队 李志 发表在程序人生 公众号我们常用的高级语言有很多种,比较出名的有CC++、Python、 PHP、Go、Pascal等。而这些语言根据运行的方式不同,...

    VEIGHTZ 评论0 收藏0
  • PHP7源码分析】PHP7源码研究之浅谈Zend虚拟机

    摘要:中词法语法分析,生成抽象语法树,然后编译成及被执行均由虚拟机完成。通常情况下这部分是可选部分,主要为便于程序的读写方便而使用。指令虚拟机的指令称为,每条指令对应一个。 作者 陈雷编程语言的虚拟机是一种可以运行中间语言的程序。中间语言是抽象出的指令集,由原生语言编译而成,作为虚拟机执行阶段的输入。很多语言都实现了自己的虚拟机,比如Java、C#和Lua。PHP语言也有自己的虚拟机,称为Z...

    马龙驹 评论0 收藏0
  • 【LNMPR源码学习】笔记汇总

    摘要:此文用于汇总跟随陈雷老师及团队的视频,学习源码过程中的思考整理与心得体会,此文会不断更新视频传送门每日学习记录使用录像设备记录每天的学习源码学习源码学习内存管理笔记源码学习内存管理笔记源码学习内存管理笔记源码学习基本变量笔记 此文用于汇总跟随陈雷老师及团队的视频,学习源码过程中的思考、整理与心得体会,此文会不断更新 视频传送门:【每日学习记录】使用录像设备记录每天的学习 PHP7...

    Barrior 评论0 收藏0

发表评论

0条评论

antyiwei

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<