我的编程空间,编程开发者的网络收藏夹
学习永远不晚

PHP内核中OPCODE是什么

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

PHP内核中OPCODE是什么

这篇文章给大家分享的是有关PHP内核中OPCODE是什么的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。

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 = $aline     #* 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_OPLINEzend_free_op free_op1, free_op2;zval *op1, *op2, *result;op1 = GE***_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 实现的。

执行过程

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

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

struct _zend_op_array {zend_uchar type;zend_uchar arg_flags[3]; 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;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;HashTable *static_variables;zend_string *filename;uint32_t line_start;uint32_t line_end;zend_string *doc_comment;uint32_t early_binding; 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_OPLINEzend_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时,就会退出循环,从而结束程序的运行。

方法调用

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

<?phpfunction foo() {echo 'test';}foo();

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

compiled vars:  noneline     #* E I O op                           fetch          ext  return  operands-------------------------------------------------------------------------------------   2     0  E >   NOP   6     1        INIT_FCALL                                               'foo' 2        DO_FCALL                                      0 3      > RETURN                                                   1compiled vars:  noneline     #* 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 实现的呢?

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

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

compiled vars:  !0 = $aline     #* 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 = $nline     #* 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 指令判断是否已经到达数组的结尾,如果到达则退出循环。

感谢各位的阅读!关于“PHP内核中OPCODE是什么”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

PHP内核中OPCODE是什么

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

PHP内核中OPCODE是什么

这篇文章给大家分享的是有关PHP内核中OPCODE是什么的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。OPCODE什么是 OPCODE?它是一种虚拟机能够识别并处理的指令。Zend 虚拟机包含了一系列的 OPCO
2023-06-17

Java中什么是内核线程

这篇文章将为大家详细讲解有关Java中什么是内核线程,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。Java是什么Java是一门面向对象编程语言,可以编写桌面应用程序、Web应用程序、分布式系
2023-06-14

Java中的内核线程是什么

Java中的内核线程是什么?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。Java可以用来干什么Java主要应用于:1. web开发;2. Android开发;3
2023-06-07

Linux中进程内核栈是什么

这篇文章主要介绍了Linux中进程内核栈是什么,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。什么是进程的“内核栈”?在每一个进程的生命周期中,必然会通过到系统调用陷入内核。在
2023-06-12

linux中内核模块指的是什么

本文小编为大家详细介绍“linux中内核模块指的是什么”,内容详细,步骤清晰,细节处理妥当,希望这篇“linux中内核模块指的是什么”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。在linux中,内核模块是linu
2023-06-29

Linux 内核代码中的 EXPORT_SYMBOL_GPL 是什么

EXPORT_SYMBOL_GPL 是一个宏,用于将符号(函数、变量、结构体等)导出为 Linux 内核模块的全局符号。在 Linux 内核中,一些符号是被标记为 EXPORT_SYMBOL 或 EXPORT_SYMBOL_GPL 的,这意
2023-09-27

Linux内核指的是什么

今天就跟大家聊聊有关Linux内核指的是什么,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。内核属于操作系统的核心部分,它具有操作系统基本的功能,主要负责管理系统的内存、进程、设备驱动
2023-06-28

Linux中Device Mapper内核空间是什么

这篇文章主要讲解了“Linux中Device Mapper内核空间是什么”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Linux中Device Mapper内核空间是什么”吧!Devicem
2023-06-27

什么是Linux内核抢占

这篇文章主要讲解了“什么是Linux内核抢占”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“什么是Linux内核抢占”吧!环境:处理器架构:arm64内核源码:linux-5.11ubuntu
2023-06-15

Linux内核的作用是什么

Linux内核是操作系统的核心组件,它负责管理计算机的硬件资源,并提供给应用程序访问这些资源的接口。它的主要作用包括以下几点:硬件管理:Linux内核负责管理计算机的硬件资源,包括CPU、内存、磁盘、网络接口等,它通过驱动程序与这些硬件进行
Linux内核的作用是什么
2024-03-06

Linux系统的内核是什么

Linux系统的内核是什么,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。相信接触过Linux的人或多或少都知道内核这个东西,下面就和大家聊一下Linux内核到底是什么东西?
2023-06-28

linux内核移植指的是什么

这篇“linux内核移植指的是什么”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“linux内核移植指的是什么”文章吧。在li
2023-06-29

Linux内核查看命令是什么

这篇文章主要介绍“Linux内核查看命令是什么”,在日常操作中,相信很多人在Linux内核查看命令是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Linux内核查看命令是什么”的疑惑有所帮助!接下来,请跟
2023-06-28

Linux内核 vs Windows内核有什么区别

这篇文章给大家分享的是有关Linux内核 vs Windows内核有什么区别的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。Windows 和 Linux 可以说是我们比较常见的两款操作系统的。Windows 基本
2023-06-15

Linux内核中的循环链表结构是什么

Linux内核中的循环链表结构是什么,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。文章中引用的代码来源于LXR,所分析的内核版本是v2.6.31。linux内核通过定义li
2023-06-17

WordPress5.7更新的核心内容是什么

小编给大家分享一下WordPress5.7更新的核心内容是什么,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!WordPress 5.7 发布更新的核心内容就是又改
2023-06-14

linux编译内核的步骤是什么

编译Linux内核的步骤如下:1. 获取内核源代码:从官方网站或开源社区下载最新版本的Linux内核源代码。2. 安装编译工具:确保系统中已经安装了必要的编译工具,如GCC、Make工具和相应的依赖库。可以通过运行`sudo apt-get
2023-10-07

Linux内核升级的方法是什么

本篇内容介绍了“Linux内核升级的方法是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!Linux内核简介:1.从技术层面讲,内核是硬件
2023-06-28

Linux内核移植的步骤是什么

今天给大家介绍一下Linux内核移植的步骤是什么。文章的内容小编觉得不错,现在给大家分享一下,觉得有需要的朋友可以了解一下,希望对大家有所帮助,下面跟着小编的思路一起来阅读吧。Linux内核如何进行移植?所谓的内核移植就是从一种硬件平台转移
2023-06-28

ubuntu编译内核的方法是什么

要编译Ubuntu内核,可以按照以下步骤进行操作:1. 安装编译所需的依赖项:```sudo apt-get install git fakeroot build-essential ncurses-dev xz-utils libssl-
2023-10-08

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录