【走进php内核】之 Zend引擎执行过程

本文详细介绍了Zend引擎的执行过程,包括数据结构如opcode、zend_op_array、zend_executor_globals和zend_execute_data,以及执行流程的四个步骤:分配stack、初始化zend_execute_data、执行opcode和释放stack。文章还特别讨论了函数的执行流程,从初始化阶段、参数传递、函数调用、函数执行到函数返回的各个阶段。通过对具体opcode的分析,阐述了函数调用时的上下文切换和局部变量管理。
摘要由CSDN通过智能技术生成

Zend引擎主要包含两个核心部分:编译、执行:

前面分析了Zend的编译过程以及PHP用户函数的实现,接下来分析下Zend引擎的执行过程。

1 数据结构

执行流程中有几个重要的数据结构,先看下这几个结构。

1.1 opcode

opcode是将PHP代码编译产生的Zend虚拟机可识别的指令,php7共有173个opcode,定义在zend_vm_opcodes.h中,PHP中的所有语法实现都是由这些opcode组成的。

struct _zend_op {
    const void *handler; //对应执行的C语言function,即每条opcode都有一个C function处理
    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; //返回值类型
};

1.2 zend_op_array

zend_op_array是Zend引擎执行阶段的输入,整个执行阶段的操作都是围绕着这个结构,关于其具体结构前面我们已经讲过了。

这里再重复说下zend_op_array几个核心组成部分:

  • opcode指令:即PHP代码具体对应的处理动作,与二进制程序中的代码段对应
  • 字面量存储:PHP代码中定义的一些变量初始值、调用的函数名称、类名称、常量名称等等称之为字面量,这些值用于执行时初始化变量、函数调用等等
  • 变量分配情况:与字面量类似,这里指的是当前opcodes定义了多少变量、临时变量,每个变量都有一个对应的编号,执行初始化按照总的数目一次性分配zval,使用时也完全按照编号索引,而不是根据变量名索引

1.3 zend_executor_globals

zend_executor_globals executor_globals是PHP整个生命周期中最主要的一个结构,是一个全局变量,在main执行前分配(非ZTS下),直到PHP退出,它记录着当前请求全部的信息,经常见到的一个宏EG操作的就是这个结构。

//zend_compile.c
#ifndef ZTS
ZEND_API zend_compiler_globals compiler_globals;
ZEND_API zend_executor_globals executor_globals;
#endif

//zend_globals_macros.h
# define EG(v) (executor_globals.v)

zend_executor_globals结构非常大,定义在zend_globals.h中,比较重要的几个字段含义如下图所示:

1.4 zend_execute_data

zend_execute_data是执行过程中最核心的一个结构,每次函数的调用、include/require、eval等都会生成一个新的结构,它表示当前的作用域、代码的执行位置以及局部变量的分配等等,等同于机器码执行过程中stack的角色,后面分析具体执行流程的时候会详细分析其作用。

#define EX(element)             ((execute_data)->element)

//zend_compile.h
struct _zend_execute_data {
    const zend_op       *opline;  //指向当前执行的opcode,初始时指向zend_op_array起始位置
    zend_execute_data   *call;             /* current call                   */
    zval                *return_value;  //返回值指针
    zend_function       *func;          //当前执行的函数(非函数调用时为空)
    zval                 This;          //这个值并不仅仅是面向对象的this,还有另外两个值也通过这个记录:call_info + num_args,分别存在zval.u1.reserved、zval.u2.num_args
    zend_class_entry    *called_scope;  //当前call的类
    zend_execute_data   *prev_execute_data; //函数调用时指向调用位置作用空间
    zend_array          *symbol_table; //全局变量符号表
#if ZEND_EX_USE_RUN_TIME_CACHE
    void               **run_time_cache;   /* cache op_array->run_time_cache */
#endif
#if ZEND_EX_USE_LITERALS
    zval                *literals;  //字面量数组,与func.op_array->literals相同
#endif
};

zend_execute_data与zend_op_array的关联关系:

2 执行流程

Zend的executor与linux二进制程序执行的过程是非常类似的,在C程序执行时有两个寄存器ebp、esp分别指向当前作用栈的栈顶、栈底,局部变量全部分配在当前栈,函数调用、返回通过call、ret指令完成,调用时call将当前执行位置压入栈中,返回时ret将之前执行位置出栈,跳回旧的位置继续执行,在Zend VM中zend_execute_data就扮演了这两个角色,zend_execute_data.prev_execute_data保存的是调用方的信息,实现了call/ret,zend_execute_data后面会分配额外的内存空间用于局部变量的存储,实现了ebp/esp的作用。

注意:在执行前分配内存时并不仅仅是分配了zend_execute_data大小的空间,除了sizeof(zend_execute_data)外还会额外申请一块空间,用于分配局部变量、临时(中间)变量等,具体的分配过程下面会讲到。

Zend执行opcode的简略过程:

  • step1: 为当前作用域分配一块内存,充当运行栈,zend_execute_data结构、所有局部变量、中间变量等等都在此内存上分配
  • step2: 初始化全局变量符号表,然后将全局执行位置指针EG(current_execute_data)指向step1新分配的zend_execute_data,然后将zend_execute_data.opline指向op_array的起始位置
  • step3: 从EX(opline)开始调用各opcode的C处理handler(即_zend_op.handler),每执行完一条opcode将EX(opline)++继续执行下一条,直到执行完全部opcode,函数/类成员方法调用、if的执行过程:
    • step3.1: if语
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值