【PHP7原始碼分析】PHP7原始碼研究之淺談Zend虛擬機器

LNMPR原始碼研究發表於2018-07-24

順風車運營研發團隊 陳雷
程式語言的虛擬機器是一種可以執行中間語言的程式。中間語言是抽象出的指令集,由原生語言編譯而成,作為虛擬機器執行階段的輸入。很多語言都實現了自己的虛擬機器,比如Java、C#和Lua。PHP語言也有自己的虛擬機器,稱為Zend虛擬機器

PHP7完成基本的準備工作後,會啟動Zend引擎,載入註冊的擴充套件模組,然後讀取對應的指令碼檔案,Zend引擎會對檔案進行詞法和語法分析,生成抽象語法樹,接著抽象語法樹被編譯成Opcodes,如果開啟了Opcache,編譯的環節會被跳過從Opcache中直接讀取Opcodes進行執行。

PHP7中詞法語法分析,生成抽象語法樹,然後編譯成Opcodes及被執行均由Zend虛擬機器完成。這裡將詳細闡述抽象語法樹編譯成Opcodes的過程,以及Opcodes被執行的過程,來闡述Zend虛擬機器的實現原理及關鍵的資料結構。

1 基礎知識

Zend虛擬機器(稱為Zend VM)是PHP語言的核心,承擔了語法詞法解析、抽象語法樹編譯以及指令的執行工作,下面我們討論一下Zend虛擬機器的基礎架構以及相關的基礎知識。

1.1 Zend虛擬機器架構

Zend虛擬機器主要分為解釋層、中間資料層和執行層,下面給出各層包含的內容,如圖1所示。

clipboard.png
圖1 Zend虛擬機器架構圖

下面解釋下各層的作用。

(1)解釋層

這一層主要負責把PHP程式碼進行詞法和語法分析,生成對應的抽象語法樹;另一個工作就是把抽象語法樹進行編譯,生成符號表和指令集;

(2)中間資料層

這一層主要包含了虛擬機器的核心部分,執行棧的維護,指令集和符號表的儲存,而這三個是執行引擎排程執行的基礎;

(3)執行層

這一層是執行指令集的引擎,這一層是最終的執行並生成結果,這一層裡面實現了大量的底層函式。

為了更好地理解Zend虛擬機器各層的工作,我們先了解一下物理機的一些基礎知識,讀者可以對照理解虛擬機器的原理。

1.2 符號表

符號表是在編譯過程中,編譯程式用來記錄源程式中各種名字的特性資訊,所以也稱為名字特性表。名字一般包含程式名、過程名、函式名、使用者定義型別名、變數名、常量名、列舉值名、標號名等。特性資訊指的是名字的種類、型別、維數、引數個數、數值及目標地址(儲存單元地址)等。

符號表有什麼作用呢?一是協助進行語義檢查,比如檢查一個名字的引用和之前的宣告是否相符,二是協助中間程式碼生成,最重要的是在目的碼生成階段,當需要為名字分配地址時,符號表中的資訊將是地址分配的主要依據。

clipboard.png
圖2 符號表建立示例

符號表一般有三種構造和處理方法,分別是線性查詢,二叉樹和Hash技術,其中線性查詢法是最簡單的,按照符號出現的順序填表,每次查詢從第一個開始順序查詢,效率比較低;二叉樹實現了對摺查詢,在一定程度上提高了效率;效率最高的是通過Hash技術實現符號表,相信大家對Hash技術有一定的瞭解,而PHP7中符號表就是使用的HashTable實現的。

1.3 函式呼叫棧

為了更清晰地瞭解虛擬機器中函式呼叫的過程,我們先了解一下物理機的簡單原理,主要涉及函式呼叫棧的概念,而Zend虛擬機器參照了物理機的基本原理,做了類似的設計。

下面以一段C程式碼描述一下系統棧和函式過程呼叫,程式碼如下:

  int funcB(int argB1, int argB2)

  {

         int varB1, varB2;

         return argB1+argB2;

  }

  int funcA(int argA1, int argA2)

  {

       int varA1, varA2;

        return argA1+argA2+funcB( 3, 4);

  }    

  int main()

  {

      int varMain;

      return funcA(1, 2);

     }

這段程式碼執行時,首先main函式會壓棧, 首先將區域性變數varMain入棧,main函式呼叫了funcA函式,C語言會從後往前,將函式引數壓棧,先壓第二個引數argA2=2,再壓第一個引數argA1=1,同時對於funcA的返回會產生一個臨時變數等待賦值,也會被壓棧,這些稱為main函式的棧幀;接著將funcA壓棧,同樣的先將區域性變數varA1和varA2壓入棧中,因為呼叫了函式funcB,會將引數argB2=4和argB1=3壓入棧中,同時把funcB的返回產生的臨時變數壓入棧中,這部分稱為funcA的棧幀;同樣,funcB被壓入棧中,如圖3所示。

clipboard.png
圖3 函式呼叫壓棧過程示意圖

funcB函式執行,對argB1和argB2進行相加操作,執行後得到返回值為7,然後funcB的棧幀出棧,funcA中臨時變數TempB被賦值為7,繼而進行相加操作,得到結果為10,然後funcA出棧,main函式中臨時變數TempA被賦值為10,最終main函式返回並出棧,整個函式呼叫結束。如圖4所示。

clipboard.png
圖4 函式呼叫出棧過程示意圖

1.4 指令

彙編語句中的指令語句一般格式為:

     [標號:]     [字首]  指令助記符    [運算元]     [;註釋]

其中:

  • 1)識別符號欄位由各種有效字元組成,一般表示符號地址,具有段基址、偏移量、型別三種屬性。通常情況下這部分是可選部分,主要為便於程式的讀寫方便而使用。
  • 2)助記符,規定指令或偽指令的操作功能,是語句中唯一不可缺少的部分。對於指令,彙編程式會將其翻譯成機器語言指令:
MOV   AX, 100  →   B8 00 01
  • 3)運算元,指令語句中提供給指令的操作物件、存放位置。運算元可以是1個、2個或0個,2個時用逗號‘,’分開。比如“RET;”對應的運算元個數是0個,“INC BX;”對應的運算元個數是1,“MOV AX,DATA;”對應的運算元個數是2個。
  • 4)註釋,以“ ;”開始,給以程式設計說明。

    符號表、函式呼叫棧以及指令基本構成了物理機執行的基本元素,Zend虛擬機器也同樣實現了符號表,函式呼叫棧及指令,來執行PHP程式碼,下面我先討論一下Zend虛擬機器相關的資料結構。

2相關資料結構

Zend虛擬機器包含了詞法語法分析,抽象語法樹的編譯,以及Opcodes的執行,本文主要詳細介紹抽象語法樹和Opcodes的執行過程,在展開介紹之前,先闡述一下用到的基本的資料結構,為後面原理性的介紹奠定基礎。

2.1 EG(v)

