本文共 3709 字,大约阅读时间需要 12 分钟。
在上一篇《》文章中已经介绍了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 =<<
函数token_get_all()可以将一段PHP代码 Scanning成tokens,分割后的结果如下:
array(19) { [0]=> array(3) { [0]=> int(367) [1]=> string(6) " 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_CVb)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了???) 参考: 参考:转载地址:https://blog.csdn.net/lml200701158/article/details/52295747 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!