【PHP7源碼學習】2019-03-28 Zend虛擬機

baiyanphp

所有視頻:https://segmentfault.com/a/11...node

複習

基本概念

  • 首先複習幾個基本概念:segmentfault

    opline:在zend虛擬機中,每條指令都是一個 opline,每一個opline由操做數、指令操做、返回值組成
    opcode:每一個指令操做都對應一個 opcode(如ZEND_ASSIGN/ZEND_ADD等等),在PHP7中,有100多種指令操做,全部的指令集被稱做opcodes
    handler:每一個opcode指令操做都對應一個 handler指令處理函數,處理函數中有具體的指令操做執行邏輯
  • 咱們知道,在通過編譯階段(zend_compile函數)中,咱們生成AST並對其遍歷,生成一條條指令,每一條指令都是一個opline。以後經過pass_two函數生成了這些指令所對應的handler,這些信息均存在op_array中。既然指令和handler已經生成完畢,接下來的任務就是要交給zend虛擬機,加載這些指令,並最終執行對應的handler邏輯。
  • 指令在PHP7中,由如下元素構成:
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; //返回值的類型

};
  • 在PHP7中,每一個操做數有5種類型可選,以下:
#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 */
IS_CONST類型:值爲1,表示常量,如$a = 1中的1或者$a = "hello world"中的hello world
IS_TMP_VAR類型:值爲2,表示臨時變量,如$a=」123」.time(); 這裏拼接的臨時變量」123」.time()的類型就是IS_TMP_VAR,通常用於操做的中間結果
IS_VAR類型:值爲4,表示變量,可是這個變量並非PHP中常見的聲明變量,而是返回的臨時變量,如$a = time()中的time()
IS_UNUSED:值爲8,表示沒有使用的操做數
IS_CV:值爲16,表示形如$a這樣的變量
  • 對AST進行遍歷以後,最終存放全部指令集(oplines)的地方爲op_array:
struct _zend_op_array {

      uint32_t last; //下面oplines數組大小

      zend_op *opcodes; //oplines數組,存放全部指令

      int last_var;//操做數類型爲IS_CV的個數

      uint32_t T;//操做數類型爲IS_VAR和IS_TMP_VAR的個數之和

      zend_string **vars;//存放IS_CV類型操做數的數組

      ...

      int last_literal;//下面常量數組大小

      zval *literals;//存放IS_CONST類型操做數的數組

};

op_array的存儲狀況

  • 爲了複習op_array的存儲狀況,咱們具體gdb一下,使用下面的測試用例:
<?php
$a = 2;
  • 根據以上測試用例,在zend_execute處打一個斷點,這裏完成了對AST的遍歷並生成了最終的op_array,已經進入到虛擬機執行指令的入口。首先咱們先觀察傳入的參數op_array,它是通過AST遍歷以後生成的最終的op_array:

  • last = 2;表示一共有兩個opcodes:一個是賦值ASSIGN,另外一個是腳本爲咱們自動生成的返回語句return 1,opcodes是一個數組,每一個數組單元具體存儲了每條指令的信息(操做數、返回值等等),咱們打印一下數組的內容:

  • last_var = 1;表示有一個CV類型的變量,這裏就是$a
  • T = 1;表示IS_TMP_VAR和IS_VAR變量類型的數量之和,而咱們腳本中並無這樣的變量,它是在存儲中間的返回值的時候,這個返回值類型就是一個IS_VAR類型,因此T的值一開始就爲1
  • vars是一個二級指針,能夠理解爲外層的一級指針首先指向一個數組,這個數組裏每一個存儲單元都是一個zend_string*類型的指針,而每一個指針都指向了一個zend_string結構體,咱們打印數組第一個單元的值,發現其指向的zend_string值爲a:

  • last_literal = 2;表示腳本中一共有2個常量,一個是咱們本身複製的值2,另外一個是腳本爲咱們自動生成的返回語句return 1中的值1:
  • literals是一個zend_array,裏面每個單元都是一個zval,存儲這些常量的實際的值,咱們能夠看到,其值爲2和1,與上面的描述相符:

  • 咱們能夠畫出最終的op_array存儲結構圖:

  • 這樣一來,咱們就能夠清晰地看出指令在op_array中是如何存儲的。那麼接下來,咱們須要將其加載到虛擬機的執行棧楨上,來最終執行這些指令。