首先介紹的是全域性變數executor_globals,EG(v)是對應的取值巨集,executor_globals對應的是結構體_zend_executor_globals,是PHP生命週期中非常核心的資料結構。這個結構體中維護了符號表(symbol_table, function_table,class_table等),執行棧(zend_vm_stack)以及包含執行指令的zend_execute_data,另外還包含了include的檔案列表,autoload函式,異常處理handler等重要資訊,下面給出_zend_executor_globals的結構圖,然後分別闡述其含義,如圖5所示。

clipboard.png
圖5 EG(v)結構圖

這個結構體比較複雜,下面我們介紹幾個核心的成員。

  • 1)symbol_table:符號表,這裡面主要是存的全域性變數,以及一些魔術變數,比如$_GET、$_POST等;
  • 2)function_table:函式表,主要存放函式,包括大量的內部函式,以及使用者自定義的函式,比如zend_version,func_num_args,str系列函式,等等;
  • 3)class_table:類表,主要存放內建的類以及使用者自定義的類,比如stdclass、throwable、exception等類;
  • 4)zend_constants:常量表,存放PHP中的常量,比如E_ERROR、E_WARNING等;
  • 5)vm_stack:虛擬機器的棧,執行時壓棧出棧都在這上面操作;
  • 6)current_execute_data:對應_zend_execute_data結構體,存放執行時的資料。

下面針對於符號表、指令集、執行資料和執行棧進行詳細介紹。

2.2 符號表

PHP7中符號表分為了symbol_table、function_table和class_table等。

  • (1)symbol_table

symbol_table裡面存放了變數資訊,其型別是HashTable,下面我們看一下具體的定義:

       //符號表快取

       zend_array *symtable_cache[SYMTABLE_CACHE_SIZE];

      zend_array **symtable_cache_limit;

      zend_array **symtable_cache_ptr;

       //符號表

       zend_array symbol_table;

symbol_table裡面有什麼呢,程式碼”$a=1;”對應的symnol_table,如圖6所示。

clipboard.png
圖6 symbol_table示意圖

從圖6中可以看出,符號表中有我們常見的超全域性變數$_GET、$_POST等,還有全域性變數$a。在編譯過程中會呼叫zend_attach_symbol_table函式將變數加入symbol_table中。

  • (2)function_table

function_table對應的是函式表,其型別也是HashTable,見程式碼:

       HashTable *function_table;  /* function symbol table */

函式表中儲存哪些函式呢?同樣以上述程式碼為例,我們利用GDB印一下function_table的內容:

(gdb) p *executor_globals.function_table

$1 = {gc = {refcount = 1, u = {v = {type = 7 '\a', flags = 0 '\000', gc_info = 0}, type_info = 7}}, u = {v = {

      flags = 25 '\031', nApplyCount = 0 '\000', nIteratorsCount = 0 '\000', consistency = 0 '\000'},

    flags = 25}, nTableMask = 4294966272, arData = 0x12102b0, nNumUsed = 848, nNumOfElements = 848,

  nTableSize = 1024, nInternalPointer = 0, nNextFreeElement = 0, pDestructor = 0x8d0dc3 <zend_function_dtor>}

可以看出,函式表中有大量的函式,上面列印顯示有848個之多,這裡面主要是內部函式,比如zend_version、func_num_args、cli_get_process_title,等等。

  • (3)class_table

class_table對應的是類表,其也是HashTable:

 HashTable *class_table; /* class table */

類表裡面也有大量的內建類,比如stdclass、traversable、xmlreader等。

符號表裡面存放了執行時需要的資料,比如在symbol_table中,key為_GET的Bucket對應的又是個HashTable,裡面存放的是$_GET[xxx],執行時會從中取對應的值。

2.3 指令

Zend虛擬機器的指令稱為opline,每條指令對應一個opcode。PHP程式碼在編譯後生成opline,Zend虛擬機器根據不同的opline完成PHP程式碼的執行,opline由操作指令、運算元和返回值組成,與機器指令非常類似,opline對應的結構體為zend_op,見程式碼:

  struct _zend_op {

      const void *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; //返回值的型別

};

對應的記憶體佔用圖如圖7所示。

clipboard.png
圖7 zend_op結構圖

PHP程式碼會被編譯成一條一條的opline,分解為最基本的操作,舉個例子,如果把opcode當成一個計算器,只接受兩個運算元op1和 op2,執行一個操作handler,比如加減乘除,然後它返回一個結果result,再稍加處理算術溢位的情況存在extended_value中。下面詳細介紹下各個欄位。

  • (1) Opcode

Opcode有時候被稱為所謂的位元組碼(Bytecode),是被軟體直譯器解釋執行的指令集。這些軟體指令集通常會提供一些比對應的硬體指令集更高階的資料型別和操作。

注意:Opcode和Bytecode其實是兩個含義不同的詞,但經常會把它們當作同一個意思來互動使用。

Zend虛擬機器有非常多Opcode,對應可以做非常多事情,並且隨著PHP的發展, Opcode也越來越多,意味著PHP可以做越來越多的事情。所有的Opcode都在PHP的原始碼檔案Zend/zend_vm_opcodes.h中定義。Opcode的名稱是自描述的,比如:

  • ZEND_ASSGIN:賦值操作;
  • ZEND_ADD:兩個數相加操作;
  • ZEND_JMP:跳轉操作。

PHP 7.1.0中有186個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_FETCH_THIS                      184

#define ZEND_ISSET_ISEMPTY_THIS              186

#define ZEND_VM_LAST_OPCODE                  186
  • (2)運算元

op1和op2都是運算元,但不一定全部使用,也就是說每個Opcode對應的hanlder最多可以使用兩個運算元(也可以只使用其中一個,或者都不使用)。每個運算元都可以理解為函式的引數,返回值result是hanlder函式對運算元op1和op2計算後的結果。op1、op2和result對應的型別都是znode_op,其定義為一個聯合體:

typedef union _znode_op {

      uint32_t      constant;

      uint32_t      var;

      uint32_t      num;

      uint32_t      opline_num; /*  Needs to be signed */

#if ZEND_USE_ABS_JMP_ADDR

      zend_op       *jmp_addr;

#else

      uint32_t      jmp_offset;

#endif

#if ZEND_USE_ABS_CONST_ADDR

      zval          *zv;

#endif

} znode_op;

這樣其實每個運算元都是uint32型別的數字,一般表示的是變數的位置。運算元有5種不同的型別,具體在Zend/zend_compile.h中定義:

#define IS_CONST        (1<<0)

#define IS_TMP_VAR      (1<<1)

#define IS_VAR          (1<<2)

#define IS_UNUSED       (1<<3)   /* Unused variable */

#define IS_CV           (1<<4)   /* Compiled variable */

