设为首页 收藏本站
查看: 512|回复: 0

[经验分享] PHP-Zend引擎剖析之Hello World(二)

[复制链接]
累计签到:1 天
连续签到:1 天
发表于 2017-4-9 06:56:31 | 显示全部楼层 |阅读模式
前言



这一次,我围绕Hello World来展开Zend虚拟机的执行过程。Hello World的PHP版本:


<?php

     echo 'Hello World';

?>


前一篇文章聊到的词法分析阶段就会把上边的脚本分析出一个Token序列:

DSC0000.png

我们得到一个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文件)。

DSC0001.png

DSC0002.png

我们追踪到$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做了些什么事情:

DSC0003.png

我们先看看如何编译出opcode的。





词法语法分析->opcode



从上节知道我们通过zend_compile_file(实际上为compile_file()<定义在Zend/zend_language_scanner.c的555行>)把脚本文件编译出opcode,实际上通过zendparse这个API来编译出opcode的。

DSC0004.png

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里边:

DSC0005.png

留意到zend_op这个结构,于是跟踪发现这个就是最后每条语句对应的opcode结构了!!!!

DSC0006.png opcode的结构跟汇编有很大的相似之处,一个操作符,两个操作数。

在Zend引擎中,每个opcode主要的东西就是那个handler,一会我们会看到Zend里边是怎么生成这个handler的。到了这里先Hold住一下,回过头,我们看一下Hello World这个例子生成的opcode是什么。

装上vld,然后运行:php -dvld.active=1 HelloWorld.php,我们就可以看到这个PHP文件编译出来的opcode列表了:

DSC0007.png

可以看到echo这个语句的opcode类型是ECHO,同时return没有返回值,只有一个操作数"Hello World"。

现在经过了语法分析,我们对每条语句都编译出了opcode,Zend就会把它放入一个op_array里边(其实就是一个opcode的列表)。

回过头我们看一下zend_do_echo做了什么事情:

DSC0008.png

首先通过get_next_op在当前的op_array的最后边生成一条opcode,然后设置其opcode类型是ZEND_ECHO,然后设置它的第一个参数op1,同时标记第二个参数op2是不需要使用的(unused的)。

经过了这么多步骤之后我们得到了一个op_array的列表,这个列表里边的每一条opcode都绑定了自己的类型,接着我们看一下每个opcode节点是如何绑定handler的。

zend_vm_def.h定义了ZEND_ECHO的handler,留意到这里的40,一会需要用到,因为echo的参数可以有几种:常量,变量等等,所以对应着不同的handler

DSC0009.png

在zend_vm_execute.h定义了opcode对应的所有的handler,我们只关注echo相关的handler,注意到其中的代码:


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就是:

DSC00010.png

zend_opcode_handlers[40*25+0*5+0*5] = zend_opcode_handlers[1000] = labels[1000] = ZEND_ECHO_SPEC_CONST_HANDLER(怎么来的?因为:41914行-40913行-1=1000)。

虚拟机执行opcode



前边我们已经解释了zend_compile_file把一个脚本编译成一个opcode的list:


 EG(active_op_array) = zend_compile_file(file_handle, type TSRMLS_CC); zend_execute(EG(active_op_array) TSRMLS_CC);


在这之后,Zend引擎用zend_execute执行返回的opcode。

我们定位到了zend_execute最后执行到Zend/zend_vm_execute.h的337行:





DSC00011.png

可以看到,虚拟机执行的时候会循环当前的opcode列表,然后调用每一行opcode的handler,根据handler返回值确定下一步做啥(例如函数调用等,以后再展开)。

在这篇文章中我们只关注跟Hello World相关的东西,我们前边知道echo的handler是ZEND_ECHO_SPEC_CONST_HANDLER,通过最后的定位你会发现其调用了:


zend_write = (zend_write_func_t) utility_functions->write_function;


这里的utility_functions里边包含了一些基础的handler,每个sapi接入层自己修改了这里的基础函数指针,例如在命令行模式下,最后调用到了

sapi_cli_single_write:

DSC00012.png

从源码中,我们看到了最后的写操作就是调用了write/fwrite写入到标准输出流(也即是终端屏幕上)。

结语



最后根据前边的过程,再展开一下流程图就是:

DSC00013.png

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-362165-1-1.html 上篇帖子: 在PHP中使用全局变量的几种方法 下篇帖子: PHP 实现多服务器共享 SESSION 数据
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表