在虛擬機上執行指令

  • 下面讓咱們真正執行op_array中的指令,執行指令的入口爲zend_execute函數,傳入參數爲op_array以及一個zval指針:
ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value)
{
    zend_execute_data *execute_data;

    if (EG(exception) != NULL) {
        return;
    }

    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)));
    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);
    i_init_code_execute_data(execute_data, op_array, return_value);
    zend_execute_ex(execute_data);
    zend_vm_stack_free_call_frame(execute_data);
}
  • 觀察第一行,聲明瞭一個zend_execute_data類型的指針,這個類型很是重要,存儲了虛擬機執行指令時的基本信息:
struct _zend_execute_data {
    const zend_op       *opline;          //當前執行的指令 8B
    zend_execute_data   *call;           //指向本身的指針 8B
    zval                *return_value;         //存儲返回值 8B
    zend_function       *func;              //執行的函數 8B
    zval                 This;             /* this + call_info + num_args   16B */
    zend_execute_data   *prev_execute_data; //鏈表,指向前一個zend_execute_data 8B
    zend_array          *symbol_table;  //符號表 8B
#if ZEND_EX_USE_RUN_TIME_CACHE
    void               **run_time_cache;   /* cache op_array->run_time_cache  8B*/
#endif
#if ZEND_EX_USE_LITERALS
    zval                *literals;         /* cache op_array->literals     8B */
#endif
};
  • 能夠看到,這個zend_execute_data一共是80個字節
  • 隨後執行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)));這個函數,咱們s進去看下:
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_vm_calc_used_stack(num_args, func);這個函數調用,它用來計算虛擬機在執行棧楨上所用的空間,此時應該沒有佔用任何空間,咱們打印一下used_stack:

  • 發現這裏的used_stack果真是0,而後進入下一個if中,繼續執行used_stack += func->op_array.last_var + func->op_array.T - MIN(func->op_array.num_args, num_args);這個與函數相關,咱們尚未講,那麼咱們直接看這個函數外層返回的used_stack值,爲112B:

  • 那麼繼續往下執行zend_vm_stack_push_call_frame_ex(used_stack, call_info,func, num_args, called_scope, object):
static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame_ex(uint32_t used_stack, uint32_t call_info, zend_function *func, uint32_t num_args, zend_class_entry *called_scope, zend_object *object)
{
    zend_execute_data *call = (zend_execute_data*)EG(vm_stack_top);

    ZEND_ASSERT_VM_STACK_GLOBAL;

    if (UNEXPECTED(used_stack > (size_t)(((char*)EG(vm_stack_end)) - (char*)call))) {
        call = (zend_execute_data*)zend_vm_stack_extend(used_stack);
        ZEND_ASSERT_VM_STACK_GLOBAL;
        zend_vm_init_call_frame(call, call_info | ZEND_CALL_ALLOCATED, func, num_args, called_scope, object);
        return call;
    } else {
        EG(vm_stack_top) = (zval*)((char*)call + used_stack);
        zend_vm_init_call_frame(call, call_info, func, num_args, called_scope, object);
        return call;
    }
}
  • 一樣忽略複雜的函數參數,只關注傳入的used_stack = 112便可。咱們首先看第一行:把executor_globals中的vm_stack_top字段賦值給當前的zend_execute_data指向本身的指針,說明zend_execute_data的起始地址爲EG這個宏的返回值,查看這個值:

  • 能夠看到,zend_execute_data的起始地址爲0x7ffff5e1c030,繼續往下執行代碼:

  • 下面的if是用來判斷棧上是否有足夠的空間,若是已經使用的棧空間太多,那麼須要從新分配棧空間,顯然咱們這裏沒有進這個if,說明棧空間仍是夠的,那麼執行下面的else。重點在於:
EG(vm_stack_top) = (zval*)((char*)call + used_stack);
  • 如今這個棧頂的位置變成了0x7ffff5e1c0a0,也就是0x7ffff5e1c030 + 112的結果。至於指針加法步長的運算,本質上就是地址a + 步長 * sizeof(地址類型)(地址類型若是是char *,步長就是1;若是是Int *,步長就是4),舉例子:
int *p;
p+3;
  • 假如p的地址是0x7ffff5e1c030,那麼p+3的結果就應該是0x7ffff5e1c030 + 3 * sizeof(int) = 0x7ffff5e1c03c
  • 咱們畫出此時棧上的結構圖:

  • 此時這個返回值call就是棧頂的位置,可是top指針並不指向棧頂,而是指向棧的中間:

enter description here

  • 接下來回到最外層的zend_execute函數,繼續往下執行:

  • 能夠看到,接下來將符號表中的內容賦值給了execute_data中的symbol_table字段,這個符號表是一個zend_array,此時還只有幾個默認的_GET這幾個預先添加的符號,並無咱們本身的$a:

  • 那麼咱們繼續往下走,關注i_init_code_execute_data()函數:
static zend_always_inline void i_init_code_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;

    zend_attach_symbol_table(execute_data);

    if (!op_array->run_time_cache) {
        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;
}
  • 這裏的EX宏對應全局變量execute_data,EG宏對應全局變量executor_globals,要區分開
  • 重點關注zend_attach_symbol_table(execute_data)函數:
ZEND_API void zend_attach_symbol_table(zend_execute_data *execute_data) /* {{{ */
{
    zend_op_array *op_array = &execute_data->func->op_array;
    HashTable *ht = execute_data->symbol_table;

    /* copy real values from symbol table into CV slots and create
       INDIRECT references to CV in symbol table  */
     // 從符號表中拷貝真實的值到CV槽中,而且建立對符號表中CV變量的間接引用
    if (EXPECTED(op_array->last_var)) {
        zend_string **str = op_array->vars;
        zend_string **end = str + op_array->last_var;
        zval *var = EX_VAR_NUM(0);

        do {
            zval *zv = zend_hash_find(ht, *str);

            if (zv) {
                if (Z_TYPE_P(zv) == IS_INDIRECT) {
                    zval *val = Z_INDIRECT_P(zv);

                    ZVAL_COPY_VALUE(var, val);
                } else {
                    ZVAL_COPY_VALUE(var, zv);
                }
            } else {
                ZVAL_UNDEF(var);
                zv = zend_hash_add_new(ht, *str, var);
            }
            ZVAL_INDIRECT(zv, var);
            str++;
            var++;
        } while (str != end);
    }
}
  • 咱們此時的符號表只包含_GET這類默認初始化的變量,並不包含咱們本身的$a。首先進入if,由於last_var = 1($a),因此將str和end賦值,他們分別指向vars和vars後面1偏移量的位置,如圖:

  • 接下來在符號表ht中遍歷,查找是否有$a這個CV型變量,如今確定是沒有的,因此進入else分支,執行ZVAL_UNDEF(var)與zv = zend_hash_add_new(ht, *str, var);

  • 上面 EX_VAR_NUM(0)這個宏是一個申請一個CV槽大小的空間,可是在這裏咱們沒有使用,因此ZVAL_UNDEF(var)將這個槽中的zval類型置爲IS_UNDEF類型,而後經過zend_hash_add_new將$a加入到符號表這個zend_array中。那麼若是下一次再引用$a的時候,就會走上面的if分支,這樣CV槽就有了用武之地。把$a拷貝到CV槽中,那麼在符號表中經過間接引用找到它便可,就不用屢次將其加入到符號表中,節省時間與空間。最後將str與var指針的位置日後挪,說明本次遍歷完成
  • 回到i_init_code_execute_data函數,下面幾行是用來操做運行時緩存的代碼,咱們暫時跳過,回到zend_execute主函數,接下來會調用zend_execute()函數,在這裏真正執行指令所對應的handler邏輯:

  • 賦值操做對應的是ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,咱們看看這個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();
        //從literals數組中獲取op2對應的值,也就是值2
      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);
      ...
}
  • 這樣,一個賦值指令就被虛擬機執行完畢,那麼還有一個return 1默認的腳本返回值的指令,也是同理,這裏再也不展開,那麼最終的虛擬機執行棧楨的狀況以下:

  • 回到zend_execute主函數,最後調用了zend_vm_stack_free_call_frame(execute_data)函數,最終釋放虛擬機佔用的棧空間,完畢。

參考資料

相關文章
相關標籤/搜索