這些型別是按位表示的,具體含義如下。

  • 1)IS_CONST:值為1,表示一個常量,都是隻讀的,值不可改變,比如$a=”hello world” 中的hello world。
  • 2)IS_VAR:值為4,是PHP變數,這個變數並不是PHP程式碼中宣告的變數,常見的是返回的臨時變數,比如$a=time(), 函式time返回值的型別就是IS_VAR,這種型別的變數是可以被其他Opcode對應的handler重複使用的。
  • 3)IS_TMP_VAR :值為2,也是PHP變數,跟IS_VAR不同之處是,不能與其他Opcode重複使用,舉個例子,$a=”123”.time(); 這裡拼接的臨時變數”123”.time()的型別就是IS_TMP_VAR,一般用於操作的中間結果。
  • 4)IS_UNUSED :值為8,表示這個運算元沒有包含任何有意義的東西。
  • 5)IS_CV :值為16,編譯變數(Compiled Variable):這個運算元型別表示一個PHP變數:以$something形式在PHP指令碼中出現的變數。
  • (3)handler

handler為Opcode對應的是實際的處理函式,Zend虛擬機器對每個Opcode的工作方式是完全相同的,都有一個handler的函式指標,指向處理函式的地址,這是一個C函式,包含了執行Opcode對應的程式碼,使用op1,op2做為引數,執行完成後,會返回一個結果result,有時也會附加一段資訊extended_value。檔案Zend/zend_vm_execute.h包含了所有的handler對應的函式,php-7.1.0中這個檔案有62000+行。

注意:Zend/zend_vm_execute.h並非手動編寫的,而是由zend_vm_gen.php這個PHP指令碼解析zend_vm_def.h和zend_vm_execute.skl後生成,這個很有意思,先有雞還是先有蛋?沒有PHP 哪來的這個php指令碼呢?這個是後期產物,早期php版本不用這個。這個類似於GO語言的自舉,自己編譯自己。

同一個Opcode對應的handler函式會根據運算元的型別而不同,比如ZEND_ASSIGN對應的handler就有多個:

ZEND_ASSIGN_SPEC_VAR_CONST_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_CONST_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_TMP_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_TMP_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_VAR_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_VAR_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_CV_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_CV_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_CV_TMP_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_CV_TMP_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_CV_VAR_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_CV_VAR_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_CV_CV_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_CV_CV_RETVAL_USED_HANDLER,

其函式命名是有如下規則的:

ZEND_[opcode]_SPEC_(運算元1型別)_(運算元2型別)_(返回值型別)_HANDLER

舉個例子,對於PHP程式碼:

$a = 1;

對應的handler為ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,其定義為:

static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS)

{

      USE_OPLINE

 

      zval *value;

      zval *variable_ptr;

 

      SAVE_OPLINE();

        //獲取op2對應的值,也就是1

      value = EX_CONSTANT(opline->op2);

        //在execute_data中獲取op1的位置,也就是$a

      variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var);

        /*程式碼省略*/

       //將1賦值給$a

      value = zend_assign_to_variable(variable_ptr, value, IS_CONST);

              

      }

         /*程式碼省略*/

      ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

}

從程式碼中可以非常直觀的看出,常量1是如何賦值給CV型別的$a的。

  • (4)extended_value

extended_value是存的擴充套件資訊,Opcodes和CPU的指令類似,有一個標示指令欄位handler,以及這個Opcode所操作的運算元op1和op2,但PHP不像彙編那麼底層, 在指令碼實際執行的時候可能還需要其他更多的資訊,extended_value欄位就儲存了這類資訊;

  • (5)lineno

lineno對應原始碼檔案中的行號。

到這裡,相信讀者對指令opline有了比較深刻的認識,在Zend虛擬機器執行時,這些指令被組裝在一起,成為指令集,下面我們介紹一下指令集。

2.4 指令集

在介紹指令集前,需要先介紹一個編譯過程用到的一個基礎的結構體znode,其結構如下:

  typedef struct _znode { /* used only during compilation */

      zend_uchar op_type;//變數型別

      zend_uchar flag;

      union {

               //表示變數的位置

               znode_op op;

                 //常量

               zval constant; /* replaced by literal/zv */

      } u;

} znode

znode只會在編譯過程中使用,其中op_type對應的是變數的型別,u是聯合體,u.op是運算元的型別,zval constant用來存常量。znode在後續生成指令集時會使用到。

Zend虛擬機器中的指令集對應的結構為zend_op_array,其結構如下:

  struct _zend_op_array {

      /* Common elements */

      /*程式碼省略common是為了函式能夠快速訪問Opcodes而設定的*/

      /* END of common elements */

      //這部分是存放opline的陣列,last為總個數

      uint32_t last;

      zend_op *opcodes;

     

      int last_var;//變數型別為IS_CV的個數

      uint32_t T;//變數型別為IS_VAR和IS_TMP_VAR的個數

      zend_string **vars;//存放IS_CV型別變數的陣列

       /*程式碼省略*/

      /* static variables support */

      HashTable *static_variables; //靜態變數

        /*程式碼省略*/

      int last_literal;//常量的個數

      zval *literals;//常量陣列

 

      int  cache_size;//執行時快取陣列大小

      void **run_time_cache;//執行時快取

 

      void *reserved[ZEND_MAX_RESERVED_RESOURCES];

};

其結構圖如圖8所示。

clipboard.png
圖8 zend_op_array結構圖

這個結構體中有幾個關鍵變數。

  • 1)last和opcodes,這部分是存放Opline的陣列,也就是指令集存放的位置,其中last為陣列中Opline的個數。
  • 2)last_var代表IS_CV型別變數的個數,這種型別變數存放在vars陣列中;在整個編譯過程中,每次遇到一個IS_CV型別的變數(類似於$something),就會去遍歷vars陣列,檢查是否已經存在,如果不存在,則插入到vars中,並將last_var的值設定為該變數的運算元;如果存在,則使用之前分配的運算元,見程式碼:
 result->op_type = IS_CV;

 result->u.op.var = lookup_cv(CG(active_op_array), name);

 

  //lookup_cv:

  static int lookup_cv(zend_op_array *op_array, zend_string* name) /* {{{ */{

      int i = 0;

      zend_ulong hash_value = zend_string_hash_val(name);

 

       //遍歷vars

      while (i < op_array->last_var) {

                  //如果存在直接返回

               if (ZSTR_VAL(op_array->vars[i]) == ZSTR_VAL(name) ||

                   (ZSTR_H(op_array->vars[i]) == hash_value &&

                    ZSTR_LEN(op_array->vars[i]) == ZSTR_LEN(name) &&

                    memcmp(ZSTR_VAL(op_array->vars[i]), ZSTR_VAL(name), ZSTR_LEN(name)) == 0)) {

                        zend_string_release(name);

                        return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);

               }

               i++;

      }

       //否則插入到vars中,並將last_var的值設定為該變數的運算元

      i = op_array->last_var;

      op_array->last_var++;

      if (op_array->last_var > CG(context).vars_size) {

               CG(context).vars_size += 16; /* FIXME */

               op_array->vars = erealloc(op_array->vars, CG(context).vars_size * sizeof(zend_string*));

      }

 

      op_array->vars[i] = zend_new_interned_string(name);

      return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);

}
  • 3)T為IS_VAR和IS_TMP_VAR型別變數的總數,編譯時遇到這種型別,T就會加一,用於後續在執行棧上分配空間。
  • 4)static_variables是用於存放靜態變數的HashTable。
  • 5)literals是用來存放常量(IS_CONST)型別的陣列,last_literal為常量的個數。
  • 6)run_time_cache用作執行時快取的操作,本書不展開討論。

