PHP 7语言的执行原理
我们常用的高级语言有很多种,比较出名的有 C\C++、Python、PHP、Go、Pascal 等。而这些语言根据运行的方式不同,大体分为两种:编译型语言和解释型语言。
其中,编译型语言包括 C\C++、Pascal、Go 等。这里说的编译是指在应用源程序执行之前,就将程序源代码 “翻译” 成汇编语言,然后进一步根据软硬件环境编译成目标文件。一般称完成编译工作的工具为编译器。而解释型语言,在程序运行时才被 “翻译” 为机器语言。但是执行一次 “翻译” 一次,所以执行效率较低。解释器的工作就是解释型语言中,负责 “翻译” 源代码的程序。
下面会更详细地讨论一下编译型语言和解释型语言的运行方式。
编译型语言与解释型语言
我们知道,对于一段 C
语言代码,需要经过预编译、编译、汇编和链接,才能成为可执行的二进制文件。以 hello.c
为例:
#include<stdio.h>
int main(){
printf("hello world");
return 1;
}

对于这段 C
代码,main
是程序入口函数,实现的功能是打印字符串 “hello world” 到屏幕上。编译和执行过程如图2-1所示。
-
第1步:
C
语言代码预处理(比如依赖处理、宏替换等)。如以上代码示例,#inlcude<stdio.h>
就会在预处理阶段被替换。 -
第2步:编译。编译器会把
C
语言翻译成汇编语言程序,一条C
语言通常编译为多条汇编代码。同时编译器会对程序进行优化,生成目标汇编程序。 -
第3步:编译得到的汇编语言通过汇编器再汇编成目标程序
hello.o
。 -
第4步:链接。程序中往往包含一些共享目标文件,如示例程序中的
printf()
函数,位于静态库,需要经过链接器(如 Uinx 连接器ld
)进行链接。
以 C
语言为代表的编译型语言,代码发生更新都要经过以上步骤。
我们在本章对编译型语言与解释型语言的区别的理解,立足于源代码被编译成目标平台 CPU 指令的时机。对于编译型语言,编译结果已经是针对当前 CPU 体系的指令;而解释型语言,需要先编译成中间代码,再经由该解释型语言的特定虚拟机,翻译成特定 CPU 体系的指令被执行。解释型语言是在运行过程中,翻译为目标平台的指令。常说解释型语言 “慢”,主要也是慢在这里。
在 PHP 7 中,源代码首先进行词法分析,将源代码切割为多个字符串单元,分割后的字符串称为 Token
。而一个一个独立的 Token
是无法表达完整语义的,需经过语法分析阶段,将 Token
转换为抽象语法树(简称 AST
)。之后,抽象语法树被转换为机器指令执行。在 PHP 中,这些指令称为 opcode
(后文会对 opcode
做更详细的解释,此处读者可以将其看待为 CPU 指令)。
到 AST
的生成这一步,编译型语言与解释型语言所需经历的过程相似。从抽象语法树之后开始产生差异。

图2-2是执行 PHP(如无特殊说明,本章提到的 PHP 均为 PHP 7 版本)代码的简化步骤,其中最后一步的左侧分支是编译型语言的过程。
-
第 1 步:源码通过词法分析得到
Token
。 -
第 2 步:基于语法分析器生成抽象语法树(AST)。
-
第 3 步:抽象语法树转换为
opcodes
(opcode 指令集合), PHP 解释执行opcodes
。
接下来在基本步骤的基础上,细化 PHP 语言的执行原理,以便更清晰地建立认知。
PHP 7的执行原理概述

