PHP-Zend引擎剖析之词法分析(一)
前言
闲来研究一下PHP底层的Zend引擎源码,Zend引擎是PHP脚本的虚拟机。 在PHP上层有SAPI接口,负责对各个接入层的抽象,例如PHP在Apache模块里边的实现,Fast-CGI的实现,命令行的实现。在PHP底层便是Zend虚拟机,Zend虚拟机负责解析PHP语法的文件,上层可以在虚拟机中注册函数/变量提供给虚拟机调用,例如从Apache分发过来的HTTP请求经过PHP的Apache SAPI接口后,便会注册一些$_COOKIE、$_GET等全局变量,而在命令行模式下便没有这些跟HTTP相关的全局变量。 Zend引擎跟其他编译器跟解释器一样,会经历词法分析/语法分析,语法分析后会生成op code,也就是PHP的中间代码,最终Zend虚拟机执行的是op code。第一篇贡献给Zend引擎的理当是词法分析的源码剖析。 PS:分析的代码是PHP-5.5.5的源码包,下载地址:http://windows.php.net/downloads/releases/php-5.5.5-src.zip。词法分析
词法分析阶段就是从输入流里边一个字符一个字符的扫描,识别出对应的词素,最后把源文件转换成为一个TOKEN序列,然后丢给语法分析器。 从词法分析阶段中,词法分析器也能检测到源代码里边的一些错误。例如在Zend引擎的词法分析阶段就有这样一段代码:zend_error(E_COMPILE_WARNING, "Unterminated comment starting line %d", CG(zend_lineno));
当检测到/*开头,但是没有*/结尾时,Zend引擎会抛出一个Waring提示,但是并不影响接下来的词法解析,词法分析阶段一般都不会造成严重的解析错误,因为词法分析阶段的职责就是识别出Token序列而已,它并不需要知道Token跟Token之间是否具备什么联系(那个应该是语法分析阶段的职责)。在Zend引擎的词法分析器中也会抛出致命的解析错误而终止词法分析阶段,如下代码:
zend_error_noreturn(E_COMPILE_ERROR, "Could not convert the script from the detected " "encoding "%s" to a compatible encoding", zend_multibyte_get_encoding_name(LANG_SCNG(script_encoding)));
这个解析错误是因为从输入流里边检测到的代码的编码不合法,显然,这里是应该终止掉整个解析过程的。 Zend引擎的词法分析器re2c来生成,词法分析的阶段会涉及到各个状态,其变量命名均为yy开头(下文会说明)。
源码高亮
我找了一个清晰的流程来分析怎么进入到词法分析阶段的。 我们以命令行的PHP为入口来研究一下,以HelloWorld的例子来看,我们在命令行执行:php -s HelloWorld.php,结果如下:



lex词法分析器
Zend引擎的lex文件位于$PHPSRC/Zend/zend_language_scanner.l,如果你安装了re2c,可以通过以下命令来生成c文件:re2c -F -c -o zend_language_scanner.c zend_language_scanner.l
我们主要剖析的是zend_language_scanner.l文件。在re2c生成的词法解析器中,我认为有两个维度的状态机。第一个维度是字符串的维度来维护的状态,第二个是字符的维度来维护状态。第二个维度的状态机就是字符间状态的跳转,在这里我们忽略之。 例如在Zend引擎中,当扫描到"<?php"时,Zend会将当前第一维度的状态设置为ST_IN_SCRIPTING,表示现在我们已经进入了PHP脚本解析的状态了。这个维度的状态可以很方便的在lex文件中作为各种前置条件,例如在lex文件中有很多这样的声明:

Parse error: syntax error, unexpected "World" (T_STRING), expecting "," or ";" in /home/raphealguo/tmp/HelloWorld.php on line 2
在词法解析器扫描字符的过程中,需要记录扫描过程的各个参数以及当前状态,这些变量都是以yy开头命名。常用到的就是:yy_state, yy_text, yyleng, yy_cursor, yy_limit 各个变量的状态扫描前后的变化示意图。 扫描echo前:

扫描echo后:

通过一个字符一个字符的扫描最终会得到一个Token序列,然后交由语法分析器去解析,接着就是剖析Zend引擎的lex文件规则是怎么写的了。
lex文件剖析
Zend词法解析状态
Zend引擎在做词法解析时会自己维护扫描过程的状态,其实就是将yy_text等变量自己封装一个结构体,我们可以在lex文件中看到很多SCNG的宏调用,例如:SCNG(yy_start) = YYCURSOR; 定位一下#define SCNG,可以发现在lex文件的91行有这样的宏定义:/* Globals Macros */#define SCNG LANG_SCNG
我们重新定位到#define LANG_SCNG在文件$PHPSRC/Zend/zend_globals_macros.h中的第56行(我们忽略52行ZTS的判断,这是一个线程安全的宏定义):
# define LANG_SCNG(v) (language_scanner_globals.v) //这里可以看到实际上在扫描过程中 都是调全局扫描状态的属性,例如SCNG(yy_start)相当于language_scanner_globals.yy_startextern ZEND_API zend_php_scanner_globals language_scanner_globals;#endif
可以看到Zend引擎维护了一个zend_php_scanner_globals的结构体(实际上在27行里边是一个typedef的重命名,本来是叫做_zend_php_scanner_globals这个结构体),_zend_php_scanner_globals这个结构体的定义在$PHPSRC/Zend/zend_globals.h,可以看到其结构有部分跟原来lex扫描器的变量是一致的,但是它好包装了一些堆栈,还有输入输出流(解析PHP文件时不一定是文件输入流,也有可能从终端输入的命令,所以这里包装一个输入输出流是很合理的)。



#define YYGETCONDITION() SCNG(yy_state)#define YYSETCONDITION(s) SCNG(yy_state) = s #define BEGIN(state) YYSETCONDITION(STATE(state))static void _yy_push_state(int new_state TSRMLS_DC){//将当前状态压栈,然后重设当前状态为新状态zend_stack_push(&SCNG(state_stack), (void *) &YYGETCONDITION(), sizeof(int));YYSETCONDITION(new_state);}
进入PHP解析状态
我们知道PHP是嵌入式的,只有包含在<?php ?>或者<? ?>标签中的字符才会被执行解析,在lex文件的1732-1805行就是扫描<?php这样起始标签的规则声明,源码如下:


PHP注释
接着我们看一下PHP里边注释是怎么扫描的。先找到1919行关于单行注释的规则声明:

PHP数字类型
从一开始的正则规则里边可以知道PHP支持5中类型的数字常量声明:

PHP变量类型
PHP的变量是以美元符$开头,从词法规则里边可以看到:


PHP字符串类型
PHP的字符串类型在词法分析阶段应该是最复杂的,PHP里边的字符串可以由单引号跟双引号来围住,单引号的字符串比双引号的字符串效率会更高,一会我们可以看到为什么。 先来看一下单引号的规则:





PHP魔术变量
PHP魔术变量分为编译时替换以及运行时替换,词法规则文件里边的1593-1722行定义了以下魔术变量: __CLASS__, __TRAIT__, __FUNCTION__, __METHOD__, __LINE__, __FILE__, __DIR__, __NAMESPACE__ 魔术变量的剖析留到之后再写,留意到__contruct这类并不在词法声明的规则里边出现。PHP的容错机制
在前边说单行注释的时候已经描述了一种容错机制,在语法文件的1490行,2432行均有词法分析阶段的容错机制。

结语
文章中还忽略了单字符的词素(规则位于1454行)以及强制类型转换的规则(例如:(int)$str, 规则位于1230行),Zend引擎的在词法分析阶段开始前还会检查文件的编码问题以及文件流的操作问题,之后再找篇文章细细研究一下这两块的内容。最后不由得不感叹一下,尽管对编译原理的熟悉程度不高,但是re2c的书写出来的规则真心容易懂。声明:该文观点仅代表作者本人,入门客AI创业平台信息发布平台仅提供信息存储空间服务,如有疑问请联系rumenke@qq.com。
- 上一篇:没有了
- 下一篇: PHP-Zend引擎剖析之Hello World(二)