2.5 執行資料

Zend在棧上執行的資料為zend_execute_data,其結構體為:

struct _zend_execute_data {

      const zend_op       *opline;           /* 要執行的指令 */

      zend_execute_data   *call;             /* current call*/

      zval                *return_value;     /* 返回值 */

      zend_function       *func;             /* 執行函式 */

      zval                 This;             /* this + call_info + num_args */

      zend_execute_data   *prev_execute_data;

      zend_array          *symbol_table; /*符號表*/

      void               **run_time_cache;   /* 執行時快取 */

      zval                *literals;         /* 快取常量 */

};

下面我們介紹下各欄位。

  • 1)opline對應的是zend_op_array中Opcodes陣列裡面的zend_op,表示正在執行的opline。
  • 2)prev_execute_data: op_array上下文切換的時候,這個欄位用來儲存切換前的op_array,此欄位非常重要,能將每個op_array的execute_data按照呼叫的先後順序連線成一個單連結串列,每當一個op_array執行結束要還原到呼叫前op_array的時候,就通過當前的execute_data中的prev_execute_data欄位來得到呼叫前的執行器資料。
  • 3)symbol_table,當前使用的符號表,一般會取EG(symbol_table)。
  • 4)literals,常量陣列,用來快取常量。

zend_execute_data是在執行棧上執行的關鍵資料,可以用EX巨集來取其中的值,見程式碼:

#define EX(element) ((execute_data)->element)

瞭解完執行資料,下面接下來討論一下執行棧。

2.6 執行棧

Zend虛擬機器中有個類似函式呼叫棧的結構體,叫_zend_vm_stack。EG裡面的vm_stack也是這種型別的。其定義如下:

struct _zend_vm_stack {

      zval *top; //棧頂位置

      zval *end; //棧底位置

      zend_vm_stack prev;

};

typedef struct _zend_vm_stack *zend_vm_stack;

可以看出,棧的結構比較簡單,有三個變數top指向棧使用到的位置,end指向棧底,pre是指向上一個棧的指標,也就意味著所有棧在一個單向連結串列上。
PHP程式碼在執行時,引數的壓棧操作,以及出棧呼叫執行函式都是在棧上進行的,下面介紹下棧操作的核心函式。

  • 1)初始化

初始化呼叫的函式為zend_vm_stack_init,主要進行記憶體申請,以及對_zend_vm_stack成員變數的初始化,見程式碼:

ZEND_API void zend_vm_stack_init(void)

{

      EG(vm_stack) = zend_vm_stack_new_page(ZEND_VM_STACK_PAGE_SIZE(0 /* main stack */), NULL);

      EG(vm_stack)->top++;

      EG(vm_stack_top) = EG(vm_stack)->top;

      EG(vm_stack_end) = EG(vm_stack)->end;

}

該函式調首先呼叫zend_vm_stack_new_page為EG(vm_stack)申請記憶體,申請的大小為161024 sizeof(zval),見程式碼:

static zend_always_inline zend_vm_stack zend_vm_stack_new_page(size_t size, zend_vm_stack prev) {

      zend_vm_stack page = (zend_vm_stack)emalloc(size);

 

      page->top = ZEND_VM_STACK_ELEMENTS(page);

      page->end = (zval*)((char*)page + size);

      page->prev = prev;

      return page;

}

然後將zend_vm_stack的top指向zend_vm_stack的結束的位置,其中 zend_vm_stack佔用24個位元組,end指向申請記憶體的最尾部,pre指向null,如圖9所示。

clipboard.png
圖9 zend_vm_stack初始化後示意圖

可以看出,多個vm_stack構成單連結串列,將多個棧連線起來,棧初始的大小為16×1024個zval的大小,棧頂部佔用了一個*zval和struct _zend_vm_stack的大小,

  • (2)入棧操作

呼叫的函式為zend_vm_stack_push_call_frame,見程式碼:

static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame(uint32_t call_info, zend_function *func, uint32_t num_args, zend_class_entry *called_scope, zend_object *object)

{

      uint32_t used_stack = zend_vm_calc_used_stack(num_args, func);

 

      return zend_vm_stack_push_call_frame_ex(used_stack, call_info,

               func, num_args, called_scope, object);

}

該函式會分配一塊用於當前作用域的記憶體空間,並返回zend_execute_data的起始位置。首先呼叫zend_vm_calc_used_stack計算棧需要的空間,見程式碼:

 static zend_always_inline uint32_t zend_vm_calc_used_stack(uint32_t num_args, zend_function *func)

{

      uint32_t used_stack = ZEND_CALL_FRAME_SLOT + num_args;

 

      if (EXPECTED(ZEND_USER_CODE(func->type))) {

               used_stack += func->op_array.last_var + func->op_array.T - MIN(func->op_array.num_args, num_args);

      }

      return used_stack * sizeof(zval);

}

這段程式碼是按照zval的大小對齊,我們知道zval為16位元組,那麼對於zend_execute_data,大小為80,那麼對應5個zval;同時對應IS_CV型別變數個數(last_var)以及變數型別為IS_VAR和IS_TMP_VAR的個數(T),如圖10所示。

clipboard.png
圖10 壓棧過程

到此,我們瞭解了Zend虛擬機器中符號表、指令集、執行資料以及執行棧相關的資料結構,下面我們基於這些基本知識,來介紹一下指令集生成的過程。

3 抽象語法樹編譯過程

抽象語法樹(AST)的編譯是生成指令集Opcodes的過程,詞法語法分析後生成的AST會儲存在CG(ast)中,然後Zend虛擬機器會將AST進一步轉換為zend_op_array,以便在虛擬機器中執行。下面我們討論一下AST的編譯過程。

編譯過程在zend_compile函式中,在該函式裡,首先呼叫zendparse做了詞法和語法分析的工作,然後開始對CG(ast)的遍歷,根據節點不同的型別編譯為不同指令opline,程式碼如下:

static zend_op_array *zend_compile(int type)

{

      /**程式碼省略**/

      if (!zendparse()) { //詞法語法分析

               /**程式碼省略**/    

 

                 //初始化zend_op_array       

                 init_op_array(op_array, type, INITIAL_OP_ARRAY_SIZE);

               /**程式碼省略**/

 

                 //遍歷ast生成opline

               zend_compile_top_stmt(CG(ast));

               /**程式碼省略**/

 

                //設定handler

               pass_two(op_array);

                /**程式碼省略**/

      }

       /**程式碼省略**/

      return op_array;

}

從上面的過程中可以看出,編譯的主要過程是op_array的初始化,呼叫zend_compile_top_stmt遍歷抽象語法樹成opline,以及呼叫pass_two函式設定handler。下面我們一一闡述。

3.1 op_array初始化

在遍歷抽象語法樹之前,需要先初始化指令集op_array,用來存放指令。op_array的初始化工作,呼叫的函式為init_op_array,該函式會將op_array進行初始化,程式碼如下:

op_array = emalloc(sizeof(zend_op_array));

init_op_array(op_array, type, INITIAL_OP_ARRAY_SIZE);

void init_op_array(zend_op_array *op_array, zend_uchar type, int initial_ops_size)

{

      op_array->type = type;

      op_array->arg_flags[0] = 0;

      op_array->arg_flags[1] = 0;

      op_array->arg_flags[2] = 0;

      /**程式碼省略**/

}

CG(active_op_array) = op_array;

首先通過emalloc申請記憶體,大小為sizeof(zend_op_array)=208,然後初始化op_array的所有成員變數,把op_array賦值給CG(active_op_array)。

3.2 抽象語法樹編譯

抽象語法樹的編譯過程,是遍歷抽象語法樹,生成對應指令集的過程,編譯是在 zend_compile_top_stmt() 中完成,這個函式是總入口,會被多次遞迴呼叫。其中傳入的引數為CG(ast),這個AST是通過詞法和語法分析得到的。下面我們看一下zend_compile_top_stmt的程式碼:

void zend_compile_top_stmt(zend_ast *ast) /* {{{ */

{

      if (!ast) {

               return;

      }

       //對於kind為ZEND_AST_STMT_LIST的節點,轉換為zend_ast_list

      if (ast->kind == ZEND_AST_STMT_LIST) {

               zend_ast_list *list = zend_ast_get_list(ast);

               uint32_t i;

               //根據children的個數進行遞迴呼叫

               for (i = 0; i < list->children; ++i) {

                        zend_compile_top_stmt(list->child[i]);

               }

               return;

      }

       //其他kind的節點呼叫zend_compile_stmt

      zend_compile_stmt(ast);

 

      if (ast->kind != ZEND_AST_NAMESPACE && ast->kind != ZEND_AST_HALT_COMPILER) {

               zend_verify_namespace();

      }

      if (ast->kind == ZEND_AST_FUNC_DECL || ast->kind == ZEND_AST_CLASS) {

               CG(zend_lineno) = ((zend_ast_decl *) ast)->end_lineno;

               zend_do_early_binding();

      }

}

從程式碼中可以看到,對於zend_compile_top_stmt,會對AST節點的kind進行判斷,然後走不同的邏輯,實際上是對AST的深度遍歷,我們以下面的程式碼為例,看一下對AST的遍歷過程。

<?php

$a = 1;

$b = $a + 2;

echo $b;

可以得到的AST如圖11所示。

clipboard.png
圖11 抽象語法樹示意圖

通過這課抽象語法樹。可以很直觀的看出,CG(ast)節點下面有三個子女:

  • 1)第一個子女,其kind是ZEND_AST_ASSIGN,有兩個子女,分別是ZEND_AST_VAR和ZEND_AST_ZVAL,對應$a=1。
  • 2)第二個子女,其kind也是ZEND_AST_ASSIGN,有兩個子女分別是ZEND_AST_VAR和ZEND_AST_BINARY_OP,其中ZEND_AST_BINARY_OP對應的是相加操作,對應的是$b=$a+2。
  • 3)第三個子女,其kind是ZEND_AST_STMT_LIST,有一個子女,為ZEND_AST_ECHO,對應的是echo $b。

下面我們看整棵樹的遍歷過程。

  • 1. Assign編譯過程
  • 1)首先根節點kind為ZEND_AST_STMT,會呼叫函式zend_ast_get_list將其轉換為zend_ast_list *,得到children的個數為2,接著遞迴呼叫zend_compile_top_stmt,這樣就可以把抽象語法樹根節點的最左子女遍歷一遍,以便生成對應的指令;
  • 2)遍歷第一個子女節點,對應的kind為ZEND_AST_ASSIGN,編譯過程是呼叫函式zend_compile_stmt,繼而呼叫zend_compile_expr函式,見程式碼:
void zend_compile_stmt(zend_ast *ast) /* {{{ */

{

      /*…程式碼省略…*/

      switch (ast->kind) {

               /*…程式碼省略…*/

                 default:

               {

                        znode result;

                        zend_compile_expr(&result, ast);

                        zend_do_free(&result);

               }

                  /*…程式碼省略…*/

}    

void zend_compile_expr(znode *result, zend_ast *ast) /* {{{ */

{

      /*…程式碼省略…*/

      switch (ast->kind) {

               /*…程式碼省略…*/

               case ZEND_AST_ASSIGN:

                        zend_compile_assign(result, ast);

                        return; 

         /*…程式碼省略…*/

}

最終呼叫的函式為zend_compile_assign,對ZEND_AST_ASSIGN節點進行編譯:

void zend_compile_assign(znode *result, zend_ast *ast) /* {{{ */

{

      zend_ast *var_ast = ast->child[0];

      zend_ast *expr_ast = ast->child[1];

 

      znode var_node, expr_node;

      zend_op *opline;

      uint32_t offset;

       /*…程式碼省略…*/

      

      switch (var_ast->kind) {

               case ZEND_AST_VAR:

               case ZEND_AST_STATIC_PROP:

                        offset = zend_delayed_compile_begin();

                        zend_delayed_compile_var(&var_node, var_ast, BP_VAR_W);

                        zend_compile_expr(&expr_node, expr_ast);

                        zend_delayed_compile_end(offset);

                        zend_emit_op(result, ZEND_ASSIGN, &var_node, &expr_node);

                        return;

         /*…程式碼省略…*/

}

從程式碼中可以看出,kind為ZEND_AST_ASSIGN的抽象語法樹有兩個子女,左child為var_ast,右child為expr_ast,分別進行處理。

  • 3)呼叫zend_delayed_compile_begin:
static inline uint32_t zend_delayed_compile_begin(void) /* {{{ */

{

      return zend_stack_count (&CG(delayed_oplines_stack));

}

該函式會獲取CG的delayed_oplines_stack棧頂的位置,其中delayed_oplines_stack是對於依賴後續編譯動作儲存資訊的棧。等expr_ast編譯後使用,呼叫zend_delayed_compile_end(offset)來獲取棧裡的資訊。

  • 4)對於左子女var_ast呼叫zend_delayed_compile_var:
void zend_delayed_compile_var(znode *result, zend_ast *ast, uint32_t type) /* {{{ */

{

      zend_op *opline;

      switch (ast->kind) {

               case ZEND_AST_VAR:

                        zend_compile_simple_var(result, ast, type, 1);

   /**程式碼省略**/

}

其中kind為ZEND_AST_VAR,繼而呼叫zend_compile_simple_var函式:

static void zend_compile_simple_var(znode *result, zend_ast *ast, uint32_t type, int delayed) /* {{{ */

{

      zend_op *opline;

 

      /*…程式碼省略…*/

      } else if (zend_try_compile_cv(result, ast) == FAILURE) {

               /*…程式碼省略…*/

      }

}

繼而呼叫zend_try_compile_cv函式:

static int zend_try_compile_cv(znode *result, zend_ast *ast) /* {{{ */

{

      zend_ast *name_ast = ast->child[0];

      if (name_ast->kind == ZEND_AST_ZVAL) {

               /*…程式碼省略…*/

 

               result->op_type = IS_CV;

               result->u.op.var = lookup_cv(CG(active_op_array), name);

 

      /*…程式碼省略…*/

}

核心函式是lookup_cv,在這裡面組裝了運算元,見程式碼:

static int lookup_cv(zend_op_array *op_array, zend_string* name) /* {{{ */{

      int i = 0;

      zend_ulong hash_value = zend_string_hash_val(name);

       //判斷變數是否在vars中存在,若存在直接返回對應的位置

      while (i < op_array->last_var) {

               if (ZSTR_VAL(op_array->vars[i]) == ZSTR_VAL(name) ||

                   (ZSTR_H(op_array->vars[i]) == hash_value &&

                    ZSTR_LEN(op_array->vars[i]) == ZSTR_LEN(name) &&

                    memcmp(ZSTR_VAL(op_array->vars[i]), ZSTR_VAL(name), ZSTR_LEN(name)) == 0)) {

                        zend_string_release(name);

                        return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);

               }

               i++;

      }

       //若不存在,則寫入vars中,返回新插入的位置

      i = op_array->last_var;

      op_array->last_var++;

      /*…程式碼省略…*/

 

      op_array->vars[i] = zend_new_interned_string(name);

 

      return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);

}

從程式碼中可以看出,變數是存放到op_array->vars中的,而返回的是一個int型的地址,這個是什麼呢?我們看一下巨集ZEND_CALL_VAR_NUM的定義:

#define ZEND_CALL_VAR_NUM(call, n) \

      (((zval*)(call)) + (ZEND_CALL_FRAME_SLOT + ((int)(n))))

#define ZEND_CALL_FRAME_SLOT \

      ((int)((ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval)) - 1) / ZEND_MM_ALIGNED_SIZE(sizeof(zval))))

可以看出,這個值都是sizeof(zval)的整數倍,在筆者的機器上,zval的大小為16,而zend_execute_data大小為80,所以返回的是每個變數的偏移值,即80+16*i,如圖12所示。

clipboard.png
圖12 左子女var_ast編譯示意圖

此時,就將賦值語句$a=1中,左側表示式$a編譯完成,賦值給了znode* result,下面繼續對右子女常量1的編譯。

  • 5)對於右子女,呼叫函式zend_compile_expr進行編譯,見程式碼:
void zend_compile_expr(znode *result, zend_ast *ast) /* {{{ */

{

    /* CG(zend_lineno) = ast->lineno; */

    CG(zend_lineno) = zend_ast_get_lineno(ast);

 

     switch (ast->kind) {

         case ZEND_AST_ZVAL:

            ZVAL_COPY(&result->u.constant, zend_ast_get_zval(ast));

            result->op_type = IS_CONST;

            return;

從程式碼中可以看出,對於常量1,通過ZVAL_COPY,將值複製到result->u.constan中,同時將result->op_type賦值為IS_CONST。這樣,對於assign操作的兩個運算元都編譯完成了,下面我們看一下對應指令opline的生成過程。

  • 6)opline生成呼叫函式zend_emit_op,見程式碼:
static zend_op *zend_emit_op(znode *result, zend_uchar opcode, znode *op1, znode *op2) /* {{{ */

{

       //分配和獲取opline,並設定其opcode

      zend_op *opline = get_next_op(CG(active_op_array));

      opline->opcode = opcode;

        //設定運算元1

      if (op1 == NULL) {

               SET_UNUSED(opline->op1);

      } else {

               SET_NODE(opline->op1, op1);

      }

        //設定運算元2

      if (op2 == NULL) {

               SET_UNUSED(opline->op2);

      } else {

               SET_NODE(opline->op2, op2);

      }

 

      zend_check_live_ranges(opline);

 

      if (result) {

                  //設定返回值

               zend_make_var_result(result, opline);

      }

      return opline;

}

其中對運算元得到設定,對應的是巨集SET_NODE,見程式碼:

  #define SET_NODE(target, src) do { \

               target ## _type = (src)->op_type; \

               if ((src)->op_type == IS_CONST) { \

                        target.constant = zend_add_literal(CG(active_op_array), &(src)->u.constant); \

               } else { \

                        target = (src)->u.op; \

               } \

      } while (0)

 

int zend_add_literal(zend_op_array *op_array, zval *zv) /* {{{ */

{

      int i = op_array->last_literal;

      op_array->last_literal++;

      if (i >= CG(context).literals_size) {

               while (i >= CG(context).literals_size) {

                        CG(context).literals_size += 16; /* FIXME */

               }

               op_array->literals = (zval*)erealloc(op_array->literals, CG(context).literals_size * sizeof(zval));

      }

      zend_insert_literal(op_array, zv, i);

      return i;

}

從程式碼中可以看出,對於運算元1,會將編譯過程中臨時的結構znode傳遞給zend_op中,對於運算元2,因為是常量(IS_CONST),會呼叫zend_add_literal將其插入到op_array->literals中。

從返回值的設定,呼叫的是zend_make_var_result,其程式碼如下:

  static inline void zend_make_var_result(znode *result, zend_op *opline) /* {{{ */

{

        //返回值的型別設定為IS_VAR

      opline->result_type = IS_VAR;

        //這個是返回值的編號,對應T位置

      opline->result.var = get_temporary_variable(CG(active_op_array));

      GET_NODE(result, opline->result);

}

static uint32_t get_temporary_variable(zend_op_array *op_array) /* {{{ */

{

      return (uint32_t)op_array->T++;

}

返回值的型別為IS_VAR,result.var為T的值,下面我們給出Assign操作對應的指令圖,如圖13所示。

clipboard.png
圖13 Assign指令示意圖

從圖13中可以看出,生成的opline中opcode等於38;op1的型別為IS_CV,op1.var對應的是vm_stack上的偏移量;op2的型別為IS_CONST,op2.constant對應的是op_array中literals陣列的下標;result的型別為IS_VAR,result.var對應的是T的值;此時handler的值為空。

  • 2. Add編譯過程

對於“$b =$a+2;”語句,首先是add語句,也就是$a+1,跟assign語句型別類似,不同是呼叫了函式zend_compile_binary_op,見程式碼:

void zend_compile_binary_op(znode *result, zend_ast *ast) /* {{{ */

