編譯與執行

 

PHP7與PHP5在編譯執行上的區別

 

在PHP7之前的版本,PHP代碼在語法解析階段直接生成了ZendVM指令(也就是opline指令,後面會聊一下opline指令的~),這使得編譯器與執行器耦合在一起。這個模式的壞處就是,當我們如果要換一個VM時候,就需要修改語法解析規則,或者如果PHP的語法規則變了,(例如php中訪問對象使用->來訪問,想換成成java中 .  來訪問),我們也需要去修改語法解析規則。

PHP7加入了抽象語法樹,首先將PHP代碼解析成抽象語法樹,然後將抽象語法樹編譯成爲ZendVM指令。抽象語法樹的加入,使得PHP的編譯器與執行器很好的隔離開來了,編譯器不需要關心指令的生成規則,而執行器同樣不關心指令的語法規則是什麼樣子的,執行器根據自己的規則將抽象語法樹編譯成對應的指令。

編譯型語言和解析型語言的區別

編譯型語言 C語言

解析性語言 php

解析型語言與實際計算機之間多了一層解析器,屏蔽了不同平臺之間機器語言的差異,由解析器去處理不同平臺之間的差異,實現了跨平臺運行。但是帶來的代價是運行效率低,與編譯性語言直接執行機器指令想比,多了一層解析器的工作。

zend_compile_process.png

抽象語法樹(AST)

詞法解析,語法解析

PHP使用re2c、bison完成這個階段的工作:

  • re2c: 詞法分析器,將輸入分割爲一個個有意義的詞塊,稱爲token
  • yacc: 語法分析器,確定詞法分析器分割出的token是如何彼此關聯的
<?php
$a = 3 + 4 -6;

詞法分析器將上面的語句分解爲這些token:$a、=、2、+、3、- 、6,接着語法分析器確定了2+3-6是一個表達式,而這個表達式被賦值給了a

語法分析樹

PHP的抽象語法樹定義

typedef struct _zend_ast_list {
    zend_ast_kind kind; //語句類型
    zend_ast_attr attr;
    uint32_t lineno;
    uint32_t children;
    zend_ast *child[1];
} zend_ast_list;

PHP抽象語法樹拆分邏輯比較難理解,我給出一個例子,我們看下最終生成的語法樹。

<?php
$a = 123;
$b = "hi~";
echo $a,$b;

zend_ast

Zend虛擬機

Zend虛擬機就是PHP語言的解析器,負責PHP代碼的解析,執行。

ZendVM對於計算機而言就是普通二進制可執行程序,是編譯好的機器指令,而PHP代碼被編譯成ZendVM可識別的指令,而不是機器指令,然後ZendVM來執行,最終變爲機器指令。

ZendVM由以下部分組成:

Opline指令

opline是ZendVM定義的執行指令,每條指令的編碼都是opcode。

struct _zend_op {
   const void *handler; //指令執行handler
   znode_op op1;  //操作數1
   znode_op op2;  //操作數2
   znode_op result; //返回值
   uint32_t extended_value;
   uint32_t lineno;
   zend_uchar opcode; //opcode指令
   zend_uchar op1_type;  //操作數1類型
   zend_uchar op2_type;  //操作數2類型
   zend_uchar result_type; //返回值類型
};
 
 
//opcode 定義
#define ZEND_NOP                               0
#define ZEND_ADD                               1
#define ZEND_SUB                               2
#define ZEND_MUL                               3
#define ZEND_DIV                               4
#define ZEND_MOD                               5
#define ZEND_SL                                6
#define ZEND_SR                                7
#define ZEND_CONCAT                            8
#define ZEND_BW_OR                             9
#define ZEND_BW_AND                           10
#define ZEND_BW_XOR                           11


舉個例子:例如賦值操作 $a = 123,操作數1用來告訴VM 變量$a的位置,操作數2用來保存變量123的位置,執行的時候,ZendVM從操作數1與2獲取信息,然後進行對應動作(此時opcode指令爲 ZEND_ADD)opline指令描述其實就是:對什麼數據,做什麼處理!

Zend_op_array

opline 是編譯生成的單條指令,所有的指令組合生成了zend_op_array

struct _zend_op_array {
   /* Common elements */
   .... 以上省略
   uint32_t *refcount;
 
   uint32_t this_var;
 
   uint32_t last;
   zend_op *opcodes; //這裏就是指令集合,是一個數組,執行器執行時從該數組的第一條指令開始,直到最後
 
   int last_var;
   uint32_t T;
   zend_string **vars;
   ....以上省略
};


zend_execute_data是執行過程中最核心的一個結構,每次函數的調用、include/require、eval等都會生成一個新的結構,它表示當前的作用域、代碼的執行位置以及局部變量的分配等等。Zend_execute_data

#define EX(element)             ((execute_data)->element)
 
//zend_compile.h
struct _zend_execute_data {
    const zend_op       *opline;  //指向當前執行的opcode,初始時指向zend_op_array起始位置
    zend_execute_data   *call;             /* current call                   */
    zval                *return_value;  //返回值指針
    zend_function       *func;          //當前執行的函數(非函數調用時爲空)
    zval                 This;          //這個值並不僅僅是面向對象的this,還有另外兩個值也通過這個記錄:call_info + num_args,分別存在zval.u1.reserved、zval.u2.num_args
    zend_class_entry    *called_scope;  //當前call的類
    zend_execute_data   *prev_execute_data; //函數調用時指向調用位置作用空間
    zend_array          *symbol_table; //全局變量符號表
#if ZEND_EX_USE_RUN_TIME_CACHE
    void               **run_time_cache;   /* cache op_array->run_time_cache */
#endif
#if ZEND_EX_USE_LITERALS
    zval                *literals;  //字面量數組,與func.op_array->literals相同
#endif
};


Zend_execute_data 與Zend_op_array的關係

執行器

zend執行opcode的簡略過程

  • step1: 爲當前作用域分配一塊內存,充當運行棧,zend_execute_data結構、所有局部變量、中間變量等等都在此內存上分配
  • step2: 初始化全局變量符號表,然後將全局執行位置指針EG(current_execute_data)指向step1新分配的zend_execute_data,然後將zend_execute_data.opline指向op_array的起始位置
  • step3: 從EX(opline)開始調用各opcode的C處理handler(即_zend_op.handler),每執行完一條opcode將EX(opline)++繼續執行下一條,直到執行完全部opcode,函數/類成員方法調用、if的執行過程:
  •            step3.1: 如果是函數調用,則首先從EG(function_table)中根據function_name取出此function對應的編譯完成的zend_op_array,然後像step1一樣新分配一個zend_execute_data結構,將EG(current_execute_data)賦值給新結構的prev_execute_data,再將EG(current_execute_data)指向新的zend_execute_data,最後從新的zend_execute_data.opline開始執行,切換到函數內部,函數執行完以後將EG(current_execute_data)重新指向EX(prev_execute_data),釋放分配的運行棧,銷燬局部變量,繼續從原來函數調用的位置執行
  • 全部opcode執行完成後將step1分配的內存釋放,這個過程會將所有的局部變量"銷燬",執行階段結束

zend_execute_data.png
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章