PHP内核 - 编译与执行 - ZendVM虚拟机

PHP   PHP内核  

一、前言

  Zend虚拟机 ( Zendvm) 是 PHP 语言的解释器,它是 PHP 语言实现的核心,负责PHP代码的解析、执行。 Zendvm 预先定义好了大量的指令供用户在PHP代码中使用,这些指令对应的处理过程被编译为机器指令,执行PHP代码时,首先根据定义好的规则确定要执行的指令然后调用对应的机器指令完成执行。

  ZendVM 对于实际计算机而言就是普通的二进制可执行程序,它是编译好的机器指令。实际上,PHP 代码的编译、执行过程与编译型语言的处理过程非常相似,只不过PHP代码被编译为了 ZendVM 可识别的指令,而不是机器指令,对于 PHP 而言,实际计算机是透明的,因此我们可以把 ZendVM 当作真正的计算机来理解。

  ZendVM 由两部分组成:编译器、执行器,其中编译器负责将PHP代码解释为 ZendVM 可识别的指令 (即 opline) ,同时生成对应的符号表(函数、类等),执行器负责执行opcode对应的机器指令,如图:

  

二、opline 指令

1、opline

  online是 Zendvm 定义的执行指令,每条指令的编码为 opcode,等价于机器指令。PHP 代码在编译阶段被转化为 ZendVM 可识别的指令, ZendVM 根据不同的指令完成 PHP 代码的运行。

  opline 的编译是 PHP 编译器最核心的操作,也是编译阶段输出的产物。尽管 opline指令与机器指令并不一样,但是对于执行它们的机器而言,其含义是相同的,都是控制执行机器工作的命令。

opline 指令的结构为 zend_op:

  1. struct_zend_op {
  2. const void *handler;// 指令执行handle
  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. // 文件位于 /Zend/zend_compile.h

opline 指令的组成概括一下就是:对何数据进行何处理。前者称为操作数,也就是指令的操作对象,后者称之为opcode,即指令的处理动作。

  
2、opcode

opcode 为指令编码,唯一标识一个指令动作。目前 PHP 总共定义了208条 opcode ,所有 PHP 的语法都是基于 opcode 实现的,比如赋值,四则运算、循环、条件判断等。

ZendVM 的指令集定义在头文件 /Zend/zend_vm_opcodes.h 中。

  1. #define ZEND_NOP 0
  2. #define ZEND_ADD 1
  3. #define ZEND_SUB 2
  4. #define ZEND_MUL 3
  5. #define ZEND_DIV 4
  6. #define ZEND_MOD 5
  7. #define ZEND_SL 6
  8. #define ZEND_SR 7
  9. #define ZEND_CONCAT 8
  10. #define ZEND_BW_OR 9
  11. #define ZEND_BW_AND 10
  12. ...

  
3、操作数

zend op 结构中有三个 znode_op 类型的成员:opl、op2、result,它们称之为操作数。

操作数是运算符作用于的实体,是表达式中的一个组成部分,它规定了指令中进行数字运算的量,也就是说, opcode 指定机器的运算动作,而操作数则是该动作操纵的具体对象。

比如汇编指令 add eax, 100,意思是将eax寄存器中指向的值加上100,这里 add 就是运算符,对应 ZendVM 的 opcode 1,而eax、100就是操作数,用来告诉计算机运算操作的对象。

Zendvm 为每条指令定义了三个操作数,当然,并不是所有的指令都会用到三个,有的指
令不需要操作数,有的指令只需要一个,有的需要两个,有的则都会用到,这个由各条指令自行决定具体用途,其中 result这个操作数用于返回值,告诉 Zendvm 运算结果的存储位置。

操作数的结构为 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. } znode_op;
  7. // 文件位于 /Zend/zend_vm_opcodes.h 中。

比如赋值操作 $a = 123,操作数1用来告诉 ZendMM 变量 $a 的位置,操作数2用来保存变量值 123 的位置,执行的时候,ZendVM 从操作数获取到变量 $a 与变量值123的存储位置,从而执行对应的动作。

操作数还有类型之分,因为运算操作的对象会有不同类型,比如同样是赋值操作:$b=$a 与 $b = 123,赋的变量值是不同的,一类是变量,一类是常量。因此, ZendVM 会根据操作数的不同类型,获取操作数指定的对象。这个类型就好比 ELF 可执行程序中,有的数据从栈上读取,有的从 .data段读取。

操作数类型在 zend_op中定义,同样有三个:op1_type、op2_type、result_type,分别对应三个操作数。操作数的具体类型有以下几个:

  1. #define IS_UNUSED 0 // 表示操作数没有使用
  2. #define IS_CONST (1<<0) // 常量
  3. #define IS_TMP_VAR (1<<1) // 临时变量
  4. #define IS_VAR (1<<2) // PHP变量,比如 time()
  5. #define IS_CV (1<<3) // 变量
  6. // 文件位于 /Zend/zend_compile.h 中

  
4、handle

  handler 为每条 opcode 对应的实际处理函数, opcode只是指令的编码,编译时会根据 opcode 为每条 opine 指令设置具体的 handler.,执行时调用 handler进行处理。

  handler 为 C 语言编写的编译为机器指令的处理逻辑,默认情况下,handler就是普通的 C 语言函数。

  此外,由于操作数有多种类型,同一 opcode不同类型操作数的处理方式可能会有一些差异,因此每条 opcode 会根据操作数类型定义多个 handler。比如$a=123 与 $a=$b,虽然都是赋值操作,但是一个是 CONST 类型,一个是 CV 类型。每条指令有2个操作数,操作数有5种类型,因此,每条 opcode 最多可有5X5=25个 handler。

  

三、zend_op_array

  

  
opline 是编译生成的单条指令,所有的指令集合组成了 zend_op_array 。除了指令集合,zend_op_array 还保存着很多编译生成的关键数据,比如常量存储区就在zend_op_array 中。

zend_op_array 是编译器的输出,也是执行器的输入,它的角色相当于编译型语言最终编译出的可执行文件。

对于 ZendVM 而言, zend_op_array就是可执行数据,每个 PHP 脚本都会被编译为独立的 zend_op_array结构。

  1. struct _zend_op_array {
  2. zend_uchar type;
  3. ...
  4. // number of run_time_cache_slots * sizeof(void*)
  5. int cache_size;
  6. int last_var;// 变量数
  7. uint32_t T;// 临时变量数
  8. uint32_t last;// opcode 指令数
  9. zend_op *opcodes;// 指令集合(数组)
  10. ZEND_MAP_PTR_DEF(void **, run_time_cache);
  11. ZEND_MAP_PTR_DEF(HashTable *, static_variables_ptr);
  12. HashTable *static_variables;
  13. zend_string **vars;// PHP 变量名数组
  14. ...
  15. int last_literal;
  16. zval *literals;// 常量数组
  17. void *reserved[ZEND_MAX_RESERVED_RESOURCES];
  18. };

从zend_op_array 结构可以看到,它包含了 PHP 编译过程的所有产物。

  

四、zend_execute_data

  C 程序在执行时首先会分配运行栈,局部变量、上下文调用信息都通过栈来保存,eip寄存器指向指令区,函数调用时首先将eip入栈保存,然后移动ebp、esp分配新的枝,执行完以后再将保存的eip出栈还原,继续执行。

  从 C 程序执行的过程来看,最重要的两部分为执行栈、eip。同样地,ZendVM 的执行器在执行流程中,通过 zend execute_data 结构实现了类似 C 程序中执行栈、eip的功能。

  1. struct _zend_execute_data {
  2. const zend_op *opline; // executed opline
  3. zend_execute_data *call; // current call
  4. zval *return_value;
  5. zend_function *func; // executed function
  6. zval This; // this + call_info + num_args
  7. zend_execute_data *prev_execute_data;
  8. zend_array *symbol_table;
  9. // cache op_array->run_time_cache
  10. void **run_time_cache;
  11. };

  ZendVM 执行 opcode 指令前,首先会根据 zend_op_array 信息分配一个zend_execute_data 结构,这个结构用来保存运行时的信息,包含当前执行的指令、局部变量、上下文调用信息等。

说明:

opline:当前执行中的指令,等价于 eip 的作用,执行之初 opline 指向 zend_op_aray->opcodes指令集的第一条指令,当执行完一条指令后,该值会更新为下一条指令。

return_value:返回值,执行完之后,会把返回值设置到这个地址。

symbol table:全局变量符号表

prev_execute_data:调用上下文,当函数调用或者 include 时,会重新分配一个zend_execute_data,并把当前执行的 zend_execute_data 保存到被调函数的 zend_execute_data->prev_execute_data,被调函数执行完成后,再根据 prev_execute_data 还原到原来的执行位置, prev_execute_data 等价于 C 程序调用过程中 call、ret 指令的作用。

literals:就是 zend_op_array->literals。

除了上面介绍的这些成员, zend_execute_data 还有一个重要组成部分:动态变量区。

  

五、zend_executor_globals

zend_executor_globals 是 PHP 整个生命周期中非常重要的一个数据结构,它是全局符号表,在 main 执行前分配 (非ZTS下),直到 PHP 退出, PHP 中经常见到的 EG 宏操作的就是这个结构。

zend_executor_globals 保存着类、函数符号表,类、函数编译过程中会注册到相应的符号表中,执行时如果发生函数调用、实例化类就会去各自的符号表中查找。另外, zend_executor_globals 还有一个指针指向当前执行的 zend_execute_data。

  

 

 



Top