PHP-Zend引擎剖析之Hello World(二)
前言这一次,我围绕Hello World来展开Zend虚拟机的执行过程。Hello World的PHP版本:<?php? ? ?echo 'Hello World';?>前一篇文章聊到的词法分析阶段就会把上边的脚本分析出一个Token序列:我们得到一个Token序列:T_OPEN_TAG, T_ECHO, T_CONSTANT_ENCAPSED_STRING, ';', T_CLOSE_TAG。但在Zend虚拟机执行的过程中,是怎么去分析这个Token序列的?<!--more-->跟踪运行轨迹我们还是从命令行入手,在$PHPSRC/sapi/cli/php_cli.c中的do_cli函数里边接收了命令行的参数输入(php -f HelloWorld.php表示执行HelloWorld.php文件)。
我们追踪到$PHPSRC/main/main.c里边有php_execute_script的定义,紧接着调用了zend_execute_scripts() <Zend/Zend.c>,在zend_execute_scripts的定义里边我们发现了:?EG(active_op_array) =?zend_compile_file(file_handle, type TSRMLS_CC); zend_execute(EG(active_op_array) TSRMLS_CC);首先通过zend_compile_file把文件解析成opcode中间代码(这一步会经过词法语法分析),然后用zend_execute执行这个生成的中间代码(这里就是所谓的运行时)。这里很像C语言的编译方式,先编译成汇编,然后再转成机器码,这里的opcode就类似C语言编译过程中生成的汇编。还可以延伸出一个思路,因为每次解析PHP文件时,都需要经过词法语法分析得到对应的opcode,其实在脚本文件不变化的时候,生成的opcode也不需要变化,因此为了减少PHP脚本的执行时间,可以把脚本的opcode缓存起来(例如缓存在共享内存里边)。我给出一个流程图,然后随着这个流程图,看看Zend做了些什么事情:
我们先看看如何编译出opcode的。词法语法分析->opcode从上节知道我们通过zend_compile_file(实际上为compile_file()<定义在Zend/zend_language_scanner.c的555行>)把脚本文件编译出opcode,实际上通过zendparse这个API来编译出opcode的。
PHP的语法解析器是用bison来生成,安装完之后在$PHPSRC/Zend目录运行:bison -o zend_language_parser.c?zend_language_parser.y在Zend目录下就会生成语法解析器zend_language_parser.c。而这里的zendparse就是语法解析器里边的yyparse!我们忽略掉生成的语法解析器,就Hello World的例子来跟踪一下bison的声明文件(我去掉不想关的声明):
start:top_statement_list???? { zend_do_end_compilation(TSRMLS_C); };top_statement_list:top_statement_list? { zend_do_extended_info(TSRMLS_C); } top_statement { HANDLE_INTERACTIVE(); }|???? /* empty */;top_statement:statement????????????????????????????? { zend_verify_namespace(TSRMLS_C); };statement:unticked_statement { DO_TICKS(); }|???? T_STRING ':' { zend_do_label(&$1 TSRMLS_CC); };unticked_statement:|???? T_ECHO echo_expr_list ';'echo_expr_list:echo_expr_list ',' expr { zend_do_echo(&$3 TSRMLS_CC); }|???? expr???????????????????????? { zend_do_echo(&$1 TSRMLS_CC); };expr:r_variable???????????????????????? { $$ = $1; }|???? expr_without_variable????????? { $$ = $1; };expr_without_variable:|???? scalar??????????????????? { $$ = $1; }scalar:|???? common_scalar?????????????? { $$ = $1; };common_scalar:|???? T_CONSTANT_ENCAPSED_STRING???? { $$ = $1; };语法分析从start开始,自上而下的分析,一个PHP脚本就是对应一个top_statement_list,接着分成每一行一条语句statement,发现echo 'Hello World'是一条unticked_statement(留意一下echo_expr_list的声明,?我们还可以发现语法上是支持echo 'Hello', ' World'的)。最后递归到T_CONSTANT_ENCAPSED_STRING状态就结束了这一行的语法解析。在这里我们忽略掉编译原理在语法分析阶段是怎么去做回溯等等东西,我们关注一下Zend引擎自身的的问题。在规则后边的块"{}"里边的代码就是用来处理扫描到此规则时的动作,可以看到echo的执行是调用了zend_do_echo函数的。在动作声明的块里边我们看到了$$, $1,$2,$3等,这些对应的就是该条规则里边的返回值,参数1,参数2……,这里的返回值以及参数都是YYSTYPE类型,这个类型在43行里边有定义:#define YYSTYPE znode。znode的定义在zend_compile.h里边:





void zend_init_opcodes_handlers(void) { static const opcode_handler_t labels[] = {//40913行 ZEND_ECHO_SPEC_CONST_HANDLER,//41914行 ZEND_ECHO_SPEC_CONST_HANDLER, ZEND_ECHO_SPEC_CONST_HANDLER, ZEND_ECHO_SPEC_CONST_HANDLER, ZEND_ECHO_SPEC_CONST_HANDLER };
请花费短暂的时间先记住这里的labels以及行数。发现了获取handler的方法最后边return语句的计算,根据前面说的echo的opcode是40(假设两个参数op1,op2的type都是0),于是乎其对应的handler就是:


