├── 1.1.md ├── 1.2.md ├── 1.3.md ├── 1.4.md ├── 1.5.md ├── 1.6.md ├── README.md └── learn-material.md /1.1.md: -------------------------------------------------------------------------------- 1 | 1.1 PHP生命周期 2 | ------------- 3 | PHP开始执行以后会经过两个主要的阶段:处理请求之前的开始阶段和请求之后的结束阶段。 4 | 5 | 开始阶段有两个过程: 6 | 7 | 第一个过程是模块初始化阶段(MINIT),在整个SAPI生命周期内(例如Apache启动以后的整个生命周期内或者命令行程序整个执行过程中), 该过程只进行一次。 8 | 9 | 第二个过程是模块激活阶段(RINIT),该过程发生在请求阶段,例如通过url请求某个页面,则在每次请求处理php文件之前都会进行模块激活 10 | (RINIT请求开始)。例如PHP注册了一些扩展模块,则在MINIT阶段会回调所有模块的MINIT函数。 11 | 12 | 1) 模块在这个阶段可以进行一些初始化工作,例如注册常量,定义模块使用的类等。 13 | 14 | 2) 请求到达之后PHP初始化执行脚本的基本环境,例如创建一个执行环境,包括保存PHP运行过程中变量名称和值内容的符号表, 以及 15 | 当前所有的函数以及类等信息的符号表。然后PHP会调用所有模块的RINIT函数, 在这个阶段各个模块也可以执行一些相关的操作,模块 16 | 的RINIT函数和MINIT回调函数。 17 | 18 | 3)请求处理完后就进入了结束阶段,一般脚本执行到末尾或者通过调用exit()或die()函数, PHP都将进入结束阶段。和开始阶段对应, 19 | 结束阶段也分为两个环节,一个在请求结束后停用模块(RSHUTDOWN,对应RINIT), 一个在SAPI生命周期结束(Web服务器退出或者命令行 20 | 脚本执行完毕退出)时关闭模块(MSHUTDOWN,对应MINIT)。 21 | 22 | 下图展示了单进程SAPI生命周期: 23 |

