【PHP源码学习】2019-03-28 Zend虚拟机

栏目: PHP · 发布时间: 5年前

内容简介:baiyan全部视频:原视频地址:

baiyan

全部视频: https://segmentfault.com/a/11...

原视频地址: http://replay.xesv5.com/ll/24...

复习

基本概念

  • 首先复习几个基本概念:

    opline:在zend虚拟机中,每条指令都是一个 opline ,每个opline由操作数、指令操作、返回值组成

    opcode:每个指令操作都对应一个 opcode (如ZEND_ASSIGN/ZEND_ADD等等),在 PHP 7中,有100多种指令操作,所有的指令集被称作opcodes

    handler:每个opcode指令操作都对应一个 handler 指令处理函数,处理函数中有具体的指令操作执行逻辑

  • 我们知道,在经过编译阶段(zend_compile函数)中,我们生成AST并对其遍历,生成一条条指令,每一条指令都是一个opline。之后通过pass_two函数生成了这些指令所对应的handler,这些信息均存在op_array中。既然指令和handler已经生成完毕,接下来的任务就是要交给zend虚拟机,加载这些指令,并最终执行对应的handler逻辑。
  • 指令在PHP7中,由以下元素构成:
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; //返回值的类型

};
  • 在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这样的变量

  • 对AST进行遍历之后,最终存放所有指令集(oplines)的地方为op_array:
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类型操作数的数组

};

op_array的存储情况

  • 为了复习op_array的存储情况,我们具体gdb一下,使用下面的测试用例:
<?php
$a = 2;
  • 根据以上测试用例,在zend_execute处打一个断点,这里完成了对AST的遍历并生成了最终的op_array,已经进入到虚拟机执行指令的入口。首先我们先观察传入的参数op_array,它是经过AST遍历之后生成的最终的op_array:

【PHP源码学习】2019-03-28 Zend虚拟机

  • last = 2;表示一共有两个opcodes:一个是赋值ASSIGN,另一个是脚本为我们自动生成的返回语句return 1,opcodes是一个数组,每个数组单元具体存储了每条指令的信息(操作数、返回值等等),我们打印一下数组的内容:

【PHP源码学习】2019-03-28 Zend虚拟机

  • last_var = 1;表示有一个CV类型的变量,这里就是$a
  • T = 1;表示IS_TMP_VAR和IS_VAR变量类型的数量之和,而我们脚本中并没有这样的变量,它是在存储中间的返回值的时候,这个返回值类型就是一个IS_VAR类型,所以T的值一开始就为1
  • vars是一个二级指针,可以理解为外层的一级指针首先指向一个数组,这个数组里每个存储单元都是一个zend_string*类型的指针,而每个指针都指向了一个zend_string结构体,我们打印数组第一个单元的值,发现其指向的zend_string值为a:

【PHP源码学习】2019-03-28 Zend虚拟机

  • last_literal = 2;表示脚本中一共有2个常量,一个是我们自己复制的值2,另一个是脚本为我们自动生成的返回语句return 1中的值1:
  • literals是一个zend_array,里面每一个单元都是一个zval,存储这些常量的实际的值,我们可以看到,其值为2和1,与上面的描述相符:

【PHP源码学习】2019-03-28 Zend虚拟机

  • 我们可以画出最终的op_array存储结构图:

【PHP源码学习】2019-03-28 Zend虚拟机

  • 这样一来,我们就可以清晰地看出指令在op_array中是如何存储的。那么接下来,我们需要将其加载到虚拟机的执行栈桢上,来最终执行这些指令。

在虚拟机上执行指令

  • 下面让我们真正执行op_array中的指令,执行指令的入口为zend_execute函数,传入参数为op_array以及一个zval指针:
ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value)
{
    zend_execute_data *execute_data;

    if (EG(exception) != NULL) {
        return;
    }

    execute_data = zend_vm_stack_push_call_frame(ZEND_CALL_TOP_CODE | ZEND_CALL_HAS_SYMBOL_TABLE,
        (zend_function*)op_array, 0, zend_get_called_scope(EG(current_execute_data)), zend_get_this_object(EG(current_execute_data)));
    if (EG(current_execute_data)) {
        execute_data->symbol_table = zend_rebuild_symbol_table();
    } else {
        execute_data->symbol_table = &EG(symbol_table);
    }
    EX(prev_execute_data) = EG(current_execute_data);
    i_init_code_execute_data(execute_data, op_array, return_value);
    zend_execute_ex(execute_data);
    zend_vm_stack_free_call_frame(execute_data);
}
  • 观察第一行,声明了一个zend_execute_data类型的指针,这个类型非常重要,存储了虚拟机执行指令时的基本信息:
struct _zend_execute_data {
    const zend_op       *opline;          //当前执行的指令 8B
    zend_execute_data   *call;           //指向自己的指针 8B
    zval                *return_value;         //存储返回值 8B
    zend_function       *func;              //执行的函数 8B
    zval                 This;             /* this + call_info + num_args   16B */
    zend_execute_data   *prev_execute_data; //链表,指向前一个zend_execute_data 8B
    zend_array          *symbol_table;  //符号表 8B
#if ZEND_EX_USE_RUN_TIME_CACHE
    void               **run_time_cache;   /* cache op_array->run_time_cache  8B*/
#endif
#if ZEND_EX_USE_LITERALS
    zval                *literals;         /* cache op_array->literals     8B */
#endif
};
  • 可以看到,这个zend_execute_data一共是80个字节
  • 随后执行zend_vm_stack_push_call_frame(ZEND_CALL_TOP_CODE | ZEND_CALL_HAS_SYMBOL_TABLE,(zend_function*)op_array, 0, zend_get_called_scope(EG(current_execute_data)), zend_get_this_object(EG(current_execute_data)));这个函数,我们s进去看下:
static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame(uint32_t call_info, zend_function *func, uint32_t num_args, zend_class_entry *called_scope, zend_object *object)
{
    uint32_t used_stack = zend_vm_calc_used_stack(num_args, func);

    return zend_vm_stack_push_call_frame_ex(used_stack, call_info,
        func, num_args, called_scope, object);
}
  • 先不看复杂的函数参数,直接看zend_vm_calc_used_stack(num_args, func);这个函数调用,它用来计算虚拟机在执行栈桢上所用的空间,此时应该没有占用任何空间,我们打印一下used_stack:

【PHP源码学习】2019-03-28 Zend虚拟机

  • 发现这里的used_stack果然是0,然后进入下一个if中,继续执行used_stack += func->op_array.last_var + func->op_array.T - MIN(func->op_array.num_args, num_args);这个与函数相关,我们还没有讲,那么我们直接看这个函数外层返回的used_stack值,为112B:

【PHP源码学习】2019-03-28 Zend虚拟机

  • 那么继续往下执行zend_vm_stack_push_call_frame_ex(used_stack, call_info,func, num_args, called_scope, object):
static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame_ex(uint32_t used_stack, uint32_t call_info, zend_function *func, uint32_t num_args, zend_class_entry *called_scope, zend_object *object)
{
    zend_execute_data *call = (zend_execute_data*)EG(vm_stack_top);

    ZEND_ASSERT_VM_STACK_GLOBAL;

    if (UNEXPECTED(used_stack > (size_t)(((char*)EG(vm_stack_end)) - (char*)call))) {
        call = (zend_execute_data*)zend_vm_stack_extend(used_stack);
        ZEND_ASSERT_VM_STACK_GLOBAL;
        zend_vm_init_call_frame(call, call_info | ZEND_CALL_ALLOCATED, func, num_args, called_scope, object);
        return call;
    } else {
        EG(vm_stack_top) = (zval*)((char*)call + used_stack);
        zend_vm_init_call_frame(call, call_info, func, num_args, called_scope, object);
        return call;
    }
}
  • 同样忽略复杂的函数参数,只关注传入的used_stack = 112即可。我们首先看第一行:把executor_globals中的vm_stack_top字段赋值给当前的zend_execute_data指向自己的指针,说明zend_execute_data的起始地址为EG这个宏的返回值,查看这个值:

【PHP源码学习】2019-03-28 Zend虚拟机

  • 可以看到,zend_execute_data的起始地址为0x7ffff5e1c030,继续往下执行代码:

【PHP源码学习】2019-03-28 Zend虚拟机

  • 下面的if是用来判断栈上是否有足够的空间,如果已经使用的栈空间太多,那么需要重新分配栈空间,显然我们这里没有进这个if,说明栈空间还是够的,那么执行下面的else。重点在于:
EG(vm_stack_top) = (zval*)((char*)call + used_stack);
  • 现在这个栈顶的位置变成了0x7ffff5e1c0a0,也就是0x7ffff5e1c030 + 112的结果。至于指针加法步长的运算,本质上就是 地址a + 步长 * sizeof(地址类型) (地址类型如果是char *,步长就是1;如果是Int *,步长就是4),举例子:
int *p;
p+3;
  • 假如p的地址是0x7ffff5e1c030,那么p+3的结果就应该是0x7ffff5e1c030 + 3 * sizeof(int) = 0x7ffff5e1c03c
  • 我们画出此时栈上的结构图:

【PHP源码学习】2019-03-28 Zend虚拟机

  • 此时这个返回值call就是栈顶的位置,但是top指针并不指向栈顶,而是指向栈的中间:

【PHP源码学习】2019-03-28 Zend虚拟机

  • 接下来回到最外层的zend_execute函数,继续往下执行:

【PHP源码学习】2019-03-28 Zend虚拟机

  • 可以看到,接下来将符号表中的内容赋值给了execute_data中的symbol_table字段,这个符号表是一个zend_array,此时还只有几个默认的_GET这几个预先添加的符号,并没有我们自己的$a:

【PHP源码学习】2019-03-28 Zend虚拟机

  • 那么我们继续往下走,关注i_init_code_execute_data()函数:
static zend_always_inline void i_init_code_execute_data(zend_execute_data *execute_data, zend_op_array *op_array, zval *return_value) /* {{{ */
{
    ZEND_ASSERT(EX(func) == (zend_function*)op_array);

    EX(opline) = op_array->opcodes;
    EX(call) = NULL;
    EX(return_value) = return_value;

    zend_attach_symbol_table(execute_data);

    if (!op_array->run_time_cache) {
        op_array->run_time_cache = emalloc(op_array->cache_size);
        memset(op_array->run_time_cache, 0, op_array->cache_size);
    }
    EX_LOAD_RUN_TIME_CACHE(op_array);
    EX_LOAD_LITERALS(op_array);

    EG(current_execute_data) = execute_data;
}
  • 这里的EX宏对应全局变量execute_data,EG宏对应全局变量executor_globals,要区分开
  • 重点关注zend_attach_symbol_table(execute_data)函数:
ZEND_API void zend_attach_symbol_table(zend_execute_data *execute_data) /* {{{ */
{
    zend_op_array *op_array = &execute_data->func->op_array;
    HashTable *ht = execute_data->symbol_table;

    /* copy real values from symbol table into CV slots and create
       INDIRECT references to CV in symbol table  */
     // 从符号表中拷贝真实的值到CV槽中,并且创建对符号表中CV变量的间接引用
    if (EXPECTED(op_array->last_var)) {
        zend_string **str = op_array->vars;
        zend_string **end = str + op_array->last_var;
        zval *var = EX_VAR_NUM(0);

        do {
            zval *zv = zend_hash_find(ht, *str);

            if (zv) {
                if (Z_TYPE_P(zv) == IS_INDIRECT) {
                    zval *val = Z_INDIRECT_P(zv);

                    ZVAL_COPY_VALUE(var, val);
                } else {
                    ZVAL_COPY_VALUE(var, zv);
                }
            } else {
                ZVAL_UNDEF(var);
                zv = zend_hash_add_new(ht, *str, var);
            }
            ZVAL_INDIRECT(zv, var);
            str++;
            var++;
        } while (str != end);
    }
}
  • 我们此时的符号表只包含_GET这类默认初始化的变量,并不包含我们自己的$a。首先进入if,因为last_var = 1($a),所以将str和end赋值,他们分别指向vars和vars后面1偏移量的位置,如图:

【PHP源码学习】2019-03-28 Zend虚拟机

  • 接下来在符号表ht中遍历,查找是否有$a这个CV型变量,现在肯定是没有的,所以进入else分支,执行ZVAL_UNDEF(var)与zv = zend_hash_add_new(ht, *str, var);

【PHP源码学习】2019-03-28 Zend虚拟机

  • 上面 EX_VAR_NUM(0)这个宏是一个申请一个CV槽大小的空间,但是在这里我们没有使用,所以ZVAL_UNDEF(var)将这个槽中的zval类型置为IS_UNDEF类型,然后通过zend_hash_add_new将$a加入到符号表这个zend_array中。那么如果下一次再引用$a的时候,就会走上面的if分支,这样CV槽就有了用武之地。把$a拷贝到CV槽中,那么在符号表中通过间接引用找到它即可,就不用多次将其加入到符号表中,节省时间与空间。最后将str与var指针的位置往后挪,说明本次遍历完成
  • 回到i_init_code_execute_data函数,下面几行是用来操作运行时缓存的代码,我们暂时跳过,回到zend_execute主函数,接下来会调用zend_execute()函数,在这里真正执行指令所对应的handler逻辑:

【PHP源码学习】2019-03-28 Zend虚拟机

  • 赋值操作对应的是ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,我们看看这个handler里具体做了什么:
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();
        //从literals数组中获取op2对应的值,也就是值2
      value = EX_CONSTANT(opline->op2);
        //在execute_data的符号表中获取op1的位置,也就是$a
      variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var);
      ...
       //最终将1赋值给$a
      value = zend_assign_to_variable(variable_ptr, value, IS_CONST);
      ...
}
  • 这样,一个赋值指令就被虚拟机执行完毕,那么还有一个return 1默认的脚本返回值的指令,也是同理,这里不再展开,那么最终的虚拟机执行栈桢的情况如下:

【PHP源码学习】2019-03-28 Zend虚拟机

  • 回到zend_execute主函数,最后调用了zend_vm_stack_free_call_frame(execute_data)函数,最终释放虚拟机占用的栈空间,完毕。

参考资料


以上所述就是小编给大家介绍的《【PHP源码学习】2019-03-28 Zend虚拟机》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Is Parallel Programming Hard, And, If So, What Can You Do About

Is Parallel Programming Hard, And, If So, What Can You Do About

Paul E. McKenney

The purpose of this book is to help you understand how to program shared-memory parallel machines without risking your sanity.1 By describing the algorithms and designs that have worked well in the pa......一起来看看 《Is Parallel Programming Hard, And, If So, What Can You Do About 》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

URL 编码/解码
URL 编码/解码

URL 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具