PHP是解析型高级语言,事实上从Zend内核的角度来看PHP就是一个普通的C程序,它有main函数,我们写的PHP代码是这个程序的输入,然后经过内核的处理输出结果,内核将PHP代码”翻译”为C程序可识别的过程就是PHP的编译。

那么这个”翻译”过程具体都有哪些操作呢?

C程序在编译时将一行行代码编译为机器码,每一个操作都认为是一条机器指令,这些指令写入到编译后的二进制程序中,执行的时候将二进制程序load进相应的内存区域(常量区、数据区、代码区)、分配运行栈,然后从代码区起始位置开始执行,这是C程序编译、执行的简单过程。

同样,PHP的编译与普通的C程序类似,只是PHP代码没有编译成机器码,而是解析成了若干条opcode数组,每条opcode就是C里面普通的struct,含义对应C程序的机器指令,执行的过程就是引擎依次执行opcode,比如我们在PHP里定义一个变量:$a = 123;,最终到内核里执行就是malloc一块内存,然后把值写进去。

所以PHP的解析过程任务就是将PHP代码转化为opcode数组,代码里的所有信息都保存在opcode中,然后将opcode数组交给zend引擎执行,opcode就是内核具体执行的命令,比如赋值、加减操作、函数调用等,每一条opcode都对应一个处理handle,这些handler是提前定义好的C函数。

从PHP代码到opcode是怎么实现的?最容易想到的方式就是正则匹配,当然过程没有这么简单。PHP编译过程包括词法分析、语法分析,使用re2c、bison完成,旧的PHP版本直接生成了opcode,PHP7新增了抽象语法树(AST),在语法分析阶段生成AST,然后再生成opcode数组。
zend_compile2.png

PHP编译阶段的基本过程如下图:
zend_compile_process.png

后面两个小节将看下 PHP代码->AST->Opcodes 的具体编译过程。

3.1.1 词法解析、语法解析

这一节我们分析下PHP的解析阶段,即 PHP代码->抽象语法树(AST) 的过程。

PHP使用re2c、bison完成这个阶段的工作:

  • re2c: 词法分析器,将输入分割为一个个有意义的词块,称为token
  • bison: 语法分析器,确定词法分析器分割出的token是如何彼此关联的

例如:

  1. $a = 2 + 3;

词法分析器将上面的语句分解为这些token:$a、=、2、+、3,接着语法分析器确定了2+3是一个表达式,而这个表达式被赋值给了a,我们可以这样定义词法解析规则:

  1. /*!re2c
  2. LABEL [a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*
  3. LNUM [0-9]+
  4. //规则
  5. "$"{LABEL} {return T_VAR;}
  6. {LNUM} {return T_NUM;}
  7. */

然后定义语法解析规则:

  1. //token定义
  2. %token T_VAR
  3. %token T_NUM
  4. //语法规则
  5. statement:
  6. T_VAR '=' T_NUM '+' T_NUM {ret = str2int($3) + str2int($5);printf("%d",ret);}
  7. ;

上面的语法规则只能识别两个数值相加,假如我们希望支持更复杂的运算,比如:

  1. $a = 3 + 4 - 6;

则可以配置递归规则:

  1. //语法规则
  2. statement:
  3. T_VAR '=' expr {}
  4. ;
  5. expr:
  6. T_NUM {...}
  7. |expr '?' T_NUM {}
  8. ;

这样将支持若干表达式,用语法分析树表示:
zend_parse_1.png

接下来我们看下PHP具体的解析过程,PHP编译阶段流程:

zend_compile_process.png

其中 zendparse() 就是词法、语法解析过程,这个函数实际就是bison中提供的语法解析函数 yyparse()

  1. #define yyparse zendparse

yyparse() 不断调用 yylex() 得到token,然后根据token匹配语法规则:

zend_parse_2.png

  1. #define yylex zendlex
  2. //zend_compile.c
  3. int zendlex(zend_parser_stack_elem *elem)
  4. {
  5. zval zv;
  6. int retval;
  7. ...
  8. again:
  9. ZVAL_UNDEF(&zv);
  10. retval = lex_scan(&zv);
  11. if (EG(exception)) {
  12. //语法错误
  13. return T_ERROR;
  14. }
  15. ...
  16. if (Z_TYPE(zv) != IS_UNDEF) {
  17. //如果在分割token中有zval生成则将其值复制到zend_ast_zval结构中
  18. elem->ast = zend_ast_create_zval(&zv);
  19. }
  20. return retval;
  21. }

这里两个关键点需要注意:

(1) token值:词法解析器解析到的token值内容就是token值,这些值统一通过 zval 存储,上面的过程中可以看到调用lex_scan参数是是个zval*,在具体的命中规则总会将解析到的token保存到这个值,从而传递给语法解析器使用,比如PHP中的解析变量的规则:$a;,其词法解析规则为:

  1. <ST_IN_SCRIPTING,ST_DOUBLE_QUOTES,ST_HEREDOC,ST_BACKQUOTE,ST_VAR_OFFSET>"$"{LABEL} {
  2. //将匹配到的token值保存在zval中
  3. zend_copy_value(zendlval, (yytext+1), (yyleng-1)); //只保存{LABEL}内容,不包括$,所以是yytext+1
  4. RETURN_TOKEN(T_VARIABLE);
  5. }

zendlval就是我们传入的zval*,yytext指向命中的token值起始位置,yyleng为token值的长度。

(2) 语义值类型:bison调用re2c分割token有两个含义,第一个是token类型,另一个是token值,token类型一般以yylex的返回值告诉bison,而token值就是语义值,这个值一般定义为固定的类型,这个类型就是语义值类型,默认为int,可以通过 YYSTYPE 定义,而PHP中这个类型是 zend_parser_stack_elem ,这就是为什么zendlex的参数为zend_parser_stack_elem的原因。

  1. #define YYSTYPE zend_parser_stack_elem
  2. typedef union _zend_parser_stack_elem {
  3. zend_ast *ast; //抽象语法树主要结构
  4. zend_string *str;
  5. zend_ulong num;
  6. } zend_parser_stack_elem;

实际这是个union,ast类型用的比较多(其它两种类型暂时没发现有地方在用),这样可以通过%token、%type将对应的值修改为elem.ast,所以在zend_language_parser.y中使用的$$、$1、$2……多数都是 zend_parser_stack_elem.ast

  1. %token <ast> T_LNUMBER "integer number (T_LNUMBER)"
  2. %token <ast> T_DNUMBER "floating-point number (T_DNUMBER)"
  3. %token <ast> T_STRING "identifier (T_STRING)"
  4. %token <ast> T_VARIABLE "variable (T_VARIABLE)"
  5. %type <ast> top_statement namespace_name name statement function_declaration_statement
  6. %type <ast> class_declaration_statement trait_declaration_statement
  7. %type <ast> interface_declaration_statement interface_extends_list

语法解析器从start开始调用,然后层层匹配各个规则,语法解析器根据命中的语法规则创建AST节点,最后将生成的AST根节点赋到 CG(ast)

  1. %% /* Rules */
  2. start:
  3. top_statement_list { CG(ast) = $1; }
  4. ;
  5. top_statement_list:
  6. top_statement_list top_statement { $$ = zend_ast_list_add($1, $2); }
  7. | /* empty */ { $$ = zend_ast_create_list(0, ZEND_AST_STMT_LIST); }
  8. ;

首先会创建一个根节点list,然后将后面不断命中top_statement生成的ast加到这个list中,zend_ast具体结构:

  1. enum _zend_ast_kind {
  2. ZEND_AST_ZVAL = 1 << ZEND_AST_SPECIAL_SHIFT,
  3. ZEND_AST_ZNODE,
  4. /* list nodes */
  5. ZEND_AST_ARG_LIST = 1 << ZEND_AST_IS_LIST_SHIFT,
  6. ...
  7. };
  8. struct _zend_ast {
  9. zend_ast_kind kind; /* Type of the node (ZEND_AST_* enum constant) */
  10. zend_ast_attr attr; /* Additional attribute, use depending on node type */
  11. uint32_t lineno; /* Line number */
  12. zend_ast *child[1]; /* Array of children (using struct hack) */
  13. };
  14. typedef struct _zend_ast_list {
  15. zend_ast_kind kind;
  16. zend_ast_attr attr;
  17. uint32_t lineno;
  18. uint32_t children;
  19. zend_ast *child[1];
  20. } zend_ast_list;

根节点实际为zend_ast_list,每条语句对应的ast保存在child中,使用中zend_ast_list、zend_ast可以相互转化,kind标识的是ast节点类型,后面会根据这个值生成具体的opcode,另外函数、类还会用到另外一种ast节点结构:

  1. typedef struct _zend_ast_decl {
  2. zend_ast_kind kind;
  3. zend_ast_attr attr; /* Unused - for structure compatibility */
  4. uint32_t start_lineno; //开始行号
  5. uint32_t end_lineno; //结束行号
  6. uint32_t flags;
  7. unsigned char *lex_pos;
  8. zend_string *doc_comment;
  9. zend_string *name;
  10. zend_ast *child[4]; //类中会将继承的父类、实现的接口以及类中的语句解析保存在child中
  11. } zend_ast_decl;

这么看比较难理解,接下来我们从一个简单的例子看下最终生成的语法树。

  1. $a = 123;
  2. $b = "hi~";
  3. echo $a,$b;

具体解析过程这里不再解释,有兴趣的可以翻下zend_language_parse.y中,这个过程不太容易理解,需要多领悟几遍,最后生成的ast如下图:

zend_ast.png

总结:

这一节我们主要介绍了PHP词法、语法解析生成抽象语法树(AST)的过程,此过程是PHP语法实现的基础,也是zend引擎非常关键的一部分,后续介绍的内容都是基于此过程的产出结果展开的。这部分内容关键在于对re2c、bison的应用上,如果是初次接触它们可能不太容易理解,这里不再对re2c、bison作更多解释,想要了解更多的推荐看下 《flex与bison》 这本书。

3.1.2 抽象语法树编译流程

上一小节我们简单介绍了从PHP代码解析为抽象语法树的过程,这一节我们再介绍下从 抽象语法树->Opcodes 的过程。

语法解析过程的产物保存于CG(AST),接着zend引擎会把AST进一步编译为 zend_op_array ,它是编译阶段最终的产物,也是执行阶段的输入,后面我们介绍的东西基本都是围绕zend_op_array展开的,AST解析过程确定了当前脚本定义了哪些变量,并为这些变量 顺序编号 ,这些值在使用时都是按照这个编号获取的,另外也将变量的初始化值、调用的函数/类/常量名称等值(称之为字面量)保存到zend_op_array.literals中,这些字面量也有一个唯一的编号,所以执行的过程实际就是根据各指令调用不同的C函数,然后根据变量、字面量、临时变量的编号对这些值进行处理加工。

我们首先看下zend_op_array的结构,明确几个关键信息,然后再看下ast编译为zend_op_array的过程。

3.1.2.1 zend_op_array数据结构

PHP主脚本会生成一个zend_op_array,每个function也会编译为独立的zend_op_array,所以从二进制程序的角度看zend_op_array包含着当前作用域下的所有堆栈信息,函数调用实际就是不同zend_op_array间的切换。

zend_compile.png

  1. struct _zend_op_array {
  2. //common是普通函数或类成员方法对应的opcodes快速访问时使用的字段,后面分析PHP函数实现的时候会详细讲
  3. ...
  4. uint32_t *refcount;
  5. uint32_t this_var;
  6. uint32_t last;
  7. //opcode指令数组
  8. zend_op *opcodes;
  9. //PHP代码里定义的变量数:op_type为IS_CV的变量,不含IS_TMP_VAR、IS_VAR的
  10. //编译前此值为0,然后发现一个新变量这个值就加1
  11. int last_var;
  12. //临时变量数:op_type为IS_TMP_VAR、IS_VAR的变量
  13. uint32_t T;
  14. //PHP变量名数组
  15. zend_string **vars; //这个数组在ast编译期间配合last_var用来确定各个变量的编号,非常重要的一步操作
  16. ...
  17. //静态变量符号表:通过static声明的
  18. HashTable *static_variables;
  19. ...
  20. //字面量数量
  21. int last_literal;
  22. //字面量(常量)数组,这些都是在PHP代码定义的一些值
  23. zval *literals;
  24. //运行时缓存数组大小
  25. int cache_size;
  26. //运行时缓存,主要用于缓存一些znode_op以便于快速获取数据,后面单独介绍这个机制
  27. void **run_time_cache;
  28. void *reserved[ZEND_MAX_RESERVED_RESOURCES];
  29. };

zend_op_array.opcodes指向指令列表,具体每条指令的结构如下:

  1. struct _zend_op {
  2. const void *handler; //指令执行handler
  3. znode_op op1; //操作数1
  4. znode_op op2; //操作数2
  5. znode_op result; //返回值
  6. uint32_t extended_value;
  7. uint32_t lineno;
  8. zend_uchar opcode; //opcode指令
  9. zend_uchar op1_type; //操作数1类型
  10. zend_uchar op2_type; //操作数2类型
  11. zend_uchar result_type; //返回值类型
  12. };
  13. //操作数结构
  14. typedef union _znode_op {
  15. uint32_t constant;
  16. uint32_t var;
  17. uint32_t num;
  18. uint32_t opline_num; /* Needs to be signed */
  19. uint32_t jmp_offset;
  20. } znode_op;

opcode各字段含义下面展开说明。

3.1.2.1.1 handler

handler为每条opcode对应的C语言编写的 处理过程 ,所有opcode对应的处理过程定义在zend_vm_def.h中,值得注意的是这个文件并不是编译时用到的,因为opcode的 处理过程 有三种不同的提供形式:CALL、SWITCH、GOTO,默认方式为CALL,这个是什么意思呢?

每个opcode都代表了一些特定的处理操作,这个东西怎么提供呢?一种是把每种opcode负责的工作封装成一个function,然后执行器循环执行即可,这就是CALL模式的工作方式;另外一种是把所有opcode的处理方式通过C语言里面的label标签区分开,然后执行器执行的时候goto到相应的位置处理,这就是GOTO模式的工作方式;最后还有一种方式是把所有的处理方式写到一个switch下,然后通过case不同的opcode执行具体的操作,这就是SWITCH模式的工作方式。

假设opcode数组是这个样子:

  1. int op_array[] = {
  2. opcode_1,
  3. opcode_2,
  4. opcode_3,
  5. ...
  6. };

各模式下的工作过程类似这样:

  1. //CALL模式
  2. void opcode_1_handler() {...}
  3. void opcode_2_handler() {...}
  4. ...
  5. void execute(int []op_array)
  6. {
  7. void *opcode_handler_list[] = {&opcode_1_handler, &opcode_2_handler, ...};
  8. while(1){
  9. void handler = opcode_handler_list[op_array[i]];
  10. handler(); //call handler
  11. i++;
  12. }
  13. }
  14. //GOTO模式
  15. void execute(int []op_array)
  16. {
  17. while(1){
  18. goto opcode_xx_handler_label;
  19. }
  20. opcode_1_handler_label:
  21. ...
  22. opcode_2_handler_label:
  23. ...
  24. ...
  25. }
  26. //SWITCH模式
  27. void execute(int []op_array)
  28. {
  29. while(1){
  30. switch(op_array[i]){
  31. case opcode_1:
  32. ...
  33. case opcode_2:
  34. ...
  35. ...
  36. }
  37. i++;
  38. }
  39. }

三种模式效率是不同的,GOTO最快,怎么选择其它模式呢?下载PHP源码后不要直接编译,Zend目录下有个文件:zend_vm_gen.php,在编译PHP前执行:php zend_vm_gen.php --with-vm-kind=CALL|SWITCH|GOTO,这个脚本将重新生成:zend_vm_opcodes.hzend_vm_opcodes.czend_vm_execute.h三个文件覆盖原来的,然后再编译PHP即可。

后面分析的过程使用的都是默认模式CALL,也就是opcode对应的handler为一个函数指针,编译时opcode对应的handler是如何根据opcode索引到的呢?

opcode的数值各不相同,同时可以根据两个zend_op的类型设置不同的处理handler,因此每个opcode指令最多有20个(25去掉重复的5个)对应的处理handler,所有的handler按照opcode数值的顺序定义在一个大数组中:zend_opcode_handlers,每25个为同一个opcode,如果对应的op_type类型handler则可以设置为空:

  1. //zend_vm_execute.h
  2. void zend_init_opcodes_handlers(void)
  3. {
  4. static const void *labels[] = {
  5. ZEND_NOP_SPEC_HANDLER,
  6. ZEND_NOP_SPEC_HANDLER,
  7. ...
  8. };
  9. zend_opcode_handlers = labels;
  10. }

索引的算法:

  1. //zend_vm_execute.h
  2. static const void *zend_vm_get_opcode_handler(zend_uchar opcode, const zend_op* op)
  3. {
  4. //因为op_type为2的倍数,所以这里做了下转化,转成了0-4
  5. static const int zend_vm_decode[] = {
  6. _UNUSED_CODE, /* 0 */
  7. _CONST_CODE, /* 1 = IS_CONST */
  8. _TMP_CODE, /* 2 = IS_TMP_VAR */
  9. _UNUSED_CODE, /* 3 */
  10. _VAR_CODE, /* 4 = IS_VAR */
  11. _UNUSED_CODE, /* 5 */
  12. _UNUSED_CODE, /* 6 */
  13. _UNUSED_CODE, /* 7 */
  14. _UNUSED_CODE, /* 8 = IS_UNUSED */
  15. _UNUSED_CODE, /* 9 */
  16. _UNUSED_CODE, /* 10 */
  17. _UNUSED_CODE, /* 11 */
  18. _UNUSED_CODE, /* 12 */
  19. _UNUSED_CODE, /* 13 */
  20. _UNUSED_CODE, /* 14 */
  21. _UNUSED_CODE, /* 15 */
  22. _CV_CODE /* 16 = IS_CV */
  23. };
  24. //根据op1_type、op2_type、opcode得到对应的handler
  25. return zend_opcode_handlers[opcode * 25 + zend_vm_decode[op->op1_type] * 5 + zend_vm_decode[op->op2_type]];
  26. }
  27. ZEND_API void zend_vm_set_opcode_handler(zend_op* op)
  28. {
  29. //设置zend_op的handler,这个操作是在编译期间完成的
  30. op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op);
  31. }
  32. #define _CONST_CODE 0
  33. #define _TMP_CODE 1
  34. #define _VAR_CODE 2
  35. #define _UNUSED_CODE 3
  36. #define _CV_CODE 4

3.1.2.1.2 操作数(znode_op)

操作数类型实际就是个32位整形,它主要用于存储一些变量的索引位置、数值记录等等。

  1. typedef union _znode_op {
  2. uint32_t constant;
  3. uint32_t var;
  4. uint32_t num;
  5. uint32_t opline_num; /* Needs to be signed */
  6. uint32_t jmp_offset;
  7. } znode_op;

每条opcode都有两个操作数(不一定都用到),操作数记录着当前指令的关键信息,可以用于变量的存储、访问,比如赋值语句:”3.1 php代码的编译 - 图8a”、”45”的存储位置,执行时根据op2取到值”45”,然后赋值给”3.1 php代码的编译 - 图9a”的位置通过op1获取到。当然操作数并不是全部这么用的,上面只是赋值时候的情况,其它操作会有不同的用法,如函数调用时的传参,op1记录的就是传递的参数是第几个,op2记录的是参数的存储位置,result记录的是函数接收参数的存储位置。

3.1.2.1.3 操作数类型(op_type)

每个操作都有5种不同的类型:

  1. #define IS_CONST (1<<0) //1
  2. #define IS_TMP_VAR (1<<1) //2
  3. #define IS_VAR (1<<2) //4
  4. #define IS_UNUSED (1<<3) //8
  5. #define IS_CV (1<<4) //16
  • IS_CONST:字面量,编译时就可确定且不会改变的值,比如:$a = “hello”就是常量
  • IS_TMP_VAR:临时变量,比如:3.1 php代码的编译 - 图10%EF%BC%8C%E5%85%B6%E4%B8%AD%60%22hello~%22%20.%20time()%60%E7%9A%84%E5%80%BC%E7%B1%BB%E5%9E%8B%E5%B0%B1%E6%98%AFIS_TMP_VAR%EF%BC%8C%E5%86%8D%E6%AF%94%E5%A6%82%3A#card=math&code=a%20%3D%20%22hello~%22%20.%20time%28%29%EF%BC%8C%E5%85%B6%E4%B8%AD%60%22hello~%22%20.%20time%28%29%60%E7%9A%84%E5%80%BC%E7%B1%BB%E5%9E%8B%E5%B0%B1%E6%98%AFIS_TMP_VAR%EF%BC%8C%E5%86%8D%E6%AF%94%E5%A6%82%3A)a = “123” + $b,"123" + $b的结果类型也是IS_TMP_VAR,从这两个例子可以猜测,临时变量多是执行期间其它类型组合现生成的一个中间值,由于它是现生成的,所以把IS_TMP_VAR赋值给IS_CV变量时不会增加其引用计数
  • IS_VAR:PHP变量,这个很容易认为是PHP脚本里的变量,其实不是,这里PHP变量的含义可以这样理解:PHP变量是没有显式的在PHP脚本中定义的,不是直接在代码通过$var_name定义的。这个类型最常见的例子是PHP函数的返回值,再如$a[0]数组这种,它取出的值也是IS_VAR,再比如$$a这种
  • IS_UNUSED:表示操作数没有用
  • IS_CV:PHP脚本变量,即脚本里通过$var_name定义的变量,这些变量是编译阶段确定的,所以是compile variable,

result_type除了上面几种类型外还有一种类型EXT_TYPE_UNUSED (1<<5),返回值没有使用时会用到,这个跟IS_UNUSED的区别是:IS_UNUSED表示本操作返回值没有意义(也可简单的认为没有返回值),而EXT_TYPE_UNUSED的含义是有返回值,但是没有用到,比如函数返回值没有接收。

3.1.2.1.4 字面量、变量的存储

我们先想一下C程序是如何读写字面量、变量的。

  1. #include <stdio.h>
  2. int main()
  3. {
  4. char *name = "pangudashu";
  5. printf("%s\n", name);
  6. return 0;
  7. }

我们知道指针name分配在栈上,而”pangudashu”分配在常量区,那么”name”变量名分配在哪呢?

实际上C里面是不会存变量名称的,编译的过程会将变量名替换为偏移量表示:ebp - 偏移量esp + 偏移量,将上面的代码转为汇编:

  1. .LC0:
  2. .string "pangudashu"
  3. .text
  4. .globl main
  5. .type main, @function
  6. main:
  7. .LFB0:
  8. pushq %rbp
  9. movq %rsp, %rbp
  10. subq $16, %rsp
  11. movq $.LC0, -8(%rbp)
  12. movq -8(%rbp), %rax
  13. movq %rax, %rdi
  14. call puts
  15. movl $0, %eax
  16. leave

可以看到movq $.LC0, -8(%rbp),而-8(%rbp)就是name变量。

虽然PHP代码不会直接编译为机器码,但编译、执行的设计跟C程序是一致的,也有常量区、变量也通过偏移量访问、也有虚拟的执行栈。

php_vs_c.png

在编译时就可确定且不会改变的量称为字面量,也称作常量(IS_CONST),这些值在编译阶段就已经分配zval,保存在zend_op_array->literals数组中(对应c程序的常量存储区),访问时通过_zend_op_array->literals + 偏移量读取,举个例子:

  1. <?php
  2. $a = 56;
  3. $b = "hello";

56通过(zval*)(_zend_op_array->literals + 0)取到,hello通过(zval*)(_zend_op_array->literals + 16)取到,具体变量的读写操作将在执行阶段详细分析,这里只分析编译阶段的操作。

3.1.2.2 AST->zend_op_array

上面我们介绍了zend_op_array结构,接下来我们回过头去看下语法解析(zendparse())之后的流程:

  1. ZEND_API zend_op_array *compile_file(zend_file_handle *file_handle, int type)
  2. {
  3. zend_op_array *op_array = NULL; //编译出的opcodes
  4. ...
  5. if (open_file_for_scanning(file_handle)==FAILURE) {//文件打开失败
  6. ...
  7. } else {
  8. zend_bool original_in_compilation = CG(in_compilation);
  9. CG(in_compilation) = 1;
  10. CG(ast) = NULL;
  11. CG(ast_arena) = zend_arena_create(1024 * 32);
  12. if (!zendparse()) { //语法解析
  13. zval retval_zv;
  14. zend_file_context original_file_context; //保存原来的zend_file_context
  15. zend_oparray_context original_oparray_context; //保存原来的zend_oparray_context,编译期间用于记录当前zend_op_array的opcodes、vars等数组的总大小
  16. zend_op_array *original_active_op_array = CG(active_op_array);
  17. op_array = emalloc(sizeof(zend_op_array)); //分配zend_op_array结构
  18. init_op_array(op_array, ZEND_USER_FUNCTION, INITIAL_OP_ARRAY_SIZE);//初始化op_array
  19. CG(active_op_array) = op_array; //将当前正在编译op_array指向当前
  20. ZVAL_LONG(&retval_zv, 1);
  21. if (zend_ast_process) {
  22. zend_ast_process(CG(ast));
  23. }
  24. zend_file_context_begin(&original_file_context); //初始化CG(file_context)
  25. zend_oparray_context_begin(&original_oparray_context); //初始化CG(context)
  26. zend_compile_top_stmt(CG(ast)); //AST->zend_op_array编译流程
  27. zend_emit_final_return(&retval_zv); //设置最后的返回值
  28. op_array->line_start = 1;
  29. op_array->line_end = CG(zend_lineno);
  30. pass_two(op_array);
  31. zend_oparray_context_end(&original_oparray_context);
  32. zend_file_context_end(&original_file_context);
  33. CG(active_op_array) = original_active_op_array;
  34. }
  35. ...
  36. }
  37. ...
  38. return op_array;
  39. }

compile_file()操作中有几个保存原来值的操作,这是因为这个函数在PHP脚本执行中并不会只执行一次,主脚本执行时会第一次调用,而include、require也会调用,所以需要先保存当前值,然后执行完再还原回去。

AST->zend_op_array编译是在 zend_compile_top_stmt() 中完成,这个函数是总入口,会被多次递归调用:

  1. //zend_compile.c
  2. void zend_compile_top_stmt(zend_ast *ast)
  3. {
  4. if (!ast) {
  5. return;
  6. }
  7. if (ast->kind == ZEND_AST_STMT_LIST) { //第一次进来一定是这种类型
  8. zend_ast_list *list = zend_ast_get_list(ast);
  9. uint32_t i;
  10. for (i = 0; i < list->children; ++i) {
  11. zend_compile_top_stmt(list->child[i]);//list各child语句相互独立,递归编译
  12. }
  13. return;
  14. }
  15. //各语句编译入口
  16. zend_compile_stmt(ast);
  17. if (ast->kind != ZEND_AST_NAMESPACE && ast->kind != ZEND_AST_HALT_COMPILER) {
  18. zend_verify_namespace();
  19. }
  20. //function、class两种情况的处理,非常关键的一步操作,后面分析函数、类实现的章节再详细分析
  21. if (ast->kind == ZEND_AST_FUNC_DECL || ast->kind == ZEND_AST_CLASS) {
  22. CG(zend_lineno) = ((zend_ast_decl *) ast)->end_lineno;
  23. zend_do_early_binding(); //很重要!!!
  24. }
  25. }

首先从AST的根节点开始编译,根节点类型为ZEND_AST_STMT_LIST,这个类型表示当前节点下有多个独立的节点,各child都是独立的语句生成的节点,所以依次编译即可,直到到达有效节点位置(非ZEND_AST_STMT_LIST节点),然后调用zend_compile_stmt编译当前节点:

  1. void zend_compile_stmt(zend_ast *ast)
  2. {
  3. CG(zend_lineno) = ast->lineno;
  4. switch (ast->kind) {
  5. case xxx:
  6. ...
  7. break;
  8. case ZEND_AST_ECHO:
  9. zend_compile_echo(ast);
  10. break;
  11. ...
  12. default:
  13. {
  14. znode result;
  15. zend_compile_expr(&result, ast);
  16. zend_do_free(&result);
  17. }
  18. }
  19. if (FC(declarables).ticks && !zend_is_unticked_stmt(ast)) {
  20. zend_emit_tick();
  21. }
  22. }

主要根据不同的节点类型(kind)作不同的处理,我们不会把每种类型的处理都讲一遍,这里还是根据上一节最后的例子挑几个看下具体的处理过程。

  1. $a = 123;
  2. $b = "hi~";
  3. echo $a,$b;

zendparse()阶段生成的AST:

zend_ast.png

下面的过程比较复杂,有的函数会多次递归调用,我们根据例子一步步去看下,如果你对PHP各个语法实现比较熟悉再去看整个AST的编译过程就会比较轻松。

(1)、 首先从根节点开始,有3个child,第一个节点类型为ZEND_AST_ASSIGN,zend_compile_stmt()中走到default分支

(2)、 ZEND_AST_ASSIGN类型由zend_compile_expr()处理:

  1. void zend_compile_expr(znode *result, zend_ast *ast)
  2. {
  3. CG(zend_lineno) = zend_ast_get_lineno(ast);
  4. switch (ast->kind) {
  5. case ZEND_AST_ZVAL:
  6. ZVAL_COPY(&result->u.constant, zend_ast_get_zval(ast));
  7. result->op_type = IS_CONST;
  8. return;
  9. case ZEND_AST_VAR:
  10. zend_compile_var(result, ast, BP_VAR_R);
  11. return;
  12. case ZEND_AST_ASSIGN:
  13. zend_compile_assign(result, ast);
  14. return;
  15. ...
  16. }
  17. }

继续进入zend_compile_assign():

  1. void zend_compile_assign(znode *result, zend_ast *ast)
  2. {
  3. zend_ast *var_ast = ast->child[0]; //变量名
  4. zend_ast *expr_ast = ast->child[1];//变量值表达式
  5. znode var_node, expr_node;
  6. zend_op *opline;
  7. uint32_t offset;
  8. if (is_this_fetch(var_ast)) { //检查变量名是否为this,变量名不能是this
  9. zend_error_noreturn(E_COMPILE_ERROR, "Cannot re-assign $this");
  10. }
  11. //比如这样写:my_function() = 123;即:将函数的返回值作为变量名将报错
  12. zend_ensure_writable_variable(var_ast);
  13. switch (var_ast->kind) {
  14. case ZEND_AST_VAR:
  15. case ZEND_AST_STATIC_PROP:
  16. offset = zend_delayed_compile_begin();
  17. zend_delayed_compile_var(&var_node, var_ast, BP_VAR_W); //生成变量名的znode,这个结构只在这个地方临时用,所以直接分配在stack上
  18. zend_compile_expr(&expr_node, expr_ast); //递归编译变量值表达式,最终需要得到一个ZEND_AST_ZVAL的节点
  19. zend_delayed_compile_end(offset);
  20. zend_emit_op(result, ZEND_ASSIGN, &var_node, &expr_node); //生成一条op
  21. return;
  22. ...
  23. }
  24. }

这个地方主要有三步关键操作:

第1步: 变量赋值操作有两部分:变量名、变量值,所以首先是针对变量名的操作,介绍zend_op_array时曾提到每个PHP变量都有一个编号,变量的读写都是根据这个编号操作的,这个编号最早就是这一步生成的。

zend_lookup_cv.png

中间过程我们不再细看,这里重点看下变量编号的过程,这个过程比较简单,每发现一个变量就遍历zend_op_array.vars数组,看此变量是否已经保存,没有保存的话则存入vars,然后后续变量的使用都是用的这个变量在数组中的下标,比如第一次定义的时候:$a = 123;将$a编号为0,然后:echo $a;再次使用时会遍历vars,发现已经存在,直接用其下标操作$a。

  1. static int lookup_cv(zend_op_array *op_array, zend_string* name)
  2. {
  3. int i = 0;
  4. zend_ulong hash_value = zend_string_hash_val(name);
  5. //遍历op_array.vars检查此变量是否已存在
  6. while (i < op_array->last_var) {
  7. if (ZSTR_VAL(op_array->vars[i]) == ZSTR_VAL(name) ||
  8. (ZSTR_H(op_array->vars[i]) == hash_value &&
  9. ZSTR_LEN(op_array->vars[i]) == ZSTR_LEN(name) &&
  10. memcmp(ZSTR_VAL(op_array->vars[i]), ZSTR_VAL(name), ZSTR_LEN(name)) == 0)) {
  11. zend_string_release(name);
  12. return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);
  13. }
  14. i++;
  15. }
  16. //这是一个新变量
  17. i = op_array->last_var;
  18. op_array->last_var++;
  19. if (op_array->last_var > CG(context).vars_size) {
  20. CG(context).vars_size += 16; /* FIXME */
  21. op_array->vars = erealloc(op_array->vars, CG(context).vars_size * sizeof(zend_string*));//扩容vars
  22. }
  23. op_array->vars[i] = zend_new_interned_string(name);
  24. return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i); //传NULL时返回的是96 + i*sizeof(zval)
  25. }

注意:这里变量的编号从0、1、2、3…依次递增的,但是实际使用中并不是直接用的这个下标,而是转化成了内存偏移量offset,这个是ZEND_CALL_VAR_NUM宏处理的,所以变量偏移量实际是96、112、128…递增的,这个96是根据zend_execute_data大小设定的(不同的平台下对应的值可能不同),下一篇介绍zend执行流程时会详细介绍这个结构。

  1. #define ZEND_CALL_FRAME_SLOT \
  2. ((int)((ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval)) - 1) / ZEND_MM_ALIGNED_SIZE(sizeof(zval))))
  3. #define ZEND_CALL_VAR_NUM(call, n) \
  4. (((zval*)(call)) + (ZEND_CALL_FRAME_SLOT + ((int)(n))))

第2步: 编译变量值表达式,再次调用zend_compile_expr()编译,示例中的情况比较简单,expr_ast.kind为ZEND_AST_ZVAL:

  1. void zend_compile_expr(znode *result, zend_ast *ast)
  2. {
  3. switch (ast->kind) {
  4. case ZEND_AST_ZVAL:
  5. ZVAL_COPY(&result->u.constant, zend_ast_get_zval(ast)); //将变量值复制到znode.u.constant中
  6. result->op_type = IS_CONST; //类型为IS_CONST,这种value后面将会保存在zend_op_array.literals中
  7. return;
  8. ...
  9. }
  10. }