24 | 25 | 1)启动 26 | 27 | 在调用每个模块的模块初始化前,会有一个初始化的过程,它包括: 28 | 29 | 初始化Zend引擎和核心组件、解析php.ini(比较关心)等操作。 30 | 31 | 2)ACTIVATION 32 | 33 | 激活Zend引擎、激活SAPI、环境初始化、模块请求初始化 34 | 35 | 3)运行 36 | 37 | php_execute_script函数包含了运行PHP脚本的全部过程 38 | 39 | 当一个PHP文件需要解析执行时,它可能会需要执行三个文件,其中包括一个前置执行文件、当前需要执行的主文件和一个后置执行文件。 非当前的两个文件可以在php.ini文件通过auto_prepend_file参数和auto_append_file参数设置。 如果将这两个参数设置为空,则禁用对应的执行文件。 40 | 41 | 对于需要解析执行的文件,通过zend_compile_file(compile_file函数)做词法分析、语法分析和中间代码生成操作,返回此文件的所有中间代码。 如果解析的文件有生成有效的中间代码,则调用zend_execute(execute函数)执行中间代码。 如果在执行过程中出现异常并且用户有定义对这些异常的处理,则调用这些异常处理函数。 在所有的操作都处理完后,PHP通过EG(return_value_ptr_ptr)返回结果。 42 | 43 | 3)DEACTIVATION 44 | 45 | PHP关闭请求的过程是一个若干个关闭操作的集合,这个集合存在于php_request_shutdown函数中。 46 | 47 | 具体参考:http://tipi.my.phpcloud.com/book/?p=chapt02/02-01-php-life-cycle-and-zend-engine 48 | -------------------------------------------------------------------------------- /1.2.md: -------------------------------------------------------------------------------- 1 | 1.2 SAPI接口 2 | ------------- 3 | 前言: 4 | 上一节给大家讲述了,在php底层在执行php代码的流程。我们平时只关注编码,但是根本不清楚php是怎么运行的。上一节就详细介绍了这些 5 | 内容。这一节开始给大家介绍SAPI接口。 6 | 7 | SAPI:Server Application Programming Interface 服务器端应用编程端口。SAPI提供了一个接口,使得PHP可以和其他应用进行交互数据。 8 | 常见的给apache的mod_php5,CGI,给IIS的ISAPI,还有Shell的CLI。 9 | 10 | 11 | 12 | SAPI的简单示意图 13 | 14 | 详细请参考:http://www.nowamagic.net/librarys/veda/detail/1285 15 | -------------------------------------------------------------------------------- /1.3.md: -------------------------------------------------------------------------------- 1 | 1.3 PHP代码如何被执行? 2 | ------------- 3 | 前言: 4 | 5 | 第一节和第二节分别讲述了php代码执行过程中php底层的流程以及php被执行的几种方式。本节具体讲PHP代码是如何被执行的。 6 | 7 | 众所周知,计算机的CPU只能执行二进制的机器码,每种CPU都有对应的汇编语言,汇编语言编译器将汇编语言翻译成二进制的 8 | 机器语言,然后CPU开始执行这些机器码。汇编语言作为机器语言与程序设计者之间的一个层,给我们带来了很多方便,程序员 9 | 不需要用晦涩的01数字来书写程序,当然人们并不满足这样的一个进步,于是在汇编语言之上又多了一个层-C语言,C语言更贴 10 | 近人类熟悉的“自然语言”,程序设计者可以通过C语言编译器将C源代码文件编译成目标文件(二进制文件,中间会先翻译成汇编 11 | 语言,然后由汇编语言生成机器码),然后将各个目标文件连接在一起就组成了一个可执行文件。 12 | 13 | PHP语言就是在C语言之上的一个层,PHP引擎是由C语言来实现的,因此PHP语言这一个在C之上抽象出来的层使用起来比C更简单 14 | 方便,入门门槛更低。 15 | 16 | PHP语言到C语言之间的转换如果使用“翻译”这个词是不够准确的,因为引擎不是将PHP语言转换成C语言,然后将转换后的C语言 17 | 编译链接执行。引擎在解析PHP代码的时候通常是分为两个部分,编译和执行: 18 | 19 | 编译阶段:引擎把PHP代码转换成op code中间代码 20 | 21 | 执行阶段:引擎解释并执行编译阶段产生的op code 22 | 23 | 24 | 25 | 如上图,php代码通过词法分析和语法分析转换成opcode。然后通过zend engine执行opcode。 26 | 27 | 此处注意的是,为了提高运行效率,可以用xcache、apc等工具将opcode缓存起来,这样每次执行就不需要都去编译了。 28 | -------------------------------------------------------------------------------- /1.4.md: -------------------------------------------------------------------------------- 1 | 1.4 PHP的opcode 2 | ------------- 3 | 1、简述php的运行机制 4 | 5 | a).php文件通过浏览器过来 6 | 7 | b)请求交给SAPI,随后SAPI层将控制权转给PHP 8 | 9 | c)zend_language_scanner对代码进行扫描,对php代码进行词法分析转换成一系列的tokens array 10 | 11 | d)zend_language_parser将c步骤产生的一系列tokens处理掉空格等无用的代码以后转换成一系列表达式 12 | 13 | e)经过compiler阶段生成opcode返回zend_op_array指针 14 | 15 | f)zend_vm_execute根据传入的zend_op_array指针,执行opcode并将结果返回输出 16 | 17 | 2、实例讲解 18 | ``` 19 | 24 | ``` 25 | PHP执行这段代码会经过如下4个步骤(确切的来说,应该是PHP的语言引擎Zend) 26 | 1.Scanning(Lexing) ,将PHP代码转换为语言片段(Tokens) 27 | 2.Parsing, 将Tokens转换成简单而有意义的表达式 28 | 3.Compilation, 将表达式编译成Opocdes 29 | 4.Execution, 顺次执行Opcodes,每次一条,从而实现PHP脚本的功能。 30 | 31 | 对应的tokens,可以通过token_get_all函数得到 32 | ``` 33 | Array 34 | ( 35 | [0] => Array 36 | ( 37 | [0] => 367 38 | [1] => Array 39 | ( 40 | [0] => 316 41 | [1] => echo 42 | ) 43 | [2] => Array 44 | ( 45 | [0] => 370 46 | [1] => 47 | ) 48 | [3] => Array 49 | ( 50 | [0] => 315 51 | [1] => "Hello World" 52 | ) 53 | [4] => ; 54 | [5] => Array 55 | ( 56 | [0] => 370 57 | [1] => 58 | ) 59 | [6] => = 60 | [7] => Array 61 | ( 62 | [0] => 370 63 | [1] => 64 | ) 65 | [8] => Array 66 | ( 67 | [0] => 305 68 | [1] => 1 69 | ) 70 | [9] => Array 71 | ( 72 | [0] => 370 73 | [1] => 74 | ) 75 | [10] => + 76 | [11] => Array 77 | ( 78 | [0] => 370 79 | [1] => 80 | ) 81 | [12] => Array 82 | ( 83 | [0] => 305 84 | [1] => 1 85 | ) 86 | [13] => ; 87 | [14] => Array 88 | ( 89 | [0] => 370 90 | [1] => 91 | ) 92 | [15] => Array 93 | ( 94 | [0] => 316 95 | [1] => echo 96 | ) 97 | [16] => Array 98 | ( 99 | [0] => 370 100 | [1] => 101 | ) 102 | [17] => ; 103 | ) 104 | ``` 105 | 分析这个返回结果我们可以发现,源码中的字符串,字符,空格,都会原样返回。每个源代码中的字符,都会出现在相应的顺序处。而其他的比如标签,操作符,语句,都会被转换成一个包含俩部分的Array: Token ID (也就是在Zend内部的改Token的对应码,比如,T_ECHO,T_STRING),和源码中的原来的内容。 106 | 接下来,就是Parsing阶段了,Parsing首先会丢弃Tokens Array中的多于的空格,然后将剩余的Tokens转换成一个一个的简单的表达式 107 | ``` 108 | 1.echo a constant string 109 | 2.add two numbers together 110 | 3.store the result of the prior expression to a variable 111 | 4.echo a variable 112 | ``` 113 | 然后就改Compilation阶段了,它会把Tokens编译成一个个op_array, 每个op_arrayd包含如下5个部分: 114 | ``` 115 | 1.Opcode数字的标识,指明了每个op_array的操作类型,比如add , echo 116 | 2.结果 存放Opcode结果 117 | 3.操作数1 给Opcode的操作数 118 | 4.操作数2 119 | 5.扩展值 1个整形用来区别被重载的操作符 120 | ``` 121 | 比如,我们的PHP代码会被Parsing成: 122 | ``` 123 | * ZEND_ECHO 'Hello World' 124 | * ZEND_ADD ~0 1 1 125 | * ZEND_ASSIGN !0 ~0 126 | * ZEND_ECHO !0 127 | ``` 128 | 129 | 呵呵,你可能会问了,我们的$a去那里了? 130 | 131 | 恩,这个要介绍操作数了,每个操作数都是由以下俩个部分组成 132 | 133 | a)op_type : 为IS_CONST, IS_TMP_VAR, IS_VAR, IS_UNUSED, or IS_CV 134 | 135 | b)u,一个联合体,根据op_type的不同,分别用不同的类型保存了这个操作数的值(const)或者左值(var) 136 | 137 | 而对于var来说,每个var也不一样 138 | 139 | IS_TMP_VAR, 顾名思义,这个是一个临时变量,保存一些op_array的结果,以便接下来的op_array使用,这种的操作数的u保存着一个指向变量表的一个句柄(整数),这种操作数一般用~开头,比如~0,表示变量表的0号未知的临时变量 140 | 141 | IS_VAR 这种就是我们一般意义上的变量了,他们以$开头表示 142 | 143 | IS_CV 表示ZE2.1/PHP5.1以后的编译器使用的一种cache机制,这种变量保存着被它引用的变量的地址,当一个变量第一次被引用的时候,就会被CV起来,以后对这个变量的引用就不需要再次去查找active符号表了,CV变量以!开头表示。 144 | 145 | 这么看来,我们的$a被优化成!0了。 146 | 147 | 参考:http://www.laruence.com/2008/06/18/221.html 148 | 149 | http://www.nowamagic.net/librarys/veda/detail/1324 150 | -------------------------------------------------------------------------------- /1.5.md: -------------------------------------------------------------------------------- 1 | 1.5 PHP执行opcode 2 | ------------- 3 | PHP代码编译之后会生成许多的op,每一个op都是一个zend_op类型的c变量。相关的定义可以在{PHPSRC}/Zend/zend_compile.h中 4 | ``` 5 | struct _zend_op { 6 | opcode_handler_t handler; 7 | znode result; 8 | znode op1; 9 | znode op2; 10 | ulong extended_value; 11 | uint lineno; 12 | zend_uchar opcode; 13 | }; 14 | 15 | typedef struct _zend_op zend_op; 16 | ``` 17 | 简单的说说这几个字段: 18 | 1:result,op1,op2 19 | 这三个字段都是znode类型,它们是op的操作数和操作结果载体,当然并不是每个op都需要使用这三个字段,根据op的功能不同,会使 20 | 用其中某些字段。比如类型为ZEND_ECHO的op值需要使用op1,功能就是将op1中的相应的值输出。一会再单独介绍znode类型。 21 | 22 | 2:opcode 23 | opcode的类型为zend_uchar, zend_uchar实际上就是unsigned char,此字段保存的整形值即为op的编号,用来区分不同的op类型,opcode的可取值都被定义成了宏,可以在{PHPSRC}/Zend/zend_vm_opcodes.h中看到这些宏的定义,类似如下: 24 | ``` 25 | #define ZEND_NOP 0 26 | #define ZEND_ADD 1 27 | #define ZEND_SUB 2 28 | #define ZEND_MUL 3 29 | #define ZEND_DIV 4 30 | #define ZEND_MOD 5 31 | #define ZEND_SL 6 32 | #define ZEND_SR 7 33 | #define ZEND_CONCAT 8 34 | #define ZEND_BW_OR 9 35 | #define ZEND_BW_AND 10 36 | ``` 37 | 3:handler 38 | op的执行句柄,其类型为opcode_handler_t,opcode_handler_t的类型定义为typedef int (ZEND_FASTCALL *opcode_handler_t) (ZEND_OPCODE_HANDLER_ARGS); 这个函数指针为op定义了执行方式,每一种opcode字段都对应一个种类的handler,比如opcode= 38 (ZEND_ASSIGN), 那么其对应的handler对应的就是static int ZEND_FASTCALL ZEND_ASSIGN_**种类的handler,根据op操作数类型的不同,可以确定到这个种类中的某一个具体的函数,比如如果$a = 1;这样的代码生成的op,操作数为const和cv,最后就能确定handler为函数ZEND_ASSIGN_SPEC_CV_CONST_HANDLER,这些handler函数都定义在{PHPSRC}/Zend/zend_vm_execute.h中,此文件可以由一个PHP脚本生成,其中也定义了通过op来映射得到其hander的算法。 39 | 40 | 4:lineno 41 | op对应源代码文件中的行号 42 | 43 | 5:extended_value 44 | 扩展字段暂时不介绍 45 | 46 | 操作数znode简介 47 | 操作数字段是这个类型中比较重要的部分了,其中op1,op2,result三个操作数定义为znode类型,znode相关定义在此文件中: 48 | ``` 49 | typedef struct _znode { 50 | int op_type; 51 | union { 52 | zval constant; 53 | 54 | zend_uint var; 55 | zend_uint opline_num; /* Needs to be signed */ 56 | zend_op_array *op_array; 57 | zend_op *jmp_addr; 58 | struct { 59 | zend_uint var; /* dummy */ 60 | zend_uint type; 61 | } EA; 62 | } u; 63 | } znode; 64 | ``` 65 | znode类型中定义了两个字段: 66 | 1:op_type 67 | 这个int类型的字段定义znode操作数的类型,这些类型的可取值的宏定义在此文件中 68 | ``` 69 | #define IS_CONST (1<<0) 70 | #define IS_TMP_VAR (1<<1) 71 | #define IS_VAR (1<<2) 72 | #define IS_UNUSED (1<<3) /* Unused variable */ 73 | #define IS_CV (1<<4) /* Compiled variable */ 74 | ``` 75 | 76 | IS_CONST: 77 | 表示常量,例如$a = 123; $b = "hello";这些代码生成OP后,123和"hello"都是以常量类型操作数存在。 78 | IS_TMP_VAR: 79 | 表示临时变量,临时变量一般在前面加~来表示,这是一些OP执行过程中需要用到的中间变量,例如初始化一个数组的时候,就需要一个临时变量来暂时存储数组zval,然后将数组赋值给变量。 80 | 81 | IS_VAR: 82 | 一般意义上的变量,以$开发表示,此种变量本人目前研究的较少,暂不介绍 83 | 84 | IS_UNUSED : 85 | 暂时不介绍,从名字来看应该是标识为不使用 86 | 87 | IS_CV: 88 | 这种类型的操作数比较重要,此类型是在PHP后来的版本中(大概5.1)中才出现,CV的意思是compiled variable,即编译后的变量,变量都是保存在一个符号表中,这个符号表是一个哈希表,试想如果每次读写变量的时候都需要到哈希表中去检索,势必会对效率有一定的影响,因此在执行上下文环境中,会将一些编译期间生成的变量缓存起来,此过程以后再详细介绍。此类型操作数一般以!开头表示,比如变量$a=123;$b="hello"这段代码,$a和$b对应的操作数可能就是!0和!1, 0和1相当于一个索引号,通过索引号从缓存中取得相应的值。 89 | 90 | 2:u 91 | 此字段为一个联合体,根据op_type的不同,u取不同的值。比如op_type=IS_CONST的时候,u中的constant保存的就是操作数对应的zval结构。例如$a=123时,123这个操作数中,u中的constant是一个IS_LONG类型的zval,其值lval为123。 92 | 93 | 那么引擎是如何通过这些op handler实现PHP语言的特性的呢? 94 | ``` 95 | 98 | ``` 99 | 通过某种方法(以后再介绍这些方法)我们可以知道这行代码主要生成一个zend_op,其主要成员值为: 100 | opcode = 38 (对应#define ZEND_ASSIGN 38) 101 | op1 = $a ($a变量实际上是以cv形式存在,以后介绍) 102 | op2 = 123 (以const常量形式存在) 103 | handler = ZEND_ASSIGN_SPEC_CV_CONST_HANDLER(得到这个handler的名字不是一件容易的事,以后给出方法) 104 | 105 | opcode ZEND_ASSIGN的意思是将一个常量赋值给一个cv(compiled variable),这个cv其实就是$a变量的一种存在形式。在 106 | zend_vm_execute.h中搜索到ZEND_ASSIGN_SPEC_CV_CONST_HANDLER的定义,其主要功能就是取op2的值123,将其赋值给op1的变量,当然这个 107 | 过程比想象中的要复杂一些,会有变量的初始化,变量的写时赋值等过程,以后会介绍每一个过程。这样这条PHP语句的功能就完成了。 108 | 可以看出,op handler只是按照一些固定的方式来对操作数op1 op2(可能还有result)进行操作,handler不理会这些操作数中的具体值, 109 | 这些值是在编译阶段生成op的时候确定的,比如如果$a = 123 改成 $a =456,那么生成的op中op2就是456了,handler始终按照固定的方式 110 | 来处理。 111 | 因此我们能知道,PHP的执行过程是先通过编译器将PHP代码编译成opcode,然后然后zend虚拟机按照一定顺序执行这些opcode,具体是将每个opcode分发给特定的op code handler。 112 | 113 | 参考:http://blog.csdn.net/phpkernel/article/details/5714302 114 | 115 | http://blog.csdn.net/phpkernel/article/details/5721134 116 | -------------------------------------------------------------------------------- /1.6.md: -------------------------------------------------------------------------------- 1 | 1.6 zend_execute: op执行过程详解 2 | ------------- 3 | 解释器引擎最终执行op的函数是zend_execute,实际上zend_execute是一个函数指针,在引擎初始化的时候zend_execute默认指向了 4 | execute,这个execute定义在{PHPSRC}/Zend/zend_vm_execute.h: 5 | ``` 6 | ZEND_API void execute(zend_op_array *op_array TSRMLS_DC) 7 | { 8 | zend_execute_data *execute_data; 9 | zend_bool nested = 0; 10 | zend_bool original_in_execution = EG(in_execution); 11 | 12 | 13 | if (EG(exception)) { 14 | return; 15 | } 16 | 17 | EG(in_execution) = 1; 18 | 19 | zend_vm_enter: 20 | /* Initialize execute_data */ 21 | execute_data = (zend_execute_data *)zend_vm_stack_alloc( 22 | ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)) + 23 | ZEND_MM_ALIGNED_SIZE(sizeof(zval**) * op_array->last_var * (EG(active_symbol_table) ? 1 : 2)) + 24 | ZEND_MM_ALIGNED_SIZE(sizeof(temp_variable)) * op_array->T TSRMLS_CC); 25 | 26 | EX(CVs) = (zval***)((char*)execute_data + ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data))); 27 | memset(EX(CVs), 0, sizeof(zval**) * op_array->last_var); 28 | EX(Ts) = (temp_variable *)(((char*)EX(CVs)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval**) * op_array->last_var * (EG(active_symbol_table) ? 1 : 2))); 29 | EX(fbc) = NULL; 30 | EX(called_scope) = NULL; 31 | EX(object) = NULL; 32 | EX(old_error_reporting) = NULL; 33 | EX(op_array) = op_array; 34 | EX(symbol_table) = EG(active_symbol_table); 35 | EX(prev_execute_data) = EG(current_execute_data); 36 | EG(current_execute_data) = execute_data; 37 | EX(nested) = nested; 38 | nested = 1; 39 | 40 | if (op_array->start_op) { 41 | ZEND_VM_SET_OPCODE(op_array->start_op); 42 | } else { 43 | ZEND_VM_SET_OPCODE(op_array->opcodes); 44 | } 45 | 46 | if (op_array->this_var != -1 && EG(This)) { 47 | Z_ADDREF_P(EG(This)); /* For $this pointer */ 48 | if (!EG(active_symbol_table)) { 49 | EX(CVs)[op_array->this_var] = (zval**)EX(CVs) + (op_array->last_var + op_array->this_var); 50 | *EX(CVs)[op_array->this_var] = EG(This); 51 | } else { 52 | if (zend_hash_add(EG(active_symbol_table), "this", sizeof("this"), &EG(This), sizeof(zval *), (void**)&EX(CVs)[op_array->this_var])==FAILURE) { 53 | Z_DELREF_P(EG(This)); 54 | } 55 | } 56 | } 57 | 58 | EG(opline_ptr) = &EX(opline); 59 | 60 | EX(function_state).function = (zend_function *) op_array; 61 | EX(function_state).arguments = NULL; 62 | 63 | while (1) { 64 | int ret; 65 | #ifdef ZEND_WIN32 66 | if (EG(timed_out)) { 67 | zend_timeout(0); 68 | } 69 | #endif 70 | 71 | if ((ret = EX(opline)->handler(execute_data TSRMLS_CC)) > 0) { 72 | switch (ret) { 73 | case 1: 74 | EG(in_execution) = original_in_execution; 75 | return; 76 | case 2: 77 | op_array = EG(active_op_array); 78 | goto zend_vm_enter; 79 | case 3: 80 | execute_data = EG(current_execute_data); 81 | default: 82 | break; 83 | } 84 | } 85 | 86 | } 87 | zend_error_noreturn(E_ERROR, "Arrived at end of main loop which shouldn't happen"); 88 | } 89 | ``` 90 | 此函数的参数为op_array,这是一个指向zend_op_array的指针,op_array是在编译过程中生成,这里有必要介绍一下zend_op_array这个类 91 | 型. 92 | 93 | 94 | zend_op_array简介 95 | 此类型的定义在{PHPSRC}/Zend/zend_compile.h: 96 | ``` 97 | struct _zend_op_array { 98 | /* Common elements */ 99 | zend_uchar type; 100 | char *function_name; 101 | zend_class_entry *scope; 102 | zend_uint fn_flags; 103 | union _zend_function *prototype; 104 | zend_uint num_args; 105 | zend_uint required_num_args; 106 | zend_arg_info *arg_info; 107 | zend_bool pass_rest_by_reference; 108 | unsigned char return_reference; 109 | /* END of common elements */ 110 | 111 | zend_bool done_pass_two; 112 | 113 | zend_uint *refcount; 114 | 115 | zend_op *opcodes; 116 | zend_uint last, size; 117 | 118 | zend_compiled_variable *vars; 119 | int last_var, size_var; 120 | 121 | zend_uint T; 122 | 123 | zend_brk_cont_element *brk_cont_array; 124 | int last_brk_cont; 125 | int current_brk_cont; 126 | 127 | zend_try_catch_element *try_catch_array; 128 | int last_try_catch; 129 | 130 | /* static variables support */ 131 | HashTable *static_variables; 132 | 133 | zend_op *start_op; 134 | int backpatch_count; 135 | 136 | zend_uint this_var; 137 | 138 | char *filename; 139 | zend_uint line_start; 140 | zend_uint line_end; 141 | char *doc_comment; 142 | zend_uint doc_comment_len; 143 | zend_uint early_binding; /* the linked list of delayed declarations */ 144 | 145 | void *reserved[ZEND_MAX_RESERVED_RESOURCES]; 146 | }; 147 | 148 | typedef struct _zend_op_array zend_op_array; 149 | ``` 150 | 151 | 此结构比较复杂,我们目前只介绍最基本的几个字段。 152 | 1.type: 153 | op_array的类型,首先需要说明的是,一段PHP代码被编译之后,虽然返回的是一个zend_op_array指针,但是实际上生成的 154 | zend_op_array结构可能不止一个,通过这个结构中的一些字段,例如function_name ,num_args等你也许会发现这个zend_op_array结构 155 | 似乎能和函数产生一定的联系,确实如此,用户自定义的函数,以及用户定义的类的方法,都是一个zend_op_array结构,这些 156 | zend_op_array结构在编译过程中被保存在某些地方,例如用户自定义的函数被保存进了GLOBAL_FUNCTION_TABLE,这个是全局函数符号 157 | 表,通过函数名可以在此表中检索到函数体。那么编译后返回的那个zend_op_array指针是什么呢,其实编译后返回的zend_op_array是 158 | 执行的一个入口,也可以认为它是最外层,即不在任何函数体内的全局代码组成的op_array。然而全局代码,用户自定义函数,用户自定 159 | 义的方法都拥有相同的type值:2 ,type可取值的宏定义为: 160 | ``` 161 | #define ZEND_INTERNAL_FUNCTION 1 162 | #define ZEND_USER_FUNCTION 2 163 | #define ZEND_OVERLOADED_FUNCTION 3 164 | #define ZEND_EVAL_CODE 4 165 | #define ZEND_OVERLOADED_FUNCTION_TEMPORARY 5 166 | ``` 167 | 可以看到全局代码,用户函数,用户方法都对应的是ZEND_USER_FUNCTION,这个也是最常见的type了,其中ZEND_EVAL_CODE对应的是eval函数中的PHP代码,所以我们可以想到,eval函数参数中的PHP代码也会被编译成单独的zend_op_array。 168 | 169 | 2.function_name 170 | 如果op_array是由用户定义的函数或则方法编译而生成,那么此字段对应函数的名字,如果是全局代码或则是eval部分的代码,那么此字段为控制 171 | 172 | 3.opcodes 173 | 这个字段类型为zend_op *,因此这是一个zend_op的数组,这个数组保存的就是此编译过程中生成的op,如果不了解zend_op,可以看看之前的文章 OPcode简介 , 这个字段是最重要的部分了,zend_execute最终就是执行这里保存的op。 174 | 175 | 现在基本对参数op_array有了一定的了解,那么我们就开始进入到execute中 176 | 177 | 执行过程详解 178 | execute函数开始的时候是一些基础变量的申明,其中zend_execute_data *execute_data;是执行期的数据结构,此变量在进行一定的初始化之后将会被传递给每个op的handler函数作为参数,op在执行过程中随时有可能改变execute_data中的内容。 179 | 第14行zend_vm_enter 这个跳转标签是作为虚拟机执行的入口,当op中涉及到函数调用的时候,就有可能会跳转到这里来执行函数体。 180 | 第16行到第19行为execute_data分配空间 181 | 第21行到第32行主要是对execute_data进行一些初始化,以及保存现场工作,要保存现场是因为在进入函数调用的时候,需要保存当前一些运行期间的数据,在函数调用结束之后再进行还原,可以想象为操作系统中进程调度,当进程在调出的时候需要保存寄存器等上下文环境,而当进程被调入的时候再取出来继续执行。 182 | 第41行到第51行主要是在当前动态符号表中加入$this变量,这个是在调用对象的方法时才有必要进行。 183 | 第58行开始的while无限循环就是开始执行op_array中的opcodes了,在第66行中调用当前执行的op的handler: 184 | EX(opline)->handler(execute_data TSRMLS_CC)) 185 | 然后如果handler的返回值小于0则循环继续,如果大于0则进入一个switch结构: 186 | 187 | 当返回值为1时: 188 | execute函数将返回,执行也就结束了 189 | 当返回值为2时: 190 | op_array被重新设置,并跳转到zend_vm_enter ,这个一般是函数调用或则执行eval函数中的代码,将在新的上下文执行相关函数的op_array 191 | 当返回值为3时: 192 | 循环体继续继续执行,当然再继续执行之前,EX(opline)已经往后移了一位(可能多位),也就是已经指向了后面一个新的opline,于是继续执行新的opline 193 | 当返回其他值时: 194 | 结束循环,报错,结束应该用return,也就是返回1 195 | 196 | 在op的handler中返回特定的值都被定义成了宏,例如{PHPSRC}/Zend/zend_execute.c中定义的: 197 | ``` 198 | #define ZEND_VM_NEXT_OPCODE() / 199 | CHECK_SYMBOL_TABLES() / 200 | EX(opline)++; / 201 | ZEND_VM_CONTINUE() 202 | 203 | #define ZEND_VM_SET_OPCODE(new_op) / 204 | CHECK_SYMBOL_TABLES() / 205 | EX(opline) = new_op 206 | 207 | #define ZEND_VM_JMP(new_op) / 208 | CHECK_SYMBOL_TABLES() / 209 | if (EXPECTED(!EG(exception))) { / 210 | EX(opline) = new_op; / 211 | } / 212 | ZEND_VM_CONTINUE() 213 | 214 | #define ZEND_VM_INC_OPCODE() / 215 | EX(opline)++ 216 | ``` 217 | 以及在{PHPSRC}/Zend/zend_vm_execute.c中定义的: 218 | ``` 219 | #define ZEND_VM_CONTINUE() return 0 220 | #define ZEND_VM_RETURN() return 1 221 | #define ZEND_VM_ENTER() return 2 222 | #define ZEND_VM_LEAVE() return 3 223 | #define ZEND_VM_DISPATCH(opcode, opline) return zend_vm_get_opcode_handler(opcode, opline)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); 224 | ``` 225 | 简单介绍功能 226 | ZEND_VM_NEXT_OPCODE(): 227 | 移动到下一条op,返回0,不进入switch,循环继续(这个是最常用到的) 228 | ZEND_VM_SET_OPCODE(new_op): 229 | 当前opline设置成new_op 230 | ZEND_VM_JMP(new_op) : 231 | 当前opline设置成new_op,返回0,不进入switch,循环继续 232 | ZEND_VM_INC_OPCODE(): 233 | 仅仅移动到下一条op 234 | 235 | 236 | 执行环境的切换 237 | 在前面的内容已经提到,用户自定义函数,类方法,eval的代码都会编译成单独的op_array,那么当进行函数调用等操作时,必然涉及到调用前的op_array执行环境和新的函数的op_array执行环境的切换,这一段我们将以调用用户自定义函数来介绍整个切换过程如何进行。 238 | 介绍此过程前必须了解执行环境的相关数据结构,涉及到执行环境的数据结构主要有两个: 239 | 1.执行期全局变量结构 240 | 相关的定义在{PHPSRC}/Zend/zend_globals_macros.h: 241 | ``` 242 | /* Executor */ 243 | #ifdef ZTS 244 | # define EG(v) TSRMG(executor_globals_id, zend_executor_globals *, v) 245 | #else 246 | # define EG(v) (executor_globals.v) 247 | extern ZEND_API zend_executor_globals executor_globals; 248 | #endif 249 | ``` 250 | 这里是一个条件编译,ZTS表示线程安全启用,为了简化,我们这里以非线程安全模式的情况下来介绍,那么执行期的全局变量就是executor_globals,其类型为zend_executor_globals, zend_executor_globals的定义在{PHPSRC}/Zend/zend_globals.h,结构比较庞大,这里包含了整个执行期需要用到的各种变量,无论是哪个op_array在执行,都共用这一个全局变量,在执行过程中,此结构中的一些成员可能会改变,比如当前执行的op_array字段active_op_array,动态符号表字段active_symbol_table可能会根据不同的op_array而改变,This指针会根据在不同的对象环境而改变。 251 | 另外还定义了一个EG宏来取此变量中的字段值,此宏是针对线程安全和非线程安全模式的一个封装。 252 | 253 | 2.每个op_array自身的执行数据 254 | 针对每一个op_array,都会有自己执行期的一些数据,在函数execute开始的时候我们能看到zend_vm_enter跳转标签下面就会初始一个局部变量execute_data,所以我们每次切换到新的op_array的时候,都会为新的op_array建立一个execute_data变量,此变量的类型为zend_execute_data的指针,相关定义在{PHPSRC}/Zend/zend_compile.h: 255 | ``` 256 | struct _zend_execute_data { 257 | struct _zend_op *opline; 258 | zend_function_state function_state; 259 | zend_function *fbc; /* Function Being Called */ 260 | zend_class_entry *called_scope; 261 | zend_op_array *op_array; 262 | zval *object; 263 | union _temp_variable *Ts; 264 | zval ***CVs; 265 | HashTable *symbol_table; 266 | struct _zend_execute_data *prev_execute_data; 267 | zval *old_error_reporting; 268 | zend_bool nested; 269 | zval **original_return_value; 270 | zend_class_entry *current_scope; 271 | zend_class_entry *current_called_scope; 272 | zval *current_this; 273 | zval *current_object; 274 | struct _zend_op *call_opline; 275 | }; 276 | ``` 277 | 可以用EX宏来取其中的值:#define EX(element) execute_data->element 278 | 279 | 这里只简单介绍其中两个字段: 280 | opline: 当前正在执行的op 281 | prev_execute_data: op_array环境切换的时候,这个字段用来保存切换前的op_array,此字段非常重要,他能将每个op_array的execute_data按照调用的先后顺序连接成一个单链表,每当一个op_array执行结束要还原到调用前op_array的时候,就通过当前的execute_data中的prev_execute_data字段来得到调用前的执行器数据 282 | 283 | 在executor_globals中的字段current_execute_data就是指向当前正在执行的op_array的execute_data。 284 | 285 | 再正式介绍之前还需要简单的介绍一下用户自定义函数的调用过程,详细的过程以后再函数章节中专门介绍,这里简单的说明一下: 286 | 在调用函数的时候,比如test()函数,会先在全局函数符号表中根据test来搜索相关的函数体,如果搜索不到则会报错函数没有定义,找到test的函数体之后,取得test函数的op_array,然后跳转到execute中的goto标签:zend_vm_enter,于是就进入到了test函数的执行环境。 287 | 288 | 下面我们将以一段简单的代码来介绍执行环境切换过程,例子代码: 289 | ``` 290 | 300 | ``` 301 | 这段代码非常简单,这样方便我们介绍原理,复杂的代码读者可以举一反三。 302 | 此代码编译之后会生成两个op_array,一个是全局代码的op_array,另外一个是test函数的op_array,其中全局代码中会通过函数调用进入到test函数的执行环境,执行结束之后,会返回到全局代码,然后代码结束。 303 | 304 | 下面我们分几个阶段来介绍这段代码的过程,然后从中可以知道执行环境切换的方法。 305 | 1:进入execute函数,开始执行op_array ,这个op_array就是全局代码的op_array,我们暂时称其为op_array1 306 | 首先在execute中为op_array1建立了一个execute_data数据,我们暂时命名为execute_data1,然后进行相关的初始化操作,其中比较重要的是: 307 | EX(op_array) = op_array; // 设置op_array字段为当前执行的op_array,也就是全局代码的op_array1 308 | EX(prev_execute_data) = EG(current_execute_data);//将全局执行数据中保存的当前op_array执行数据保存到op_array1的execute_data1的prev_execute_data字段,由于这是执行的第一个op_array,所以prev_execute_data实际上是空值,然后将执行期全局变量的current_execute_data设置成execute_data1,然后设置execute_data1的当前执行op,这样就可以开始执行当前的op了 309 | 310 | 2:在op_array1执行到test函数调用的的时候,首先从全局函数符号表中找到test的函数体,将函数体保存在execute_data1的function_state字段,然后从函数体中取到test的op_array,我们这里用op_array2来表示,并将op_array2赋值给EG(active_op_array): 311 | EG(active_op_array) = &EX(function_state).function->op_array; 312 | 313 | 于是执行期全局变量的动态op_array字段指向了函数test的op_array,然后用调用ZEND_VM_ENTER();这个时候会先回到execute函数中的switch结构,并且满足以下case 314 | case 2: 315 | op_array = EG(active_op_array); 316 | goto zend_vm_enter; 317 | 318 | EG(active_op_array)之前已经被我们设置为test函数的op_array2,于是在函数execute中,op_array变量就指向了test的op_array2,然后跳转到zend_vm_enter。 319 | 320 | 3:跳转到zend_vm_enter之后其实又回到了类似1中的步骤,此时为test的op_array2建立了它的执行数据execute_data,我们这里用execute_data2来表示。跟1中有些不同的是EX(prev_execute_data) = EG(current_execute_data);这个时候current_execute_data = execute_data1,也就是全局代码的执行执行期数据,然后EG(current_execute_data) = execute_data;这样current_execute_data就等于test的执行期数据execute_data2了,同时全局代码的execute_data1被保存在execute_data2的prev_execute_data字段。这个时候进行环境的切换已经完成,于是开始执行test函数。 321 | 322 | 4:test函数执行完之后就要返回到调用前的执行环境了,也就是全局代码执行环境,此阶段最重要的一个操作就是EG(current_execute_data) = EX(prev_execute_data); 在3中EX(prev_execute_data)已经设置成了全局代码的execute_data1,所以这样当前执行数据就变成了全局代码的执行数据,这样就成功的从函数test执行环境返回到了全局代码执行环境 323 | 324 | 这样,执行环境的切换过程就完成了,对于深层次的函数调用,原理一样,执行数据execute_data组成的单链表会更长 325 | 326 | 参考:http://blog.csdn.net/phpkernel/article/details/5721384 327 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | learn-php-core 2 | ============== 3 | 4 | 傻瓜式学习php内核 5 | -------------------------------------------------------------------------------- /learn-material.md: -------------------------------------------------------------------------------- 1 | 1. 符号表 2 | ``` 3 | EG() 这个宏可以用来访问符号表,函数,资源信息和常量。 4 | CG() 用来访问核心全局变量。 5 | PG() PHP全局变量。我们知道php.ini会映射一个或者多个PHP全局结构。举几个使用这个宏的例子:PG(register_globals), 6 | PG(safe_mode), PG(memory_limit) 7 | FG() 文件全局变量。大多数文件I/O或相关的全局变量的数据流都塞进标准扩展出口结构。 8 | ``` 9 | --------------------------------------------------------------------------------