{

      zend_ast *left_ast = ast->child[0];

      zend_ast *right_ast = ast->child[1];

      uint32_t opcode = ast->attr;//通過attr區分加減乘除等等操作

 

      znode left_node, right_node;

      zend_compile_expr(&left_node, left_ast);

      zend_compile_expr(&right_node, right_ast);

      /*…程式碼省略…*/

       zend_emit_op_tmp(result, opcode, &left_node, &right_node);

      /*…程式碼省略…*/

}

對於加減乘除等操作,kind都是ZEND_AST_BINARY_OP,具體操作通過AST中的attr區分的,因為$a+1會生成臨時變數,因此與Assign操作不同,呼叫的函式是zend_emit_op_tmp:

 static zend_op *zend_emit_op_tmp(znode *result, zend_uchar opcode, znode *op1, znode *op2) /* {{{ */

{

      /*…程式碼與zend_emit_op一樣…*/

      if (result) {

               zend_make_tmp_result(result, opline);

      }

 

      return opline;

}

zend_emit_op_tmp函式與zend_emit_op類似,opline中的運算元1和運算元2做了同樣的操作,而result不同之處在於,其型別是IS_TMP_VAR,因此opline如圖14所示。

clipboard.png
圖14 Add指令示意圖

對於“$b=$a+2;”相當於把臨時變數賦值給$b,與Assign編譯過程一致,生成opline如圖15所示。

clipboard.png
圖15 第2條Assign指令示意圖

  • 3. Echo編譯過程
    對於“echo $b;”,編譯過程類似於Assign和Add的編譯,不同處是呼叫函式zend_compile_echo。
void zend_compile_echo(zend_ast *ast) /* {{{ */

{

      zend_op *opline;

      zend_ast *expr_ast = ast->child[0];

 

      znode expr_node;

      zend_compile_expr(&expr_node, expr_ast);

 

      opline = zend_emit_op(NULL, ZEND_ECHO, &expr_node, NULL);

      opline->extended_value = 0;

}

Echo對應的指令只有一個運算元,對於運算元2,SET_UNUSED巨集設定為IS_UNUSED。

#define SET_UNUSED(op)  op ## _type = IS_UNUSED

生成的opline如圖16所示。

clipboard.png
圖16 Echo指令示意圖

  • 4. Return編譯過程

上面對於AST編譯並沒有結束,PHP程式碼中雖然沒有return操作,但是預設會生成一條ZEND_RETURN指令,通過zend_emit_final_return含設定,程式碼如下:

void zend_emit_final_return(int return_one) /* {{{ */

{

      znode zn;

      zend_op *ret;

      /**程式碼省略**/

 

      zn.op_type = IS_CONST;

      if (return_one) {

               ZVAL_LONG(&zn.u.constant, 1);

      } else {

               ZVAL_NULL(&zn.u.constant);

      }

 

      ret = zend_emit_op(NULL, returns_reference ? ZEND_RETURN_BY_REF : ZEND_RETURN, &zn, NULL);

      ret->extended_value = -1;

}

同樣通過zend_emit_op設定opline,設定以後的opline如圖17所示。

clipboard.png
圖17 Return指令示意圖

經過對Assign、Add和Echo的編譯後,生成的全部oplines如圖18所示。

clipboard.png
圖18 所有指令集示意圖

到這裡,我們瞭解了AST編譯生成opline指令集的過程,包括op1、op2和result的生成過程,但是此時opline中的handler還都是空指標,接下來我們看一下handler設定的過程。

3.3 設定指令handler

抽象語法樹編譯後還有一個重要操作,函式叫pass_two,這個函式中,對opline指令集做了進一步的加工,最主要的工作是設定指令的handler,程式碼如下:

ZEND_API int pass_two(zend_op_array *op_array)

{

     /**程式碼省略**/

   while (opline < end) {//遍歷opline陣列

      if (opline->op1_type == IS_CONST) {

               ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline->op1);

      } else if (opline->op1_type & (IS_VAR|IS_TMP_VAR)) {

       opline->op1.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->op1.var);

          }

     

      if (opline->op2_type == IS_CONST) {

          ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline->op2);

      } else if (opline->op2_type & (IS_VAR|IS_TMP_VAR)) {

          opline->op2.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->op2.var);

               }

    if (opline->result_type & (IS_VAR|IS_TMP_VAR)) {

       opline->result.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->result.var);

     }

     ZEND_VM_SET_OPCODE_HANDLER(opline);

     /**程式碼省略**/

}

從程式碼中可以看出,該函式會對opline指令陣列進行遍歷,對每一條opline指令進行操作,對於op1和op2如果是IS_CONST型別,呼叫ZEND_PASS_TWO_UPDATE_CONSTANT,見程式碼:

 /* convert constant from compile-time to run-time */

# define ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, node) do { \

               (node).constant *= sizeof(zval); \

      } while (0)

根據上一節的知識我們知道,對於IS_CONST型別的變數,其值是存在op_array->literals陣列中,因此,可以直接使用陣列下標乘以sizeof(zval)轉換為偏移量。

對於op1和op2如果是IS_VAR或者IS_TMP_VAR型別的變數,跟上一節一樣,通過ZEND_CALL_VAR_NUM計算偏移量。

另外一個非常重要的工作是通過ZEND_VM_SET_OPCODE_HANDLER(opline),設定opline對應的hanlder,程式碼如下:

ZEND_API void zend_vm_set_opcode_handler(zend_op* op)

{

      op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op);

}

其中opcode和handler之前的對應關係在Zend/zend_vm_execute.h中定義的。opline陣列經過一次遍歷後,handler也就設定完畢,設定後的opline陣列如圖19所示。

clipboard.png
圖19 設定handler後的指令集

到此,整個抽象語法樹就編譯完成了,最終的結果為opline指令集,接下來就是在Zend虛擬機器上執行這些指令。

4執行過程

執行的入口函式為zend_execute,在該函式中會針對上一節生成的opline指令集進行排程執行。首先會在EG(vm_stack)上分配空間,然後每一條指令依次壓棧並呼叫對應的handler。程式碼如下:

ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value)

{

      zend_execute_data *execute_data;

      /**程式碼省略**/

      //壓棧生成execute_data

      execute_data = zend_vm_stack_push_call_frame(ZEND_CALL_TOP_CODE | ZEND_CALL_HAS_SYMBOL_TABLE,

               (zend_function*)op_array, 0, zend_get_called_scope(EG(current_execute_data)), zend_get_this_object(EG(current_execute_data)));

       //設定symbol_table

      if (EG(current_execute_data)) {

               execute_data->symbol_table = zend_rebuild_symbol_table();

      } else {

               execute_data->symbol_table = &EG(symbol_table);

      }

      EX(prev_execute_data) = EG(current_execute_data);

       //初始化execute_data

      i_init_execute_data(execute_data, op_array, return_value);

       //執行

      zend_execute_ex(execute_data);

       //釋放execute_data

      zend_vm_stack_free_call_frame(execute_data);

}

這個程式碼中首先根據op_array中的指令生成對應的execute_data,然後初始化後呼叫handler執行。下面我們具體分析一下執行的過程。