首先补充说明前文提到的 PHP 7 程序执行过程,请见图 2-3。
-
第 1 步:词法分析将 PHP 代码转换为有意义的标识
Token
。该步骤的词法分析器使用Re2c
实现。 -
第 2 步:语法分析将
Token
和符合文法规则的代码生成抽象语法树。语法分析器基于Bison
实现。语法分析使用了 BNF(Backus-Naur Form,巴科斯范式)来表达文法规则,Bison
借助状态机、状态转移表和压栈、出栈等一系列操作,生成抽象语法树。 -
第 3 步:上步的抽象语法树生成对应的
opcode
,并被虚拟机执行。opcode
是 PHP 7 定义的一组指令标识,指令对应着相应的handler
(处理函数)。当虚拟机调用opcode
,会找到opcode
背后的处理函数,执行真正的处理。以常见的echo
语句为例,其对应的opcode
便是ZEND_ECHO
。
这里为了便于理解词法分析和语法分析过程,将两者分开描述。但实际情况下,出于效率考虑,两个过程并非完全独立。 |
下面通过一段示例代码,来建立 PHP 7 运转的初步理解。
示例代码如下:
<? php
echo "hello world";
从图2-3可知,这段代码首先会被切割为 Token。
Token
Token
是 PHP 代码被切割成的有意义的标识。本书介绍的 PHP 7 版本中有 137 种 Token
,在 zend_language_parser.h
文件(该文件是在 make
执行过程中生成的)中做了定义:
/* Tokens. */
#define END 0
#define T_INCLUDE 258
#define T_INCLUDE_ONCE 259
…
#define T_ERROR 392
更多 Token
的含义,感兴趣的读者可以参考 附录B。
PHP 提供了 token_get_all()
函数来获取 PHP 代码被切割后的 Token
,可以在深入源码学习前,粗略查看 PHP 代码被切割后的 Token
。对于如下代码片段:
/home/vagrant/php7/bin/php -r 'print_r(Token_get_all("<? php echo \"hello world\"; ")); '
输出结果为:
Array
(
[0] => Array
(
[0] => 379
[1] => <? php
[2] => 1
)
[1] => Array
(
[0] => 328
[1] => echo
[2] => 1
)
[2] => Array
(
[0] => 382
[1] =>
[2] => 1
)
[3] => Array
(
[0] => 323
[1] => "hello world"
[2] => 1
)
[4] => ;
)
其中,二维数组的每个成员数组的第一个值为 Token
对应的枚举值。第二个值为 Token
对应的原始字符串内容。第三个值为代码对应的行号。可以看出,词法解析器将 “<?php echo "hello world"; ” 这段文本内容切分成了 4 部分。
-
文本 “<? php”,切割后对应的
Token
值为 379,参考 PHP 7 中的源码:#define T_OPEN_TAG 379
不难理解,它是 PHP 代码的起始
tag
,也就是<? php
标识。 -
echo
对应的Token
是 T_ECHO:#define T_ECHO 328
-
源码中的空格,对应的
Token
为 T_WHITESPACE,值为 382:#define T_WHITESPACE 382
-
字符串 "hello world",对应的
Token
值为 323:#define T_CONSTANT_ENCAPSED_STRING 323
可见,Token
就是一个个的 “词块”,但是单独存在的词块不能表达完整的语义,还需要借助规则进行组织串联。语法分析器就是这个组织者。它会检查语法,匹配 Token
,对 Token
进行关联。
PHP 7 中,组织串联的产物就是 AST(Abstract Syntax Tree,抽象语法树)。
AST
AST
是 PHP 7 版本新特性。在这之前的版本中,PHP 代码的执行过程中是没有生成 AST
这一步的。PHP 7 对抽象语法树的支持,实现了 PHP 编译器和解释器解耦,有效提升了可维护性。
顾名思义,抽象语法树具有树状结构。AST
的节点分为多种类型,对应着 PHP 语法。在当前章节,我们可以认为节点类型是对语法规则的抽象,例如赋值语句,生成的抽象语法树节点为 ZEND_AST_ASSIGN
。而赋值语句的左右操作数又将作为 ZEND_AST_ASSIGN
类型节点的孩子。通过这样的节点关系,构建出抽象语法树。
如果读者希望一睹为快,可以直接跳到本书 第 13 章,其中图片描绘了一段简单的 PHP 代码生成的抽象语法树。
这里介绍 PHP-Parser 工具,它可以用来查看 PHP 代码生成的 AST
。
PHP-Parser 是 PHP 7 内核作者之一 Nikic 编写的将 PHP 源码生成 AST 的工具。源码见 https://github.com/nikic/PHP-Parser 。 |
更多关于抽象语法树的介绍,将在后续章节展开。
opcodes
AST
扮演了源码到中间代码的临时存储介质的角色,还需要将其转换为 opcode
,才能被引擎直接执行。opcode
只是单条指令,opcodes
是 opcode
的集合形式,是 PHP
执行过程中的中间代码,类似 Java 中的字节码。opcode
生成之后由虚拟机执行。
我们知道,PHP 工程优化措施中有一个比较常见的 “开启 opcache”,指的就是这里的 opcodes
的缓存(opcodes cache)。通过省去从源码到 opcode
的阶段,引擎可以直接执行缓存的 opcode
,以此提升性能。
借助 vld
插件,可以直观地看到一段 PHP 代码生成的 opcode
:
php -dvld.active=1 hello.php
经过过滤整理,对应的 opcode
为:
line op
1 ECHO
2 RETURN
其实在源码实现中,上述代码生成的 opcode
及 handler
为:
ZEND_ECHO // handler: ZEND_ECHO_SPEC_CONST_HANDLER
ZEND_RETURN // handler: ZEND_RETURN_SPEC_CONST_HANDLER
可见,ZEND_ECHO
对应的 handler
是 ZEND_ECHO_SPEC_CONST_HANDLER
。此 handler
实现的功能便是预期的 “hello world” 语句的输出。
在本书的 PHP 版本中,内核在 zend_vm_opcodes.h
中定义了 186 种 opcodes
,也可以参考本书附录 B。