第3步: 上面两步已经分别生成了变量赋值的op1、op2,下面就是根据这俩值生成opcode的过程。

  1. static zend_op *zend_emit_op(znode *result, zend_uchar opcode, znode *op1, znode *op2)
  2. {
  3. zend_op *opline = get_next_op(CG(active_op_array)); //当前zend_op_array下生成一条新的指令
  4. opline->opcode = opcode;
  5. //将op1、op2内容拷贝到zend_op中,设置op_type
  6. //如果znode.op_type == IS_CONST,则会将znode.u.contstant值转移到zend_op_array.literals中
  7. if (op1 == NULL) {
  8. SET_UNUSED(opline->op1);
  9. } else {
  10. SET_NODE(opline->op1, op1);
  11. }
  12. if (op2 == NULL) {
  13. SET_UNUSED(opline->op2);
  14. } else {
  15. SET_NODE(opline->op2, op2);
  16. }
  17. //如果此指令有返回值则想变量那样为返回值编号(后面分配局部变量时将根据这个编号索引)
  18. if (result) {
  19. zend_make_var_result(result, opline);
  20. }
  21. return opline;
  22. }
  23. static inline void zend_make_var_result(znode *result, zend_op *opline)
  24. {
  25. opline->result_type = IS_VAR; //返回值类型固定为IS_VAR
  26. opline->result.var = get_temporary_variable(CG(active_op_array)); //为返回值编个号,这个编号记在临时变量T上,上面介绍zend_op_array时说过T、last_var的区别
  27. GET_NODE(result, opline->result);
  28. }

到这我们示例中的第1条赋值语句就算编译完了,第2条同样是赋值,过程与上面相同,我们直接看最好一条输出的语句。

(3)、 echo语句的编译:echo $a,$b;实际从编译后的语法树就可以看出,一次echo多个也被编译为多次echo了,所以示例中的用法与:echo $a; echo $b;等价,我们只分析其中一个就可以了。

zend_ast_echo.png

zend_compile_stmt()中首先发现节点类型是ZEND_AST_STMT_LIST,然后调用zend_compile_stmt_list()分别编译child,具体的流程如下图所示:

zend_ast_echo_p.png

最后生成zend_op的过程:

  1. void zend_compile_echo(zend_ast *ast)
  2. {
  3. zend_op *opline;
  4. zend_ast *expr_ast = ast->child[0];
  5. znode expr_node;
  6. zend_compile_expr(&expr_node, expr_ast);
  7. opline = zend_emit_op(NULL, ZEND_ECHO, &expr_node, NULL);//生成1条新的opcode
  8. opline->extended_value = 0;
  9. }

最终zend_compile_top_stmt()编译完成后整个编译流程基本是完成了,CG(active_op_array)结构如下图所示,但是后面还有一个处理pass_two()

zend_op_array_2.png

  1. ZEND_API int pass_two(zend_op_array *op_array)
  2. {
  3. zend_op *opline, *end;
  4. if (!ZEND_USER_CODE(op_array->type)) {
  5. return 0;
  6. }
  7. //重置一些CG(context)的值,暂且忽略
  8. ...
  9. opline = op_array->opcodes;
  10. end = opline + op_array->last;
  11. while (opline < end) {
  12. switch(opline->opcode){
  13. //这里对一些操作进行针对性的处理,后面有遇到的情况我们再看
  14. ...
  15. }
  16. //如果是IS_CONST会将数组下标转化为内存偏移量,与IS_CV那种处理方式相同
  17. //所以这里实际就是将0、1、2...转为为16、32、48...(即:编号*sizeof(zval))
  18. if (opline->op1_type == IS_CONST) {
  19. ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline->op1);
  20. } else if (opline->op1_type & (IS_VAR|IS_TMP_VAR)) {
  21. //上面作相同的处理,不同的是这里的起始值是接着IS_CV的
  22. opline->op1.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->op1.var);
  23. }
  24. //与op1完全相同
  25. if (opline->op2_type == IS_CONST) {
  26. ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline->op2);
  27. } else if (opline->op2_type & (IS_VAR|IS_TMP_VAR)) {
  28. opline->op2.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->op2.var);
  29. }
  30. //返回值与op1/2相同处理
  31. if (opline->result_type & (IS_VAR|IS_TMP_VAR)) {
  32. opline->result.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->result.var);
  33. }
  34. //设置此opcode的处理handler
  35. ZEND_VM_SET_OPCODE_HANDLER(opline);
  36. opline++;
  37. }
  38. //标识当前op_array已执行过此操作
  39. op_array->fn_flags |= ZEND_ACC_DONE_PASS_TWO;
  40. return 0;
  41. }

抛开特殊opcode的处理,pass_two()主要有两个重要操作:

  • (1)将IS_CONST、IS_VAR、IS_TMP_VAR类型的操作数、返回值转化为内存偏移量,与上面提到的IS_CV变量的处理一样,其中IS_CONST类型起始值为0,然后按照编号依次递增sizeof(zval),而IS_VAR、IS_TMP_VAR唯一的不同时它的初始值接着IS_CV的,简单的讲就是先安排PHP变量的,然后接着才是各条语句的中间值、返回值
  • (2)另外一个重要操作就是设置各指令的处理handler,这个前面《3.1.2.1.1 handler》已经介绍过其索引规则

经过pass_two()处理后opcodes的样子:

zend_op_array_3.png

总结:

到这里整个PHP编译阶段就算全部完成了,最终编译的结果就是zend_op_array,其中最核心的操作就是AST的编译了,有兴趣的可以多写几个例子去看下不同节点类型的处理方式。

另外,编译阶段很关键的一个操作就是确定了各个 变量、中间值、临时值、返回值、字面量内存编号 ,这个地方非常重要,后面介绍执行流程时也会用到。