威尼斯手机平台登陆-官方网站登录

威尼斯手机平台登陆为您带来世界三大博彩公司最新相关资讯,威尼斯官方网站登录充分考虑到不同地域网民的不同需求,威尼斯手机平台登陆良好的用户界面,人性化的操作,实用的功能设计使其广泛受到欢迎,推动实体出版、影视、动漫、游戏等相关文化产业的发展。

您的位置:威尼斯手机平台登陆 > 最新留言 > 威尼斯正规官网VM)是Fackbook推出用于在执行PHP代码的虚拟机,PHP的opcode就是Zend虚拟机中的指令

威尼斯正规官网VM)是Fackbook推出用于在执行PHP代码的虚拟机,PHP的opcode就是Zend虚拟机中的指令

发布时间:2020-04-21 16:06编辑:最新留言浏览(148)

    PHP 是一门解释型的语言。诸如 Java、Python、Ruby、Javascript 等解释型语言,我们编写的代码不会被编译成机器码运行,而是会被编译中间码运行在虚拟机(VM)上。运行 PHP 的虚拟机,称之为 Zend 虚拟机,今天我们将深入内核,探究 Zend 虚拟机运行的原理。

    opcode是计算机指令中的一部分,用于指定要执行的操作, 指令的格式和规范由处理器的指令规范指定。 除了指令本身以外通常还有指令所需要的操作数,可能有的指令不需要显式的操作数。 这些操作数可能是寄存器中的值,堆栈中的值,某块内存的值或者IO端口中的值等等。

    HHVM

    HHVM是什么?

    HHVM(HipHop VM)是Fackbook推出用于在执行PHP代码的虚拟机,是一个PHP的JIT编译器,具有产生快速代码和及时编译的优点。

    HHVM能干什么?

    HHVM脚本主要应用服务器端脚本和命令行脚本两大领域,专注于服务器端脚本,如收集表单数据、生成动态页面、发送接受COOKIE等。

    HHVM为什么比ZendEngine快?

    HHVM是Facebook开发的高性能PHP虚拟机,宣称比官方Zend快9倍。

    PHP使用的Zend虚拟机(VM),首先会先将PHP代码编译成二进制指令opcode,然后逐条执行,每条opcode指令都对应一个C函数。对于PHP的用户函数、运行时局部变量、常量会存在一个Hashtable中。

    执行一次C函数的开销

    • 参数的入栈出栈
    • CPU寄存器状态保存

    例如:在PHP中执行1000w次累加

    <?php
    $sum = 0;
    // 发生1000w次C函数调用
    for($i=0; $i<10000000; $i++){
      $sum += $i;
    }
    

    若编译为机器码情况是什么样的呢?

    主频2.0GHZ的CPU每秒执行20亿次指令,函数调用则1秒只能运行1000W次。

    因此,编译为机器码执行语言如C、C++、Golang...,或拥有JIT的语言如Java、NodeJS、LuaJIT、HHVM...,单从指令执行角度上看至少比PHP快几十倍。

    对于字符串处理、JSON编码解码、iconv编码解码、数组操作等, PHP比C++、Java慢吗?

    在PHP中此类操作都是C扩展函数完成的,性能与编译型语言一致。

    PHP到底比编译型语言慢的原因在哪里呢?

    PHP代码中用户函数、类、对象操作等。

    威尼斯正规官网,运算密集型 vs IO密集型

    运算密集型程序指的是需大量执行内存复制操作、循环、运行指令等,瓶颈在CPU上,提升性能的解决方案就是提升CPU硬件配置、改进算法、提升语言/工具的执行性能。对于此类程序,PHP性能问题很明显,执行相同的逻辑,比C/C++慢几十倍甚至百倍,这是不可接受的。

    IO密集型程序瓶颈在IO等待,例如HTTP请求执行100ms后返回,其中90ms查询数据库,8ms读写文件, 那么无论C/C++还是PHP,请求响应时间总是100ms左右,语言性能优化只有2ms的空间。

    如何优化PHP呢

    • PHP语言层面优化
    • 优化PHP官方实现ZendEngine
    • 将PHP编译为其他语言字节码(bytecode),借助于其他语言虚拟机来运行。
    • 将PHP转成C/C++,编译成本地代码。
    • 开发更快的PHP虚拟机

    Zend的执行过程可分为两个环节

    • 将PHP编译为opcode
    • 执行opcode

    优化opcode可编码重复解析PHP与静态编译优化,由于PHP的动态性,这种优化方式是有局限,乐观估计可提升20%的性能。

    优化opcode架构本身,工作量大投入产生比不高。

    优化opcode执行,Zend解释器interpreter在读到opcode后会根据不同opcode调用不同函数(switch),在函数中执行语言相关的操作。优化空间不大。

    优化Zend执行性能,对于函数调用的开销,通过inline threading来优化,其原理如C中的inline关键字。

    更快的虚拟机

    HHVM 为什么更快,原因是JIT。

    JIT操作本身是耗时的,对于简单程序或许比interpreter慢。HHVM的发展就是不断优化、优化、在优化。

    威尼斯正规官网 1

    HHVM是如何超过HPHPc

    什么是JIT,如何实现一个JIT?

    动态语言中基本都会有一个eval(),作用是传入一段字符串来执行。JIT做着类似的事,不过它要拼接的不是字符串,而是不同平台下的机器码,然后执行。在JIT中更重要的优化是根据类型来生成特定的指令,从而减少指令数量和条件判断。

    类型推导

    JIT的关键是猜测类型,变量的类型要是老是变就很难优化。HHVM工程师考虑在PHP语法上做手脚,加上类型的支持,推出Hack。

    <?hh
    class Point
    {
      // 使用静态类型可让HHVM更好的优化性能,不过这也意味着和PHP语法不兼容。
      public float $x,$y;
      public function __construct(float $x, float $y)
      {
        $this->x = $x;
        $this->y = $y;
      }
    }
    

    HHVM提升PHP执行性能

    HHVM生成和执行PHP的在中间字节码,执行时通过JIT(Just In Time即时编译,软件优化技术,指在运行时才会去编译字节码为机器码)转换为机器码执行。JIT将大量重复执行的字节码在运行时编译为机器码,达到提高执行效率的目的。通常触发JIT的条件是代码或函数被多次重复调用。

    什么是字节码?

    威尼斯正规官网 2

    字节码

    ZendEngine做法是先编译为opcode,逐条执行,每条指令对应的是C语言级别的函数。

    HHVM服务器最开始的少数请求会比其余的慢,因为它必须在执行PHP和Hack代码之前将它们编译成机器码,这个效果是非常明显的,所以你不应当立即把一个新设置的HHVM服务器应用到生产环境中。你应该先发送一些人工模拟的请求到这个HHVM服务器上,对它进行热身。
    事实上,服务器启动的时候,并不会编译任何代码。初始的请求就是在HHVM的字节码解释器下运行的。原理就是:对于一个web服务器来说,最初的几个请求是不寻常的。在这个期间,开始了初始化,还对缓存进行填充等等。对这些代码路径的编译对整体性能的表现是非常糟糕的,因为一旦对服务器进行了预热,这些过程是不会被经常调用的。HHVM还利用这些请求,来收集一些代码所用到的数据类型分析的工作。所以它可以稍后更加有效地进行编译。你可以使用选项 hhvm.jit_profile_interp_requests 来调整这个门槛。
    对于发送预热请求,颗通过命令行或其它类似的地方,简单地使用 curl 这个命令功能。为了得到最好的结果:
    使用你希望在产品中看到的,能够代表最常见的请求的混合集合。例如,如果你期待所有对这个产品的请求中的40%都是到达 index.php 的,那么你的 40% 的预热请求都 应该是到 index.php 的请求。
    避免并行发送多个预热请求,若你真的并行发送了多个请求,那么并不会出现什么问题。单对于JIT编译器来说,若没有同时工作在多个请求上的话,它往往能够生成更好的代码。
    最终,你最好有个进程脚本用于服务器热身,这样的话,颗在命令行里仅仅执行一个命令就可以做到热身了。但是在最初期的时候,你还需要一些人工的参与,要实际计算出用于热身的请求数量是非常微妙的, 这主要取决于你的程序本身。

    OPCODE

    什么是 OPCODE?它是一种虚拟机能够识别并处理的指令。Zend 虚拟机包含了一系列的 OPCODE,通过 OPCODE 虚拟机能够做很多事情,列举几个 OPCODE 的例子:

    • ZEND_ADD 将两个操作数相加。
    • ZEND_NEW 创建一个 PHP 对象。
    • ZEND_ECHO 将内容输出到标准输出中。
    • ZEND_EXIT 退出 PHP。

    诸如此类的操作,PHP 定义了186个(随着 PHP 的更新,肯定会支持更多种类的 OPCODE),所有的 OPCODE 的定义和实现都可以在源码的 zend/zend_vm_def.h 文件(这个文件的内容并不是原生的 C 代码,而是一个模板,后面会说明原因)中查阅到。

    我们来看下 PHP 是如何设计 OPCODE 数据结构:

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

    仔细观察 OPCODE 的数据结构,是不是能找到汇编语言的感觉。每一个 OPCODE 都包含两个操作数,op1和 op2handler 指针则指向了执行该 OPCODE 操作的函数,函数处理后的结果,会被保存在 result 中。

    我们举一个简单的例子:

    <?php
    $b = 1;
    $a = $b + 2;
    

    我们通过 vld 扩展看到,经过编译的后,上面的代码生成了 ZEND_ADD 指令的 OPCODE。

    compiled vars:  !0 = $b, !1 = $a
    line     #* E I O op                           fetch          ext  return  operands
    -------------------------------------------------------------------------------------
       2     0  E >   ASSIGN                                                   !0, 1
       3     1        ADD                                              ~3      !0, 2
             2        ASSIGN                                                   !1, ~3
       8     3      > RETURN                                                   1
    

    其中,第二行是 ZEND_ADD 指令的 OPCODE。我们看到,它接收2个操作数,op1 是变量 $bop2 是数字常量1,返回的结果存入了临时变量中。在 zend/zend_vm_def.h 文件中,我们可以找到 ZEND_ADD 指令对应的函数实现:

    ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMPVAR|CV, CONST|TMPVAR|CV)
    {
        USE_OPLINE
        zend_free_op free_op1, free_op2;
        zval *op1, *op2, *result;
    
        op1 = GET_OP1_ZVAL_PTR_UNDEF(BP_VAR_R);
        op2 = GET_OP2_ZVAL_PTR_UNDEF(BP_VAR_R);
        if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_LONG)) {
            if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_LONG)) {
                result = EX_VAR(opline->result.var);
                fast_long_add_function(result, op1, op2);
                ZEND_VM_NEXT_OPCODE();
            } else if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_DOUBLE)) {
                result = EX_VAR(opline->result.var);
                ZVAL_DOUBLE(result, ((double)Z_LVAL_P(op1)) + Z_DVAL_P(op2));
                ZEND_VM_NEXT_OPCODE();
            }
        } else if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_DOUBLE)) {
    
        ...
    }
    

    上面的代码并不是原生的 C 代码,而是一种模板。

    为什么这样做?因为 PHP 是弱类型语言,而其实现的 C 则是强类型语言。弱类型语言支持自动类型匹配,而自动类型匹配的实现方式,就像上述代码一样,通过判断来处理不同类型的参数。试想一下,如果每一个 OPCODE 处理的时候都需要判断传入的参数类型,那么性能势必成为极大的问题(一次请求需要处理的 OPCODE 可能能达到成千上万个)。

    哪有什么办法吗?我们发现在编译的时候,已经能够确定每个操作数的类型(可能是常量还是变量)。所以,PHP 真正执行时的 C 代码,不同类型操作数将分成不同的函数,供虚拟机直接调用。这部分代码放在了 zend/zend_vm_execute.h 中,展开后的文件相当大,而且我们注意到还有这样的代码:

    if (IS_CONST == IS_CV) {
    

    完全没有什么意义是吧?不过没有关系,C 的编译器会自动优化这样判断。大多数情况,我们希望了解某个 OPCODE 处理的逻辑,还是通过阅读模板文件 zend/zend_vm_def.h 比较容易。顺便说一下,根据模板生成 C 代码的程序就是用 PHP 实现的。

    通常opcode还有另一种称谓:字节码。 例如Java虚拟机,.NET的通用中间语言(CIL: Common Intermeditate Language)等等。

    执行过程

    准确的来说,PHP 的执行分成了两大部分:编译和执行。这里我将不会详细展开编译的部分,而是把焦点放在执行的过程。

    通过语法、词法分析等一系列的编译过程后,我们得到了一个名为 OPArray 的数据,其结构如下:

    struct _zend_op_array {
        /* Common elements */
        zend_uchar type;
        zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
        uint32_t fn_flags;
        zend_string *function_name;
        zend_class_entry *scope;
        zend_function *prototype;
        uint32_t num_args;
        uint32_t required_num_args;
        zend_arg_info *arg_info;
        /* END of common elements */
    
        uint32_t *refcount;
    
        uint32_t last;
        zend_op *opcodes;
    
        int last_var;
        uint32_t T;
        zend_string **vars;
    
        int last_live_range;
        int last_try_catch;
        zend_live_range *live_range;
        zend_try_catch_element *try_catch_array;
    
        /* static variables support */
        HashTable *static_variables;
    
        zend_string *filename;
        uint32_t line_start;
        uint32_t line_end;
        zend_string *doc_comment;
        uint32_t early_binding; /* the linked list of delayed declarations */
    
        int last_literal;
        zval *literals;
    
        int  cache_size;
        void **run_time_cache;
    
        void *reserved[ZEND_MAX_RESERVED_RESOURCES];
    };
    

    内容超多对吧?简单的理解,其本质就是一个 OPCODE 数组外加执行过程中所需要的环境数据的集合。介绍几个相对来说比较重要的字段:

    • opcodes 存放 OPCODE 的数组。
    • filename 当前执行的脚本的文件名。
    • function_name 当前执行的方法名称。
    • static_variables 静态变量列表。
    • last_try_catch try_catch_array 当前上下文中,如果出现异常 try-catch-finally 跳转所需的信息。
    • literals 所有诸如字符串 foo 或者数字23,这样的常量字面量集合。

    为什么需要生成这样庞大的数据?因为编译时期生成的信息越多,执行时期所需要的时间就越少。

    接下来,我们看下 PHP 是如何执行 OPCODE。OPCODE 的执行被放在一个大循环中,这个循环位于 zend/zend_vm_execute.h 中的 execute_ex 函数:

    ZEND_API void execute_ex(zend_execute_data *ex)
    {
        DCL_OPLINE
    
        zend_execute_data *execute_data = ex;
    
        LOAD_OPLINE();
        ZEND_VM_LOOP_INTERRUPT_CHECK();
    
        while (1) {
            if (UNEXPECTED((ret = ((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) != 0)) {
                if (EXPECTED(ret > 0)) {
                    execute_data = EG(current_execute_data);
                    ZEND_VM_LOOP_INTERRUPT_CHECK();
                } else {
                    return;
                }
            }
        }
    
        zend_error_noreturn(E_CORE_ERROR, "Arrived at end of main loop which shouldn't happen");
    }
    

    这里,我去掉了一些环境变量判断分支,保留了运行的主流程。可以看到,在一个无限循环中,虚拟机会不断调用 OPCODE 指定的 handler 函数处理指令集,直到某次指令处理的结果 ret 小于0。注意到,在主流程中并没有移动 OPCODE 数组的当前指针,而是把这个过程放到指令执行的具体函数的结尾。所以,我们在大多数 OPCODE 的实现函数的末尾,都能看到调用这个宏:

    ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
    

    在之前那个简单例子中,我们看到 vld 打印出的执行 OPCODE 数组中,最后有一项指令为 ZEND_RETURN 的 OPCODE。但我们编写的 PHP 代码中并没有这样的语句。在编译时期,虚拟机会自动将这个指令加到 OPCODE 数组的结尾。ZEND_RETURN 指令对应的函数会返回 -1,判断执行的结果小于0时,就会退出循环,从而结束程序的运行。

    1. Opcode简介

    方法调用

    如果我们调用一个自定义的函数,虚拟机会如何处理呢?

    <?php
    function foo() {
        echo 'test';
    }
    
    foo();
    

    我们通过 vld 查看生成的 OPCODE。出现了两个 OPCODE 指令执行栈,是因为我们自定义了一个 PHP 函数。在第一个执行栈上,调用自定义函数会执行两个 OPCODE 指令:INIT_FCALL 和 DO_FCALL

    compiled vars:  none
    line     #* E I O op                           fetch          ext  return  operands
    -------------------------------------------------------------------------------------
       2     0  E >   NOP
       6     1        INIT_FCALL                                               'foo'
             2        DO_FCALL                                      0
             3      > RETURN                                                   1
    
    compiled vars:  none
    line     #* E I O op                           fetch          ext  return  operands
    -------------------------------------------------------------------------------------
       3     0  E >   ECHO                                                     'test'
       4     1      > RETURN                                                   null
    

    其中,INIT_FCALL 准备了执行函数时所需要的上下文数据。DO_FCALL 负责执行函数。DO_FCALL 的处理函数根据不同的调用情况处理了大量逻辑,我摘取了其中执行用户定义的函数的逻辑部分:

    ZEND_VM_HANDLER(60, ZEND_DO_FCALL, ANY, ANY, SPEC(RETVAL))
    {
        USE_OPLINE
        zend_execute_data *call = EX(call);
        zend_function *fbc = call->func;
        zend_object *object;
        zval *ret;
    
        ...
    
        if (EXPECTED(fbc->type == ZEND_USER_FUNCTION)) {
            ret = NULL;
            if (RETURN_VALUE_USED(opline)) {
                ret = EX_VAR(opline->result.var);
                ZVAL_NULL(ret);
            }
    
            call->prev_execute_data = execute_data;
            i_init_func_execute_data(call, &fbc->op_array, ret);
    
            if (EXPECTED(zend_execute_ex == execute_ex)) {
                ZEND_VM_ENTER();
            } else {
                ZEND_ADD_CALL_FLAG(call, ZEND_CALL_TOP);
                zend_execute_ex(call);
            }
        }
    
        ...
    
        ZEND_VM_SET_OPCODE(opline + 1);
        ZEND_VM_CONTINUE();
    }
    

    可以看到,DO_FCALL 首先将调用函数前的上下文数据保存到 call->prev_execute_data,然后调用 i_init_func_execute_data 函数,将自定义函数对象中的 op_array(每个自定义函数会在编译的时候生成对应的数据,其数据结构中包含了函数的 OPCODE 数组) 赋值给新的执行上下文对象。

    然后,调用 zend_execute_ex 函数,开始执行自定义的函数。zend_execute_ex 实际上就是前面提到的 execute_ex 函数(默认是这样,但扩展可能重写 zend_execute_ex 指针,这个 API 让 PHP 扩展开发者可以通过覆写函数达到扩展功能的目的,不是本篇的主题,不准备深入探讨),只是上下文数据被替换成当前函数所在的上下文数据。

    我们可以这样理解,最外层的代码就是一个默认存在的函数(类似 C 语言中的 main()函数),和用户自定义的函数本质上是没有区别的。

    opcode是计算机指令中的一部分,用于指定要执行的操作, 指令的格式和规范由处理器的指令规范指定。 除了指令本身以外通常还有指令所需要的操作数,可能有的指令不需要显式的操作数。 这些操作数可能是寄存器中的值,堆栈中的值,某块内存的值或者IO端口中的值等等

    逻辑跳转

    我们知道指令都是顺序执行的,而我们的程序,一般都包含不少的逻辑判断和循环,这部分又是如何通过 OPCODE 实现的呢?

    <?php
    $a = 10;
    if ($a == 10) {
        echo 'success';
    } else {
        echo 'failure';
    }
    

    我们还是通过 vld 查看 OPCODE(不得不说 vld 扩展是分析 PHP 的神器)。

    compiled vars:  !0 = $a
    line     #* E I O op                           fetch          ext  return  operands
    -------------------------------------------------------------------------------------
       2     0  E >   ASSIGN                                                   !0, 10
       3     1        IS_EQUAL                                         ~2      !0, 10
             2      > JMPZ                                                     ~2, ->5
       4     3    >   ECHO                                                     'success'
             4      > JMP                                                      ->6
       6     5    >   ECHO                                                     'failure'
       7     6    > > RETURN                                                   1
    

    我们看到,JMPZ 和 JMP 控制了执行流程。JMP 的逻辑非常简单,将当前的 OPCODE 指针指向需要跳转的 OPCODE。

    ZEND_VM_HANDLER(42, ZEND_JMP, JMP_ADDR, ANY)
    {
        USE_OPLINE
    
        ZEND_VM_SET_OPCODE(OP_JMP_ADDR(opline, opline->op1));
        ZEND_VM_CONTINUE();
    }
    

    JMPZ 仅仅是多了一次判断,根据结果选择是否跳转,这里就不再重复列举了。而处理循环的方式与判断基本上是类似的。

    <?php
    $a = [1, 2, 3];
    foreach ($a as $n) {
        echo $n;
    }
    
    compiled vars:  !0 = $a, !1 = $n
    line     #* E I O op                           fetch          ext  return  operands
    -------------------------------------------------------------------------------------
       2     0  E >   ASSIGN                                                   !0, <array>
       3     1      > FE_RESET_R                                       $3      !0, ->5
             2    > > FE_FETCH_R                                               $3, !1, ->5
       4     3    >   ECHO                                                     !1
             4      > JMP                                                      ->2
             5    >   FE_FREE                                                  $3
       5     6      > RETURN                                                   1
    

    循环只需要 JMP 指令即可完成,通过 FE_FETCH_R 指令判断是否已经到达数组的结尾,如果到达则退出循环。

    通常opcode还有另一种称谓: 字节码。 例如Java虚拟机,.NET的通用中间语言(CIL: Common Intermeditate Language)等等

    结语

    通过了解 Zend 虚拟机,相信你对 PHP 是如何运行的,会有更深刻的理解。想到我们写的一行行代码,最后机器执行的时候会变成数不胜数的指令,每个指令又建立在复杂的处理逻辑之上。那些从前随意写下的代码,现在会不会在脑海里不自觉的转换成 OPCODE 再品味一番呢?

    PHP中的opcode则属于前面介绍中的后着,PHP是构建在Zend虚拟机之上的。PHP的opcode就是Zend虚拟机中的指令

    Relevant Link:

    2. PHP中的Opcode

    0x1: 数据结构

    在PHP实现内部,opcode由如下的结构体表示

    php-5.6.17Zendzend_compile.h

    struct _zend_op {opcode_handler_t handler; // 执行该opcode时调用的处理函数znode_op op1; // opcode所操作的操作数znode_op op2; // opcode所操作的操作数znode_op result;ulong extended_value;uint lineno;zend_uchar opcode; // opcode代码zend_uchar op1_type;zend_uchar op2_type;zend_uchar result_type;}; 
    

    和CPU的指令类似,有一个标示指令的opcode字段,以及这个opcode所操作的操作数,PHP不像汇编那么底层, 在脚本实际执行的时候可能还需要其他更多的信息,extended_value字段就保存了这类信息, 其中的result域则是保存该指令执行完成后的结果

    例如如下代码是在编译器遇到print语句的时候进行编译的函数

    php-5.6.17Zendzend_compile.c

    void zend_do_print(znode *result, const znode *arg TSRMLS_DC) /* {{{ */{ //新创建一条zend_op zend_op *opline = get_next_op TSRMLS_CC);//将新建的zend_op的返回值类型设置为临时变量,因为print中的内存仅仅为了临时输出,并不需要保存opline->result_type = IS_TMP_VAR;//为临时变量申请空间opline->result.var = get_temporary_variable;//指定opcode为ZEND_PRINTopline->opcode = ZEND_PRINT;//将传递进来的参数赋值给这条opcode的第一个操作数SET_NODE;SET_UNUSED;GET_NODE(result, opline->result);}
    

    0x2: opcode类型: zend_op->zend_uchar opcode

    比对汇编语言的概念,每个opcode都对应于一个类型,表明该opcpde的"操作指令",opcode的类型为zend_uchar,zend_uchar实际上就是unsigned char,此字段保存的整形值即为op的编号,用来区分不同的op类型,opcode的可取值都被定义成了宏

    /Zend/zend_vm_opcodes.h

    #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_SR 7#define ZEND_CONCAT 8#define ZEND_BW_OR 9#define ZEND_BW_AND 10#define ZEND_BW_XOR 11#define ZEND_BW_NOT 12#define ZEND_BOOL_NOT 13#define ZEND_BOOL_XOR 14#define ZEND_IS_IDENTICAL 15#define ZEND_IS_NOT_IDENTICAL 16#define ZEND_IS_EQUAL 17#define ZEND_IS_NOT_EQUAL 18#define ZEND_IS_SMALLER 19#define ZEND_IS_SMALLER_OR_EQUAL 20#define ZEND_CAST 21#define ZEND_QM_ASSIGN 22#define ZEND_ASSIGN_ADD 23#define ZEND_ASSIGN_SUB 24#define ZEND_ASSIGN_MUL 25#define ZEND_ASSIGN_DIV 26#define ZEND_ASSIGN_MOD 27#define ZEND_ASSIGN_SL 28#define ZEND_ASSIGN_SR 29#define ZEND_ASSIGN_CONCAT 30#define ZEND_ASSIGN_BW_OR 31#define ZEND_ASSIGN_BW_AND 32#define ZEND_ASSIGN_BW_XOR 33#define ZEND_PRE_INC 34#define ZEND_PRE_DEC 35#define ZEND_POST_INC 36#define ZEND_POST_DEC 37#define ZEND_ASSIGN 38#define ZEND_ASSIGN_REF 39#define ZEND_ECHO 40#define ZEND_PRINT 41#define ZEND_JMP 42#define ZEND_JMPZ 43#define ZEND_JMPNZ 44#define ZEND_JMPZNZ 45#define ZEND_JMPZ_EX 46#define ZEND_JMPNZ_EX 47#define ZEND_CASE 48#define ZEND_SWITCH_FREE 49#define ZEND_BRK 50#define ZEND_CONT 51#define ZEND_BOOL 52#define ZEND_INIT_STRING 53#define ZEND_ADD_CHAR 54#define ZEND_ADD_STRING 55#define ZEND_ADD_VAR 56#define ZEND_BEGIN_SILENCE 57#define ZEND_END_SILENCE 58#define ZEND_INIT_FCALL_BY_NAME 59#define ZEND_DO_FCALL 60#define ZEND_DO_FCALL_BY_NAME 61#define ZEND_RETURN 62#define ZEND_RECV 63#define ZEND_RECV_INIT 64#define ZEND_SEND_VAL 65#define ZEND_SEND_VAR 66#define ZEND_SEND_REF 67#define ZEND_NEW 68#define ZEND_INIT_NS_FCALL_BY_NAME 69#define ZEND_FREE 70#define ZEND_INIT_ARRAY 71#define ZEND_ADD_ARRAY_ELEMENT 72#define ZEND_INCLUDE_OR_EVAL 73#define ZEND_UNSET_VAR 74#define ZEND_UNSET_DIM 75#define ZEND_UNSET_OBJ 76#define ZEND_FE_RESET 77#define ZEND_FE_FETCH 78#define ZEND_EXIT 79#define ZEND_FETCH_R 80#define ZEND_FETCH_DIM_R 81#define ZEND_FETCH_OBJ_R 82#define ZEND_FETCH_W 83#define ZEND_FETCH_DIM_W 84#define ZEND_FETCH_OBJ_W 85#define ZEND_FETCH_RW 86#define ZEND_FETCH_DIM_RW 87#define ZEND_FETCH_OBJ_RW 88#define ZEND_FETCH_IS 89#define ZEND_FETCH_DIM_IS 90#define ZEND_FETCH_OBJ_IS 91#define ZEND_FETCH_FUNC_ARG 92#define ZEND_FETCH_DIM_FUNC_ARG 93#define ZEND_FETCH_OBJ_FUNC_ARG 94#define ZEND_FETCH_UNSET 95#define ZEND_FETCH_DIM_UNSET 96#define ZEND_FETCH_OBJ_UNSET 97#define ZEND_FETCH_DIM_TMP_VAR 98#define ZEND_FETCH_CONSTANT 99#define ZEND_GOTO 100#define ZEND_EXT_STMT 101#define ZEND_EXT_FCALL_BEGIN 102#define ZEND_EXT_FCALL_END 103#define ZEND_EXT_NOP 104#define ZEND_TICKS 105#define ZEND_SEND_VAR_NO_REF 106#define ZEND_CATCH 107#define ZEND_THROW 108#define ZEND_FETCH_CLASS 109#define ZEND_CLONE 110#define ZEND_RETURN_BY_REF 111#define ZEND_INIT_METHOD_CALL 112#define ZEND_INIT_STATIC_METHOD_CALL 113#define ZEND_ISSET_ISEMPTY_VAR 114#define ZEND_ISSET_ISEMPTY_DIM_OBJ 115#define ZEND_PRE_INC_OBJ 132#define ZEND_PRE_DEC_OBJ 133#define ZEND_POST_INC_OBJ 134#define ZEND_POST_DEC_OBJ 135#define ZEND_ASSIGN_OBJ 136#define ZEND_INSTANCEOF 138#define ZEND_DECLARE_CLASS 139#define ZEND_DECLARE_INHERITED_CLASS 140#define ZEND_DECLARE_FUNCTION 141#define ZEND_RAISE_ABSTRACT_ERROR 142#define ZEND_DECLARE_CONST 143#define ZEND_ADD_INTERFACE 144#define ZEND_DECLARE_INHERITED_CLASS_DELAYED 145#define ZEND_VERIFY_ABSTRACT_CLASS 146#define ZEND_ASSIGN_DIM 147#define ZEND_ISSET_ISEMPTY_PROP_OBJ 148#define ZEND_HANDLE_EXCEPTION 149#define ZEND_USER_OPCODE 150#define ZEND_JMP_SET 152#define ZEND_DECLARE_LAMBDA_FUNCTION 153#define ZEND_ADD_TRAIT 154#define ZEND_BIND_TRAITS 155#define ZEND_SEPARATE 156#define ZEND_QM_ASSIGN_VAR 157#define ZEND_JMP_SET_VAR 158#define ZEND_DISCARD_EXCEPTION 159#define ZEND_YIELD 160#define ZEND_GENERATOR_RETURN 161#define ZEND_FAST_CALL 162#define ZEND_FAST_RET 163#define ZEND_RECV_VARIADIC 164#define ZEND_SEND_UNPACK 165#define ZEND_POW 166#define ZEND_ASSIGN_POW 167 
    

    0x3: opcode执行句柄: zend_op->handler

    op的执行句柄,其类型为opcode_handler_t

    typedef int (ZEND_FASTCALL *opcode_handler_t) (ZEND_OPCODE_HANDLER_ARGS);

    这个函数指针为op定义了执行方式,每一种opcode字段都对应一个种类的handler,比如如果$a

    1;这样的代码生成的op,操作数为const和cv,最后就能确定handler为函数ZEND_ASSIGN_SPEC_CV_CONST_HANDLER

    /Zend/zend_vm_execute.h

    void zend_init_opcodes_handlers{static const opcode_handler_t labels[] = {..ZEND_ASSIGN_SPEC_CV_CONST_HANDLER,..}}
    

    0x4: opcpde操作数znode

    操作数字段是_zend_op类型中比较重要的部分了,其中op1,op2,result三个操作数定义为znode类型

    php-5.6.17Zendzend_compile.h

    typedef struct _znode { /* used only during compilation *//*这个int类型的字段定义znode操作数的类型#define IS_CONST  //表示常量,例如$a = 123; $b = "hello";这些代码生成OP后,123和"hello"都是以常量类型操作数存在#define IS_TMP_VAR  //表示临时变量,临时变量一般在前面加~来表示,这是一些OP执行过程中需要用到的中间变量,例如初始化一个数组的时候,就需要一个临时变量来暂时存储数组zval,然后将数组赋值给变量#define IS_VAR  //一般意义上的变量,以$开发表示#define IS_UNUSED  // Unused variable #define IS_CV  // Compiled variable,这种类型的操作数比较重要,此类型是在PHP后来的版本中中才出现,CV的意思是compiled variable,即编译后的变量,变量都是保存在一个符号表中,这个符号表是一个哈希表,如果每次读写变量的时候都需要到哈希表中去检索,会对效率有一定的影响,因此在执行上下文环境中,会将一些编译期间生成的变量缓存起来。此类型操作数一般以!开头表示,比如变量$a=123;$b="hello"这段代码,$a和$b对应的操作数可能就是!0和!1, 0和1相当于一个索引号,通过索引号从缓存中取得相应的值*/int op_type;/*此字段为一个联合体,根据op_type的不同,u取不同的值1. op_type=IS_CONST的时候,u中的constant保存的就是操作数对应的zval结构2. 例如$a=123时,123这个操作数中,u中的constant是一个IS_LONG类型的zval,其值lval为123 */union {znode_op op;zval constant; /* replaced by literal/zv */zend_op_array *op_array;zend_ast *ast;} u;zend_uint EA; /* extended attributes */} znode; 
    

    0x5: opcode编译后数组op_array

    在zend_do_print函数中的第一行,我们注意到下面这行代码

    zend_op *opline = get_next_op TSRMLS_CC); 
    

    PHP脚本代码被编译后产生的opcode保存在op_array中,其内部存储的结构如下

    php-5.6.17Zendzend_compile.h

    struct _zend_op_array {/* Common elements */zend_uchar type;const char *function_name; // 如果是用户定义的函数则,这里将保存函数的名字zend_class_entry *scope;zend_uint fn_flags;union _zend_function *prototype;zend_uint num_args;zend_uint required_num_args;zend_arg_info *arg_info;/* END of common elements */zend_uint *refcount;zend_op *opcodes; // opcode数组zend_uint last;zend_compiled_variable *vars;int last_var;zend_uint T;zend_uint nested_calls;zend_uint used_stack;zend_brk_cont_element *brk_cont_array;int last_brk_cont;zend_try_catch_element *try_catch_array;int last_try_catch;zend_bool has_finally_block;/* static variables support */HashTable *static_variables;zend_uint this_var;const char *filename;zend_uint line_start;zend_uint line_end;const char *doc_comment;zend_uint doc_comment_len;zend_uint early_binding; /* the linked list of delayed declarations */zend_literal *literals;int last_literal;void **run_time_cache;int last_cache_slot;void *reserved[ZEND_MAX_RESERVED_RESOURCES];}; 
    

    整个PHP脚本代码被编译后的opcodes保存在这里,在执行的时候由下面的execute函数执行

    ZEND_API void execute(zend_op_array *op_array TSRMLS_DC){// ... 循环执行op_array中的opcode或者执行其他op_array中的opcode}
    

    每条opcode都有一个opcode_handler_t的函数指针字段,用于执行该opcode,PHP有三种方式来进行opcode的处理

    1. CALL: PHP默认使用CALL的方式,也就是函数调用的方式2. SWITCH: 由于opcode执行是每个PHP程序频繁需要进行的操作,可以使用SWITCH或者GOTO的方式来分发3. GOTO: 通常GOTO的效率相对会高一些,不过效率是否提高依赖于不同的CPU 实际上我们会发现,在/zend/zend_language_parser.c中就是Zend的opcode翻译解释执行过程,其中包含了call、switch、goto三种opcode执行方式

    这就是PHP为什么称之为解释型语言的内核原理,PHP在完成Lex词法解析后,在语法解析即生成产生式的时候,直接通过call、switch、goto的方式调用zend api进行即使解释执行

    Relevant Link:

    http://www.nowamagic.net/librarys/veda/detail/1325http://php.net/manual/zh/internals2.opcodes.list.phphttp://www.nowamagic.net/librarys/veda/detail/1543http://www.nowamagic.net/librarys/veda/detail/1324http://www.nowamagic.net/librarys/veda/detail/1543 http://www.laruence.com/2008/06/18/221.htmlhttp://www.php-internals.com/book/?p=chapt02/02-03-02-opcode 
    

    3. opcode翻译执行

    Relevant Link:

    http://www.php-internals.com/book/?p=chapt02/02-03-03-from-opcode-to-handler
    

    以上所述本文给大家介绍的PHP内核学习教程之php opcode内核实现的相关知识,希望对大家有所帮助。

    本文由威尼斯手机平台登陆发布于最新留言,转载请注明出处:威尼斯正规官网VM)是Fackbook推出用于在执行PHP代码的虚拟机,PHP的opcode就是Zend虚拟机中的指令

    关键词: