1.php是解析型的高级语言,zend内核使用c语言实现,有main函数,php脚本就是输入,内核处理后输出结果,内核将php脚本翻译成c程序可识别的opcode就是php的编译。

c语言的编译将c代码编译成机器码,这些机器码就是操作指令,将指令写入二进制程序load相应的内存区(常量区 数据区 代码区),分配运行栈,开始从代码区依次执行。

php编译差不多,将php脚本解析成opcode,每条opcode就是c的stuct,对应着相应的机器指令,执行过程就是zend引擎执行这些opcode,编译过程包括词法分析、语法分析,就的php版本直接生成opcode,php7新增在语法分析阶段生成抽象语法树,然后生成opcode_array。

2.词法分析、语法分析 (PHP代码->抽象语法树(AST))

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

      re2c: 词法分析器,将输入分割为一个个有意义的词块,称为token

          bison: 语法分析器,确定词法分析器分割出的token是如何彼此关联的

3.opcode_array结构

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

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

opcode的结构

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

handler是每条opcode对应的C语言编写的 处理过程,所有hadler定义在zend_vm_def.h中,有三种不同的提供形式:CALL、SWITCH、GOTO,默认方式为CALL。

每条opcode都有两个操作数, 操作数记录着当前指令的关键信息(操作数类型实际就是个32位整形,它主要用于存储一些变量的索引位置、数值记录等等)。

每个操作都有5种不同的类型 IS_CONST:字面量,IS_TMP_VAR:临时变量, IS_VAR:PHP变量,  IS_CV:PHP脚本变量, IS_UNUSED:表示操作数没有用。

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

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

4.抽象语法树(AST)编译  (AST-> zend_op_array)

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

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

首先从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.  
  5. switch (ast->kind) {
  6. case xxx:
  7. ...
  8. break;
  9. case ZEND_AST_ECHO:
  10. zend_compile_echo(ast);
  11. break;
  12. ...
  13. default:
  14. {
  15. znode result;
  16. zend_compile_expr(&result, ast);
  17. zend_do_free(&result);
  18. }
  19. }
  20.  
  21. if (FC(declarables).ticks && !zend_is_unticked_stmt(ast)) {
  22. zend_emit_tick();
  23. }
  24. }

根据不同的节点类型(kind)作不同的处理。

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

php代码编译的实现的更多相关文章

  1. 如何提升代码编译的速度 iOS

    前阵子有遇到代码编译速度慢的问题,特别是在swift和object-c混编的过程中问题很突显. 网上找到一篇蛮好的文章里面又一些解决方法 推荐一下 http://www.open-open.com/l ...

  2. Java 代码编译和执行的整个过程

    Java 代码编译是由 Java 源码编译器来完成,流程图如下所示: Java 字节码的执行是由 JVM 执行引擎来完成,流程图如下所示: Java 代码编译和执行的整个过程包含了以下三个重要的机制: ...

  3. java-cef系列视频第一集:从官方代码编译

    本视频介绍了如何从官方给出步骤编译java-cef代码,生成可运行可移植的发行版. 值得一提的是:截至2016-09-24java-cef代码编译方式有所改变,读者请自行查看bitbucket上关于编 ...

  4. 利用Roslyn把C#代码编译到内存中并进行执行

    Tugberk Ugurlu在其博文<Compiling C# Code Into Memory and Executing It with Roslyn>中给大家介绍了一种使用.NET下 ...

  5. JVM学习笔记(二)------Java代码编译和执行的整个过程【转】

    转自:http://blog.csdn.net/cutesource/article/details/5904542 版权声明:本文为博主原创文章,未经博主允许不得转载. Java代码编译是由Java ...

  6. JVM学习笔记(二)------Java代码编译和执行的整个过程

    Java代码编译是由Java源码编译器来完成,流程图如下所示: Java字节码的执行是由JVM执行引擎来完成,流程图如下所示: Java代码编译和执行的整个过程包含了以下三个重要的机制: Java源码 ...

  7. xcode针对不同IOS版本的代码编译问题

    有时候在项目中为了兼容低版本IOS系统,通常会针对不同的OS版本写不同的代码,例如: #define IS_IOS7_OR_LATER ([[UIDevice currentDevice].syste ...

  8. 如何把iOS代码编译为Android应用

    新闻 <iPhone 6/6 Plus中国销量曝光:单月销量650万>:据iSuppli Corp.中国研究总监王阳爆料,iPhone 6和iPhone 6 Plus在国内受欢迎的情况大大 ...

  9. Java代码编译和执行的整个过程

    Java代码编译是由Java源码编译器来完成,流程图如下所示: Java字节码的执行是由JVM执行引擎来完成,流程图如下所示: Java代码编译和执行的整个过程包含了以下三个重要的机制: Java源码 ...

  10. Keil C减小代码编译量大小的方法(gai)

    keil-C减小代码编译大小的方法整理 方法一:(通过优化代码减小) 1.1少做乘除运算,使用左/右移位来实现乘除 Eg ,普通:a = 0x80*4: 优化:a = 0x80<<2: 1 ...

随机推荐

  1. 20145221 《Java程序设计》实验报告三:敏捷开发与XP实践

    20145221 <Java程序设计>实验报告三:敏捷开发与XP实践 实验要求 以结对编程的方式编写一个软件,Blog中要给出结对同学的Blog网址 记录TDD和重构的过程,测试代码不要少 ...

  2. Python3基础 print %d 输出整数

             Python : 3.7.0          OS : Ubuntu 18.04.1 LTS         IDE : PyCharm 2018.2.4       Conda ...

  3. JS文档DOM

      访问指定节点 通过document节点获取 document.getElementById(elementId) document.getElementsByName(elementName) d ...

  4. POJ 1061 青蛙的约会(扩展欧几里得算法)

    http://poj.org/problem?id=1061 思路: 搞懂这个扩展欧几里得算法花了不少时间,数论真的是难啊. 含义:找出一对整数,使得ax+by=gcd(a,b). 接下来看这道题目, ...

  5. SRM 585 DIV2

    250pt: 一水... 500pt:题意: 给你一颗满二叉树的高度,然后找出出最少的不想交的路径并且该路径每个节点只经过一次. 思路:观察题目中给的图就会发现,其实每形成一个 就会存在一条路径. 我 ...

  6. 2016"百度之星" - 初赛(Astar Round2B)1003 瞬间移动 组合数学+逆元

    瞬间移动  Accepts: 1018  Submissions: 3620  Time Limit: 4000/2000 MS (Java/Others)  Memory Limit: 65536/ ...

  7. [java]No qualifying bean of type 解决方法

    1.错误原因:注解写错 2.原理如下: 现在的spring早就已经摆脱了之前一堆xml配置文件的情况,都是通过注解配置的方式进行依赖注入了,通常情况下,我们会有一个配置类,然后通过Annotation ...

  8. vue双向绑定原理及实现

    vue双向绑定原理及实现 一.总结 一句话总结:vue中的双向绑定主要是通过发布者-订阅者模式来实现的 发布 订阅 1.单向绑定和双向绑定的区别是什么? model view 更新 单向绑定:mode ...

  9. 这些HTML、CSS知识点,面试和平时开发都需要 No1-No4(知识点:HTML、CSS、盒子模型、内容布局)

    这些HTML.CSS知识点,面试和平时开发都需要 No1-No4   系列知识点汇总 这些HTML.CSS知识点,面试和平时开发都需要 No1-No4(知识点:HTML.CSS.盒子模型.内容布局) ...

  10. torchnet package (2)

    torchnet package (2) torchnet torch7 Dataset Iterators 尽管是用for loop语句很容易处理Dataset,但有时希望以on-the-fly m ...