在前面的章节中,我们了解到一个PHP文件在服务器端的执行过程包括以下两个大的过程:
递给php程序需要执行的文件, php程序完成基本的准备工作后启动PHP及Zend引擎, 加载注册的扩展模块。
初始化完成后读取脚本文件,Zend引擎对脚本文件进行词法分析,语法分析。然后编译成opcode执行。如过安装了apc之类的opcode缓存, 编译环节可能会被跳过而直接从缓存中读取opcode执行。
在第二步中,词法分析、语法分析,编译中间代码,执行中间代码等各个部分统称为Zend虚拟机。与Java、C#等编译型语言相比,PHP少了一个手动编译的过程,它们无需编译即可运行,我们称其为解释性语言。 Java有自己的Java虚拟机,它在多个平台上实现统一语言; C#有自己的.NET虚拟机,它在单一平台实现多种语言; PHP跟他们一样,也有属于自己的Zend虚拟机。它们在本质是相同的,它们都是抽象的计算机。这些虚拟机都是在某种较底层的语言上抽象出另外一种语言,有自己的指令集,有自己的内存管理体系。它们最终都会将抽象级别较高的语言实现转化为抽象级别较低的语言实现,并且实现其它辅助功能,如内存管理,垃圾回收等机制,以减少程序员在具体实现上的工作,从而可以将更多的时间和精力投入到业务逻辑中。从抽象层次看,Zend虚拟机比Java等语言更高级一些,这里的高级不是说功能更强大或效率更高,简单点说,Zend虚拟机离真正的机器实现更远一些。最近这些年,语言的发展只是不断的抽象,不断的远离机器,没有根本性的变化。
本章,我们从虚拟机的前世今生讲起,叙述Zend虚拟机的实现原理,关键的数据结构,并其中穿插一个关于语法实现的示例和源码加密解密的过程说明。
在wiki中虚拟机的定义是:虚拟机(Virtual Machine),在计算机科学中的体系结构里,是指一种特殊的软件,他可以在计算机平台和终端用户之间创建一种环境,而终端用户则是基于这个软件所创建的环境来操作软件。在计算机科学中,虚拟机是指可以像真实机器一样运行程序的计算机的软件实现。
虚拟机是一种抽象的计算机,它有自己的指令集,有自己的内存管理体系。在此类虚拟机上实现的语言比较低抽象层次的语言更加明了,更加简单易学。
虚拟机的类型
虚拟机是一种抽象的计算机,是对真实计算机的虚拟和模拟,现在的计算机有不同的指令集架构(ISA: Instruction Set Architecture), ISA是处理的一个部分,不同的处理器会有不同的架构,最常见的有3种:
基于栈的Stack Machines: 操作数保存在栈上。而不是使用寄存器来保存,现在很少有真实机器采用这个模型。对于虚拟机来说因为指令空间占用少,并且实现简单,很多虚拟机采用这种模型,比如:JVM,HHVM等。
基于累加器的Accumulator Machines。这个模型使用称作累加器(Accumulator)的的寄存器来保存一个操作数以及操作的结果。
基于通用寄存器的General-Purpose-Register Machines,这些寄存器没有特殊的用途。编译器可以将操作数保存在这些寄存器中。ZendVM采用的就是基于寄存器的架构。
Zend虚拟机核心实现代码
为了方便读者对Zend引擎的实现有个全面的感觉,下面列出涉及到Zend引擎实现的核心代码文件功能参考。
Zend引擎的核心文件都在$PHP_SRC/Zend/目录下面。不过最为核心的文件只有如下几个:
- PHP语法实现
Zend/zend_language_scanner.l
Zend/zend_language_parser.y
Opcode编译
Zend/zend_compile.c
执行引擎
Zend/zend_vm_*
Zend/zend_execute.c
Zend虚拟机体系结构
从概念层将Zend虚拟机的实现进行抽象,我们可以将Zend虚拟机的体系结构分为:解释层、执行引擎、中间数据层,如图所示:

当一段PHP代码进入Zend虚拟机,它会被执行两步操作:编译和执行。对于一个解释性语言来说,这是一个创造性的举动,但是,现在的实现并不彻底。现在当PHP代码进入Zend虚拟机后,它虽然会被执行这两步操作,但是这两步操作对于一个常规的执行过程来说却是连续的,也就是说它并没有转变成和Java这种编译型语言一样:生成一个中间文件存放编译后的结果。如果每次执行这样的操作,对于PHP脚本的性能来说是一个极大的损失。虽然有类似于APC,eAccelerator等缓存解决方案。但是其本质上是没有变化的,并且不能将两个步骤分离,各自发展壮大。
解释层
解释层是Zend虚拟机执行编译过程的位置。它包括词法解析、语法解析和编译生成中间代码三个部分。词法分析就是将我们要执行的PHP源文件,去掉空格,去掉注释,切分为一个个的标记(token),并且处理程序的层级结构(hierarchical structure)。
语法分析就是将接受的标记(token)序列,根据定义的语法规则,来执行一些动作,Zend虚拟机现在使用的Bison使用巴科斯范式(BNF)来描述语法。编译生成中间代码是根据语法解析的结果对照Zend虚拟机制定的opcode生成中间代码,在PHP5.3.1中,Zend虚拟机支持135条指令(见Zend/zend_vm_opcodes.h文件),无论是简单的输出语句还是程序复杂的递归调用,Zend虚拟机最终都会将所有我们编写的PHP代码转化成这135条指令的序列,之后在执行引擎中按顺序执行。
中间数据层
当Zend虚拟机执行一个PHP代码时,它需要内存来存储许多东西,比如,中间代码,PHP自带的函数列表,用户定义的函数列表,PHP自带的类,用户自定义的类,常量,程序创建的对象,传递给函数或方法的参数,返回值,局部变量以及一些运算的中间结果等。我们把这些所有的存放数据的地方称为中间数据层。
如果PHP以mod扩展的方式依附于Apache2服务器运行,中间数据层的部分数据可能会被多个线程共享,如果PHP自带的函数列表等。如果只考虑单个进程的方式,当一个进程被创建时它就会被加载PHP自带的各种函数列表,类列表,常量列表等。当解释层将PHP代码编译完成后,各种用户自定义的函数,类或常量会添加到之前的列表中,只是这些函数在其自身的结构中某些字段的赋值是不一样的。
当执行引擎执行生成的中间代码时,会在Zend虚拟机的栈中添加一个新的执行中间数据结构(zend_execute_data),它包括当前执行过程的活动符号列表的快照、一些局部变量等。
执行引擎
Zend虚拟机的执行引擎是一个非常简单的实现,它只是依据中间代码序列(EX(opline)),一步一步调用对应的方法执行。在执行引擎中没并有类似于PC寄存器一样的变量存放下一条指令,当Zend虚拟机执行到某条指令时,当它所有的任务都执行完了,这条指令会自己调用下一条指令,即将序列的指针向前移动一个位置,从而执行下一条指令,并且在最后执行return语句,如此反复。这在本质上是一个函数嵌套调用。
回到开头的问题,PHP通过词法分析、语法分析和中间代码生成三个步骤后,PHP文件就会被解析成PHP的中间代码opcode。生成的中间代码与实际的PHP代码之间并没有完全的一一对应关系。只是针对用户所给的PHP代码和PHP的语法规则和一些内部约定生成中间代码,并且这些中间代码还需要依靠一些全局变量中转数据和关联。至于生成的中间代码的执行过程是依据中间代码的顺利,依赖于执行过程中的全局变量,一步步执行。当然,在遇到一些函数跳转也会发生偏移,但是最终还是会回到偏移点。