基础知识

Zend 虚拟机(称为Zend VM)是 PHP 语言的核心,承担了语法和词法分析、AST 编译以及指令的执行工作,下面我们讨论一下 Zend 虚拟机的基础架构以及相关的基础知识。

Zend 虚拟机架构

Zend 虚拟机主要分为解释层、中间数据层和执行层,如图11-1所示。

image 2024 06 10 18 49 34 329
Figure 1. 图11-1 Zend虚拟机架构图
  1. 解释层这一层主要负责对 PHP 代码进行词法和语法分析,生成对应的 AST;另一个工作就是对 AST 进行编译,生成符号表和指令集。

  2. 中间数据层这一层主要包含了虚拟机的核心部分——执行栈的维护、指令集和符号表的存储,而这些是执行引擎调度执行的基础。

  3. 执行层这一层是执行指令集的引擎,负责最终的执行并生成结果,这一层实现了大量的底层函数。

为了更好地理解 Zend 虚拟机各层的工作,我们先了解一下物理机的一些基础知识,读者可以对照理解虚拟机的原理。

符号表

符号表是编译程序在编译过程中用来记录源程序中各种名字的特性信息,所以也称为名字特性表。名字一般包含程序名、过程名、函数名、用户定义类型名、变量名、常量名、枚举值名、标号名等。特性信息指的是名字的种类、类型、维数、参数个数、数值及目标地址(存储单元地址)等。

符号表有什么作用呢?一是协助进行语义检查,比如检查一个名字的引用和之前的声明是否相符;二是协助中间代码生成,最重要的是在目标代码生成阶段,当需要为名字分配地址时,符号表中的信息是地址分配的主要依据。

image 2024 06 10 18 51 01 184
Figure 2. 图11-2 符号表创建示例

符号表一般有 3 种构造和处理方法,分别是线性查找、二叉树和 Hash 技术。其中,线性查找法是最简单的,按照符号出现的顺序填表,每次查找从第一个位置开始顺序查找,效率比较低;二叉树实现了对半查找,在一定程度上提高了效率;效率最高的方法是通过 Hash 技术实现符号表,通过对第 5 章的学习,详细大家对 Hash 技术有一定的了解,而 PHP 7 中的符号表就是使用 HashTable 实现的。

函数调用栈

为了更清晰地了解虚拟机中函数调用的过程,我们先了解一下物理机的简单原理,主要涉及函数调用栈的概念,而 Zend 虚拟机参照了物理机的基本原理,做了类似的设计。

下面以一段 C 代码描述一下系统栈和函数过程调用,代码如下:

int funcB(int argB1, int argB2)
{
    int varB1, varB2;
    return argB1+argB2;
}
int funcA(int argA1, int argA2)
{
    int varA1, varA2;
    return argA1+argA2+funcB( 3, 4);
}
int main()
{
    int varMain;
    return funcA(1, 2);
}

这段代码运行时,首先 main 函数会压栈,局部变量 varMain 入栈,main 函数调用 funcA 函数,C 语言会从后往前将函数参数压栈,先压第二个参数 argA2=2,再压第一个参数 argA1=1,同时对于 funcA 的返回会产生一个临时变量等待赋值,也会被压栈,这些称为 main 函数的栈帧;接着将 funcA 压栈,同样先将局部变量 varA1 和 varA2 压栈,因为调用了函数 funcB,会将参数 argB2=4 和 argB1=3 压入栈中,同时将 funcB 的返回产生的临时变量压入栈中,这部分称为 funcA 的栈帧;同样,funcB 被压入栈中。函数调用压栈过程示意图如图11-3所示。

image 2024 06 10 18 52 55 425
Figure 3. 图11-3 函数调用压栈过程示意图

执行 funcB 函数,对 argB1 和 argB2 进行相加操作,执行后得到返回值为 7,然后 funcB 的栈帧出栈,funcA 中的临时变量 TempB 被赋值为 7,继而进行相加操作,得到结果为 10,然后 funcA 出栈,main 函数中的临时变量 TempA 被赋值为 10,最终 main 函数返回并出栈,整个函数调用结束。函数调用出栈过程示意图如图11-4所示。

image 2024 06 10 18 53 24 282
Figure 4. 图11-4 函数调用出栈过程示意图

指令

汇编语句中的指令语句格式一般如下:

    [标号:]     [前缀]  指令助记符    [操作数]     [;注释]

其中:

  1. 标号字段由各种有效字符组成,一般表示符号地址,具有段基址、偏移量、类型 3 种属性。通常情况下,这部分是可选部分,主要为便于程序的读写而使用。

  2. 指令助记符规定指令或伪指令的操作功能,是语句中唯一不可缺少的部分。对于指令,汇编程序会将其翻译成机器语言指令:

    MOV   AX, 100      →   B8 00 01
  3. 操作数指明指令语句中提供给指令的操作对象、存放位置。操作数可以是 1 个、2 个或 0 个,2 个操作数之间用逗号 “, ” 分开。比如 “RET; ” 对应的操作数个数是 0 个,“INC BX; ” 对应的操作数个数是 1,“MOV AX, DATA; ” 对应的操作数个数是 2 个。

  4. 注释以 “ ; ” 开始,用于对程序进行解释和说明。

符号表、函数调用栈以及指令构成了物理机执行的基本元素,Zend 虚拟机也同样实现了符号表、函数调用栈及指令,以运行 PHP 代码。下面我们先讨论一下 Zend 虚拟机相关的数据结构。