前言

这一次,我围绕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序列的?

跟踪运行轨迹

我们还是从命令行入手,在$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里边:
留意到zend_op这个结构,于是跟踪发现这个就是最后每条语句对应的opcode结构了!!!!

opcode的结构跟汇编有很大的相似之处,一个操作符,两个操作数。
在Zend引擎中,每个opcode主要的东西就是那个handler,一会我们会看到Zend里边是怎么生成这个handler的。到了这里先Hold住一下,回过头,我们看一下Hello World这个例子生成的opcode是什么。
装上vld,然后运行:php -dvld.active=1 HelloWorld.php,我们就可以看到这个PHP文件编译出来的opcode列表了:
可以看到echo这个语句的opcode类型是ECHO,同时return没有返回值,只有一个操作数"Hello World"。
现在经过了语法分析,我们对每条语句都编译出了opcode,Zend就会把它放入一个op_array里边(其实就是一个opcode的列表)。
回过头我们看一下zend_do_echo做了什么事情:
首先通过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
在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就是:
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行:

可以看到,虚拟机执行的时候会循环当前的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:
从源码中,我们看到了最后的写操作就是调用了write/fwrite写入到标准输出流(也即是终端屏幕上)。

结语

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

PHP-Zend引擎剖析之Hello World(二)相关推荐

  1. pyqt创建窗口没有句柄_Filament 渲染引擎剖析 之 FrameGraph 1 虚拟资源的定义与创建...

    Filament 使用了可扩展渲染管线(FrameGraph)来组织渲染通道和管理渲染资源,网上也搜了下可扩展渲染管线的相关的文章,一般认为可扩展渲染管线是次时代渲染引擎应该具备的比较先进的管线组织架 ...

  2. PHP-Zend引擎剖析之词法分析(一)

    前言 闲来研究一下PHP底层的Zend引擎源码,Zend引擎是PHP脚本的虚拟机. 在PHP上层有SAPI接口,负责对各个接入层的抽象,例如PHP在Apache模块里边的实现,Fast-CGI的实现, ...

  3. 【Iphone 游戏开发】游戏引擎剖析

    为什么80%的码农都做不了架构师?>>>     李华明Himi 原创,转载务必在明显处注明: 转载自[黑米GameDev街区] 原文链接: http://www.himigame. ...

  4. Iphone 游戏引擎剖析

    原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://xiaominghimi.blog.51cto.com/2614927/60699 ...

  5. Filament 渲染引擎剖析 之 FrameGraph 1 虚拟资源的定义与创建

    Filament 使用了可扩展渲染管线(FrameGraph)来组织渲染通道和管理渲染资源,网上也搜了下可扩展渲染管线的相关的文章,一般认为可扩展渲染管线是次时代渲染引擎应该具备的比较先进的管线组织架 ...

  6. Zend引擎探索 之 PHP中前置递增不返回左值

    首先来讲,一般我们对"左值"的理解就是可以出现在赋值运算符的左侧的标识符,也就是可以被赋值.这样讲也许并不十分确切,在不同的语言中对左值的定义也不尽相同.在这里我们讨论前置递增(和 ...

  7. 3D游戏引擎剖析【较全面】

    转自:http://blog.csdn.net/is01sjjj/article/details/430125 第1部分: 游戏引擎介绍, 渲染和构造3D世界 介绍 自Doom游戏时代以来我们已经走了 ...

  8. Filament 渲染引擎剖析 之 FrameGraph 2 动态构建渲染管线

    一.渲染通道的设计与实现 1 Frostbite 构建FrameGraph的准则 我们先看下Frostbite 构建FrameGraph原则,包括三个阶段: 设置阶段 setup.编译阶段compil ...

  9. 【转】游戏引擎剖析(Game Engine Anatomy 101)

    游戏引擎剖析(Game Engine Anatomy 101) 原文作者:Jake Simpson 译者: 向海 Email:GameWorldChina@myway.com 英文原版下载 : 第1部 ...

最新文章

  1. 《Android传感器开发与智能设备案例实战》——导读
  2. start_stop_time
  3. c语言编写程序x的y次方,C语言变为编程y = x-x立方/ 3! + x五次方力量/ 5! -x7th power / 7!...
  4. Eclipse内存分析工具的用法
  5. 【Python】7种方案,彻底实现可视化图片大小/分辨率控制自由
  6. java lambdamart库,LambdaMART 之 lambda(示例代码)
  7. 超赞!这些 “电子” 艺术品,真绝了!
  8. DataGridView 写入到EXCEL
  9. 使用Ultra Librarian 生成PCB库文件
  10. 截至2018年,目前,企业主流的消息中间件有哪些?各有什么优缺点?面试常问 RabbitMQ使用较多
  11. java模板变量_IntelliJ IDEA实时模板变量
  12. Cisco协议学习笔记(VTP)
  13. mysql误删除ibdata1以及日志ib_logfile*
  14. redis集群五种模式(纯概念)
  15. 所有浏览器的 CSS selectors 兼容性
  16. 企业年会活动常用的音频类、视频类工具软件
  17. c语言常见运行错误提示,c语言运行时的错误提示
  18. Python 读取加密后的xlsl
  19. Spark数据分析及处理
  20. python画蝴蝶结_Shapely用户手册

热门文章

  1. [转]如何解决:Android中 Error generating final archive: Debug Certificate expired on 10/09/18 16:30 的错误...
  2. Sql2008发送Email
  3. 关于在Webservice里使用LinqToSQL遇到一对多关系的父子表中子表需要ToList输出泛型而产生循环引用错误的解决办法!(转)...
  4. python详细安装教程linux-在Linux上安装Python3
  5. python怎么加载图片-怎样用python加载dicom图片
  6. python大数据分析实例-Python大数据处理案例
  7. python爬虫教程pdf-《Python爬虫开发与项目实战》pdf完整版
  8. 在哪里学python好-为什么要选择学python,亮点在哪呢?
  9. 大专python工资有多高-最新 | 2019年Python工程师的平均薪资是多少?
  10. python表白代码如何运行-程序员如何实现表白代码