4.1 執行棧分配

執行棧是通過2.6節介紹的zend_vm_stack_push_call_frame完成的,會在EG(vm_stack)上分配一塊記憶體區域,80個位元組用來存放execute_data,緊接著下面是根據last_var和T的數量分配zval大小的空間,以3節編譯生成的指令集為例,分配的棧如圖20所示。

clipboard.png
圖20 執行棧分配示意圖

從圖20中看出,在EG(vm_stack)上分配空間,空間的大小跟op_array中last_var和T的值相關。

4.2 初始化execute_data

在執行棧上分配空間後,會呼叫函式i_init_execute_data對執行資料進行初始化,見程式碼:

    static zend_always_inline void i_init_execute_data(zend_execute_data *execute_data, zend_op_array *op_array, zval *return_value) /* {{{ */

{

         ZEND_ASSERT(EX(func) == (zend_function*)op_array);

 

         EX(opline) = op_array->opcodes;//讀取第一條指令

         EX(call) = NULL;

         EX(return_value) = return_value;//設定返回值

 

         if (EX_CALL_INFO() & ZEND_CALL_HAS_SYMBOL_TABLE) {

                    //賦值符號表

                  zend_attach_symbol_table(execute_data);

          /**程式碼省略**/

 

          //執行時快取

          if (!op_array->run_time_cache) {

                  if (op_array->function_name) {

                           op_array->run_time_cache = zend_arena_alloc(&CG(arena), op_array->cache_size);

                  } else {

                           op_array->run_time_cache = emalloc(op_array->cache_size);

                  }

                  memset(op_array->run_time_cache, 0, op_array->cache_size);

         }

         EX_LOAD_RUN_TIME_CACHE(op_array);

         EX_LOAD_LITERALS(op_array);//設定常量陣列

 

         EG(current_execute_data) = execute_data;

}

從程式碼中可以看出,初始化工作主要做了幾件事:

  • 1)讀取op_array中的第一條指令,賦值給EX(opline),其中EX巨集是對execute_data的取值巨集;
  • 2)設定EX的返回值;
  • 3)賦值符號表;
  • 4)設定執行時快取;
  • 5)設定常量陣列。

做完這些工作後,執行棧中資料的結果如圖21所示。

clipboard.png
圖21 初始化execute_data示意圖

4.3 呼叫hanlder函式執行

接下來是呼叫execute_ex進行指令的執行,見程式碼:

ZEND_API void execute_ex(zend_execute_data *ex)

{

         ZEND_VM_LOOP_INTERRUPT_CHECK();

 

         while (1) { //迴圈

                  int ret;

                  if (UNEXPECTED((ret = ((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) != 0))

       {

                  if (EXPECTED(ret > 0)) {

                           execute_data = EG(current_execute_data);

                           ZEND_VM_LOOP_INTERRUPT_CHECK();

                  } else {

                           return;

                  }

}

}

從程式碼中可以看出,整個執行最外層是while迴圈,直到結束才退出。呼叫的是opline中對應的handler,下面以3節中生成的指令集進行詳細的闡述。

  • 1)對於第一個指令,對應的handler為:
//ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER

//通過op2獲取到常量陣列裡面的值

value = EX_CONSTANT(opline->op2);

//獲取到op1對應的位置

variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var);

//將常量賦值給對應位置的指標

value = zend_assign_to_variable(variable_ptr, value, IS_CONST);

//將結果複製到result

ZVAL_COPY(EX_VAR(opline->result.var), value);

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

首先通過op2.constant值獲取到常量表中的zval值,通過op1.var獲取到棧中對應的位置,然後將常量值賦值到對應的位置,同時將其copy到result對應的位置,如圖22所示。

clipboard.png
圖22 Assign指令執行示意圖

完成assign操作後,會呼叫ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION巨集進行下一條指令的執行,也就是opline+1。

  • 2)第二條指令對應的是相加操作,其handler為:
//ZEND_ADD_SPEC_CV_CONST_HANDLER

zval *op1, *op2, *result;

//獲取op1對應的位置

op1 = _get_zval_ptr_cv_undef(execute_data, opline->op1.var);

//獲取op2對應的值

op2 = EX_CONSTANT(opline->op2);

/**程式碼省略**/

//執行相加操作,賦值給result

add_function(EX_VAR(opline->result.var), op1, op2);

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

首先根據op1.var獲取對應的位置,然後根據op2.constant值獲取到常量表中的zval值,最後進行相加操作,賦值給result對應的位置,如圖23所示。

clipboard.png
圖23 Add指令執行示意圖

  • 3)第三條指令依然是assign,但是因為型別與第一條指令不同,因此對應的handler也不同:
//ZEND_ASSIGN_SPEC_CV_TMP_RETVAL_UNUSED_HANDLER

zval *value;

zval *variable_ptr;

//根據op2.var獲取臨時變數的位置

value = _get_zval_ptr_tmp(opline->op2.var, execute_data, &free_op2);

//根據op1.var獲取運算元1 的位置

variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var);

//將臨時變數賦值給運算元1對應的位置

value = zend_assign_to_variable(variable_ptr, value, IS_TMP_VAR);

//同時拷貝到result對應的位置

ZVAL_COPY(EX_VAR(opline->result.var), value);

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

與第一條指令類似,執行過程如圖24所示。

clipboard.png
圖24 第2條Assign指令示意圖

  • 4)第四條指令是echo操作,對應的handler為:
// ZEND_ECHO_SPEC_CV_HANDLER

zval *z;

//根據op1.var獲取對應位置的值

z = _get_zval_ptr_cv_undef(execute_data, opline->op1.var);

//呼叫zend_write輸出

zend_write(ZSTR_VAL(str), ZSTR_LEN(str));

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

這條指令會根據op1.var獲取到對應的位置,取出zval值輸出,如圖25所示。

clipboard.png
圖25 Echo指令執行示意圖

  • 5)第五條指令為return,對應的handler為:
//ZEND_RETURN_SPEC_CONST_HANDLER

zval *retval_ptr;

zval *return_value;

retval_ptr = EX_CONSTANT(opline->op1);

return_value = EX(return_value);

//呼叫zend_leave_helper_SPEC函式,返回

ZEND_VM_TAIL_CALL(zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));

這條指令沒有做實質性的操作,核心是返回-1,讓while迴圈退出,指令執行結束。

到此,整個的執行過程就闡述完成了,相信讀者通過這五條指令的執行,初步理解了Zend虛擬機器的執行過程。

4.4 釋放execute_data

指令執行完畢後,呼叫zend_vm_stack_free_call_frame釋放execute_data,並回收EG(vm_stack)上使用的空間,這部分比較簡單。

5小結

本文主要介紹了Zend虛擬機器的實現原理,包括抽象語法樹編譯生成指令集的過程,以及指令集執行的過程。同時介紹了Zend虛擬機器執行中用到的資料結構。希望讀者讀完本文,能夠對Zend虛擬機器有一定的認識。

相關文章