往期精选(欢迎转发~~)
- 如何看待程序员35岁职业危机?
- Java全套学习资料(14W字),耗时半年整理
- 我肝了三个月,为你写出了GO核心手册
- 消息队列:从选型到原理,一文带你全部掌握
- 肝了一个月的ETCD,从Raft原理到实践
- 更多…
在上一篇《深入理解PHP原理之PHP脚本执行原理》文章中已经介绍了PHP的语法分析、词法分析和Opcodes的原理,下面主要通过一个具体的示例,来看看ZE是怎样对PHP脚本进行语法分析和词法分析,并将其编译为Opcodes,下面让我们回顾一下PHP脚本执行的基本流程:
1. Zend Engine(ZE)调用词法分析器(Lex生成的,源码路径:php/Zend/zend_language_sanner.l), 将我们要执行的PHP源文件去掉空格和注释后,分割成一个个的token;
2. ZE会将得到的token forward给语法分析器(yacc生成, 源码路径:php/Zend/zend_language_parser.y),将Tokens转换成简单而有意义的表达式;
3. ZE会将转换后的表达式,编译为一个个opcode,opcode一般会以op_array的形式存在,它是PHP执行的中间语言。
4. ZE调用zend_executor来执行op_array,输出结果。
下面通过具体示例来讲解上述过程,还是以经典的“hello world”为示例,如下:
$code =<<<PHP_CODE
<?php
$str = "Hello World";
$a = 1 + 1;
echo $a;
PHP_CODE;
var_dump(token_get_all($code));
函数token_get_all()可以将一段PHP代码 Scanning成tokens,分割后的结果如下:
array(19) {
[0]=>
array(3) {
[0]=>
int(367)
[1]=>
string(6) "<?php
"
[2]=>
int(1)
}
[1]=>
array(3) {
[0]=>
int(370)
[1]=>
string(1) " "
[2]=>
int(2)
}
[2]=>
string(1) "="
[3]=>
array(3) {
[0]=>
int(370)
[1]=>
string(1) " "
[2]=>
int(2)
}
[4]=>
array(3) {
[0]=>
int(315)
[1]=>
string(13) ""Hello World""
[2]=>
int(2)
}
[5]=>
string(1) ";"
[6]=>
array(3) {
[0]=>
int(370)
[1]=>
string(2) "
"
[2]=>
int(2)
}
[7]=>
string(1) "="
[8]=>
array(3) {
[0]=>
int(370)
[1]=>
string(1) " "
[2]=>
int(3)
}
[9]=>
array(3) {
[0]=>
int(305)
[1]=>
string(1) "1"
[2]=>
int(3)
}
[10]=>
array(3) {
[0]=>
int(370)
[1]=>
string(1) " "
[2]=>
int(3)
}
[11]=>
string(1) "+"
[12]=>
array(3) {
[0]=>
int(370)
[1]=>
string(1) " "
[2]=>
int(3)
}
[13]=>
array(3) {
[0]=>
int(305)
[1]=>
string(1) "1"
[2]=>
int(3)
}
[14]=>
string(1) ";"
[15]=>
array(3) {
[0]=>
int(370)
[1]=>
string(1) "
"
[2]=>
int(3)
}
[16]=>
array(3) {
[0]=>
int(316)
[1]=>
string(4) "echo"
[2]=>
int(4)
}
[17]=>
array(3) {
[0]=>
int(370)
[1]=>
string(1) " "
[2]=>
int(4)
}
[18]=>
string(1) ";"
}
通过上面的返回结果,我们发现第一步“词法分析”将php脚本分割成数组array,该数组中包括php脚本中的空格、字符、操作数、语句等。在第二步“语法分析”中,数组array中的操作符、语句和标签等会根据文件php/Zend/zend_language_parser.y转换成tokens(下面是该文件部分源码,包含echo、while等关键字的tokens转换),但是字符串,字符,空格等不会,转换后的内容包含两个部分的Array:Token ID (也就是在Zend内部的改Token的对应码)和源码中的原来的内容。
%token T_IF "if (T_IF)"
%token T_ELSEIF "elseif (T_ELSEIF)"
%token T_ELSE "else (T_ELSE)"
%token T_ENDIF "endif (T_ENDIF)"
%token T_ECHO "echo (T_ECHO)"
%token T_DO "do (T_DO)"
%token T_WHILE "while (T_WHILE)"
然后就是Parsing阶段,Parsing首先会丢弃Tokens Array中的多余的空格,然后将剩余的Tokens转换成一个个简单的表达式,Parsing后的结果大致如下:
* ZEND_ECHO 'Hello World' // echo a constant string
* ZEND_ADD ~0 1 1 // add two numbers together
* ZEND_ASSIGN !0 ~0 // store the result of the prior expression to a variable
* ZEND_ECHO !0 // echo a variable
第三步就是Compilation阶段,它会把Parsing后的每个表达式编译成一个个zend_op,让我们回顾一下zend_op的结构:
struct _zend_op {
opcode_handler_t handler; // 执行该opcode时调用的处理函数
znode result; // 执行完后的返回结果
znode op1; // 操作数1
znode op2; // 操作数2
ulong extended_value; // 执行时可能还需要用到的其它信息
uint lineno;
zend_uchar opcode; // opcode代码
};
第四步就是将Compilation阶段生成的zend_op整合成op_array,然后就可以通过zend_executor执行,整个过程就结束了,你可能会问了,我们的$a去那里了?下面就直接引用鸟哥的解释,因为最后一个地方我也没看懂 /(ㄒoㄒ)/~~
这个要介绍操作数了,每个操作数都是由以下俩个部分组成:
a)op_type : 为IS_CONST, IS_TMP_VAR, IS_VAR, IS_UNUSED, or IS_CV
b)u,一个联合体,根据op_type的不同,分别用不同的类型保存了这个操作数的值(const)或者左值(var)
对于var来说,每个var也不一样:
IS_TMP_VAR,顾名思义,这个是一个临时变量,保存一些op_array的结果,以便接下来的op_array使用,这种操作数的u保存着一个指向变量表的一个句柄(整数),这种操作数一般用开头,比如0表示变量表的0号未知的临时变量;
IS_VAR 这种就是我们一般意义上的变量了,他们以$开头表示;
IS_CV 表示ZE2.1/PHP5.1以后的编译器使用的一种cache机制,这种变量保存着被它引用的变量的地址,当一个变量第一次被引用的时候,就会被CV起来,以后对这个变量的引用就不需要再次去查找active符号表了,CV变量以!开头表示。
这么看来,我们的a被优化成!0了。(这里没看懂,为什么变成!0了???)
参考:http://www.laruence.com/2008/06/18/221.html
参考:http://www.php-internals.com/