PHP代码是如何执行的

当我们执行一段PHP代码时,会发生很多事情。一般来说,PHP解释器在执行代码时会经历四个阶段:

  1. 词法分析
  2. 解析
  3. 编译
  4. 解释

词法分析

词法分析(或令牌化)是将字符串(本例中是PHP源代码)转换为令牌序列的过程。令牌只是它所匹配的值的命名标识符。PHP使用re2c从zend_language_scanner生成lexer。

$code = <<<'code'
<?php
$a = 1;
code;

$tokens = token_get_all($code);

foreach ($tokens as $token) {
    if (is_array($token)) {
        echo "Line {$token[2]}: ", token_name($token[0]), " ('{$token[1]}')", PHP_EOL;
    } else {
        var_dump($token);
    }
}

输出:

Line 1: T_OPEN_TAG ('<?php
')
Line 2: T_VARIABLE ('$a')
Line 2: T_WHITESPACE (' ')
string(1) "="
Line 2: T_WHITESPACE (' ')
Line 2: T_LNUMBER ('1')
string(1) ";"

上面的输出中有几个值得注意的地方。第一点是,并不是所有的源代码片段都被命名为标记。相反,有些符号被认为是它们自身的标志(例如=、;、:、?等)。第二点是lexer实际上比简单地输出一个标记流要多做一点。在大多数情况下,它还存储词素(由标记匹配的值)和匹配标记的行号(用于堆栈跟踪之类的事情)。

解析

解析器也是通过BNF语法文件与Bison一起生成的。PHP使用LALR(1)(向前看,从左到右)上下文无关的语法。前瞻性部分仅仅意味着解析器能够提前查看n个标记(在本例中为1个),以解决解析过程中可能遇到的歧义。从左到右的部分意味着它从左到右解析标记流。

生成的解析器阶段将来自lexer的标记流作为输入,并有两个作业。它首先通过尝试根据其BNF语法文件中定义的任何语法规则匹配令牌顺序来验证令牌顺序的有效性。这确保了有效的语言结构是由令牌流中的令牌组成的。解析器的第二项工作是生成抽象语法树(AST)——将在下一阶段(编译)中使用的源代码的树视图。

我们可以使用php-ast扩展查看解析器生成的AST的形式。内部AST没有直接公开,因为它不是特别“干净”(就一致性和一般可用性而言),所以php-ast扩展对它进行了一些转换,使它更易于使用。

编译

编译阶段使用AST,通过递归遍历树来发出操作码。此阶段还执行一些优化。这包括用文字参数解析一些函数调用(如strlen("abc")来整型(3))和折叠常数数学表达式(如60 60 24来整型(86400))。

在这个阶段,我们可以通过多种方式检查操作码输出,包括OPcache、VLD和PHPDBG。我将使用VLD实现这一点,因为我觉得输出看起来更友好。

解释

最后一个阶段是操作码的解释。这是在Zend引擎(ZE) VM上运行操作码的地方。实际上,关于这个阶段(至少从高层次的角度来看)几乎没什么可说的。输出几乎与您的PHP脚本通过echo、print、var_dump等命令输出的内容相同。

因此,在这个阶段,我们不需要深入研究任何复杂的东西,这里有一个有趣的事实:在生成自己的VM时,PHP需要自己作为一个依赖项。这是因为VM是由PHP脚本生成的,因为它更易于编写和维护。

tcpdump工具
PHP是编译型还是解释型?