前言
这一次,我围绕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里边:
留意到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内核开发大全》是一份深度探讨PHP扩展内核开发的资料集合,涵盖了PHP的基础、 Zend引擎的工作原理以及如何构建和优化PHP扩展等核心内容。对于想深入了解PHP内部机制和提升开发技能的程序员来说,这是一份极其...
- **基本示例**:通过一个简单的“Hello World”应用程序来演示如何创建和运行 Zend Framework 项目。 **3. Building a Website with the Zend Framework** - **网站架构**:讲解如何使用 Zend Framework 构建完整...
echo 'hello, world.'; ?> ``` 运行时,VLD会输出脚本的opcode和执行路径,帮助我们理解PHP内部的工作机制。 理解PHP的执行周期,不仅有助于我们编写更高效的代码,还能帮助我们排查和修复问题,提升应用程序的安全...
php echo "Hello World"; $a = 1 + 1; echo $a; ?` Scanning 成 Tokens,得到以下结果: ``` Array ( [0] => Array ( [0] => 367 [1] => ?php [2] => 1 ) [1] => Array ( [0] => 370 [1] => [2] => 2 ...
”可以用`<?php echo "Hello World!"; ?>`。 在代码调试方面,首先需要确保`display_errors`设置为`On`,这样PHP运行时的错误会显示出来。通常,使用`echo`或`print`函数来输出变量值进行调试,同时,通过注释代码...
PHP使用 Zend 引擎处理错误和异常,当发生`die()`、`exit()`或严重错误时,会通过`longjmp()`跳转到请求结束的地址,这可能导致内存清理代码被跳过,增加内存泄漏的风险。因此,正确处理错误并确保在异常情况下也能...
例如,简单的“Hello World”程序可以用`<?php echo "Hello World!"; ?>`来实现。 总的来说,PHP作为一门动态网页生成的脚本语言,其简易的学习曲线和丰富的工具支持使其成为初学者和专业开发者的理想选择。通过...
$this->view->message = 'Hello, World!'; // 这会查找/views/scripts/index.phtml 并渲染 } ``` 视图脚本通常位于`/application/views/scripts`目录下,对应于控制器的子目录,例如`views/scripts/index/index....
- **示例代码**:`<?php echo "Hello, World!"; ?>`。 - **运行环境**:确保Apache服务器和PHP已正确安装并配置。 ##### 2.2 PHP代码写法 - **五种写法**: - `<?php ?>`:标准格式。 - `<? ?>`:短标签。 - `...
php -r 'echo "Hello World";' ``` 在PHP命令行中运行代码时,还可以设置一些特殊的代码执行阶段。比如,`-B` 选项允许指定一段代码在开始处理输入行之前执行,`-R` 选项允许对每一输入行执行一段PHP代码,`-E` ...
在扩展开发部分,作者通过“Hello World”示例引导读者进入扩展开发的世界。一个简单的扩展通常需要声明导出函数,这些函数将在PHP脚本中作为自定义函数使用。书中详细介绍了如何编写C代码,构建扩展的结构,以及...
zend_eval_string(script, NULL, "Simple Hello World App" TSRMLS_CC); PHP_EMBED_END_BLOCK(); return 0; } ``` 为了编译这个C程序,你需要包含PHP的头文件并链接到`libphp5.so`: ```makefile CC = gcc ...
PHP执行这段代码会经过如下4个步骤(确切的来说,应该是PHP的语言引擎Zend) 复制代码 代码如下: 1.Scanning(Lexing) (扫描),将PHP代码转换为语言片段(Tokens) 2.Parsing(语法分析), 将Tokens转换成简单而有意义...
1.3.1 第一个PHP程序Hello, world 6 1.3.2 学习PHP应该准备哪些软件 8 1.3.3 相关知识领域的介绍 9 1.4 程序运行环境的搭建 10 1.4.1 Apache简介 10 1.4.2 安装Apache与PHP 10 1.4.3 使用phpinfo()确认Apache与PHP ...
在深入理解PHP之OpCode原理详解中,我们探讨了PHP代码如何从源代码形式转化为机器可执行的指令。OpCode是PHP脚本编译后的一种中间语言,类似于Java的ByteCode或.NET的MSIL,它在PHP的执行过程中起到至关重要的作用。...
例如,字符串"HelloWorld"与"elloWorld"的Levenshtein距离为1,因为只需要删除字符串前的"H"即可。 在PHP中,levenshtein函数的基本语法如下: ```php levenshtein(string1, string2, insert, replace, delete) ``...