亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

【PHP源碼學(xué)習(xí)】2019-03-28 Zend虛擬機(jī)

Neilyo / 1185人閱讀

baiyan

全部視頻:https://segmentfault.com/a/11...

復(fù)習(xí) 基本概念

首先復(fù)習(xí)幾個(gè)基本概念:

opline:在zend虛擬機(jī)中,每條指令都是一個(gè)opline,每個(gè)opline由操作數(shù)、指令操作、返回值組成
opcode:每個(gè)指令操作都對(duì)應(yīng)一個(gè)opcode(如ZEND_ASSIGN/ZEND_ADD等等),在PHP7中,有100多種指令操作,所有的指令集被稱作opcodes
handler:每個(gè)opcode指令操作都對(duì)應(yīng)一個(gè)handler指令處理函數(shù),處理函數(shù)中有具體的指令操作執(zhí)行邏輯

我們知道,在經(jīng)過編譯階段(zend_compile函數(shù))中,我們生成AST并對(duì)其遍歷,生成一條條指令,每一條指令都是一個(gè)opline。之后通過pass_two函數(shù)生成了這些指令所對(duì)應(yīng)的handler,這些信息均存在op_array中。既然指令和handler已經(jīng)生成完畢,接下來的任務(wù)就是要交給zend虛擬機(jī),加載這些指令,并最終執(zhí)行對(duì)應(yīng)的handler邏輯。

指令在PHP7中,由以下元素構(gòu)成:

  struct _zend_op {

      const void *handler; //操作執(zhí)行的函數(shù)

      znode_op op1; //操作數(shù)1

      znode_op op2; //操作數(shù)2

      znode_op result; //返回值

      uint32_t extended_value; //擴(kuò)展值

      uint32_t lineno; //行號(hào)

      zend_uchar opcode; //opcode值

      zend_uchar op1_type; //操作數(shù)1的類型

      zend_uchar op2_type; //操作數(shù)2的類型

      zend_uchar result_type; //返回值的類型

};

在PHP7中,每個(gè)操作數(shù)有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,表示臨時(shí)變量,如$a=”123”.time(); 這里拼接的臨時(shí)變量”123”.time()的類型就是IS_TMP_VAR,一般用于操作的中間結(jié)果
IS_VAR類型:值為4,表示變量,但是這個(gè)變量并不是PHP中常見的聲明變量,而是返回的臨時(shí)變量,如$a = time()中的time()
IS_UNUSED:值為8,表示沒有使用的操作數(shù)
IS_CV:值為16,表示形如$a這樣的變量

對(duì)AST進(jìn)行遍歷之后,最終存放所有指令集(oplines)的地方為op_array:

  struct _zend_op_array {

      uint32_t last; //下面oplines數(shù)組大小

      zend_op *opcodes; //oplines數(shù)組,存放所有指令

      int last_var;//操作數(shù)類型為IS_CV的個(gè)數(shù)

      uint32_t T;//操作數(shù)類型為IS_VAR和IS_TMP_VAR的個(gè)數(shù)之和

      zend_string **vars;//存放IS_CV類型操作數(shù)的數(shù)組

      ...

      int last_literal;//下面常量數(shù)組大小

      zval *literals;//存放IS_CONST類型操作數(shù)的數(shù)組

};
op_array的存儲(chǔ)情況

為了復(fù)習(xí)op_array的存儲(chǔ)情況,我們具體gdb一下,使用下面的測(cè)試用例:


根據(jù)以上測(cè)試用例,在zend_execute處打一個(gè)斷點(diǎn),這里完成了對(duì)AST的遍歷并生成了最終的op_array,已經(jīng)進(jìn)入到虛擬機(jī)執(zhí)行指令的入口。首先我們先觀察傳入的參數(shù)op_array,它是經(jīng)過AST遍歷之后生成的最終的op_array:

last = 2;表示一共有兩個(gè)opcodes:一個(gè)是賦值A(chǔ)SSIGN,另一個(gè)是腳本為我們自動(dòng)生成的返回語句return 1,opcodes是一個(gè)數(shù)組,每個(gè)數(shù)組單元具體存儲(chǔ)了每條指令的信息(操作數(shù)、返回值等等),我們打印一下數(shù)組的內(nèi)容:

last_var = 1;表示有一個(gè)CV類型的變量,這里就是$a

T = 1;表示IS_TMP_VAR和IS_VAR變量類型的數(shù)量之和,而我們腳本中并沒有這樣的變量,它是在存儲(chǔ)中間的返回值的時(shí)候,這個(gè)返回值類型就是一個(gè)IS_VAR類型,所以T的值一開始就為1

vars是一個(gè)二級(jí)指針,可以理解為外層的一級(jí)指針首先指向一個(gè)數(shù)組,這個(gè)數(shù)組里每個(gè)存儲(chǔ)單元都是一個(gè)zend_string*類型的指針,而每個(gè)指針都指向了一個(gè)zend_string結(jié)構(gòu)體,我們打印數(shù)組第一個(gè)單元的值,發(fā)現(xiàn)其指向的zend_string值為a:

last_literal = 2;表示腳本中一共有2個(gè)常量,一個(gè)是我們自己復(fù)制的值2,另一個(gè)是腳本為我們自動(dòng)生成的返回語句return 1中的值1:

literals是一個(gè)zend_array,里面每一個(gè)單元都是一個(gè)zval,存儲(chǔ)這些常量的實(shí)際的值,我們可以看到,其值為2和1,與上面的描述相符:

我們可以畫出最終的op_array存儲(chǔ)結(jié)構(gòu)圖:

這樣一來,我們就可以清晰地看出指令在op_array中是如何存儲(chǔ)的。那么接下來,我們需要將其加載到虛擬機(jī)的執(zhí)行棧楨上,來最終執(zhí)行這些指令。

在虛擬機(jī)上執(zhí)行指令

下面讓我們真正執(zhí)行op_array中的指令,執(zhí)行指令的入口為zend_execute函數(shù),傳入?yún)?shù)為op_array以及一個(gè)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);
}

觀察第一行,聲明了一個(gè)zend_execute_data類型的指針,這個(gè)類型非常重要,存儲(chǔ)了虛擬機(jī)執(zhí)行指令時(shí)的基本信息:

struct _zend_execute_data {
    const zend_op       *opline;          //當(dāng)前執(zhí)行的指令 8B
    zend_execute_data   *call;           //指向自己的指針 8B
    zval                *return_value;         //存儲(chǔ)返回值 8B
    zend_function       *func;              //執(zhí)行的函數(shù) 8B
    zval                 This;             /* this + call_info + num_args   16B */
    zend_execute_data   *prev_execute_data; //鏈表,指向前一個(gè)zend_execute_data 8B
    zend_array          *symbol_table;  //符號(hào)表 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
};

可以看到,這個(gè)zend_execute_data一共是80個(gè)字節(jié)

隨后執(zhí)行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)));這個(gè)函數(shù),我們s進(jìn)去看下:

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);
}

先不看復(fù)雜的函數(shù)參數(shù),直接看zend_vm_calc_used_stack(num_args, func);這個(gè)函數(shù)調(diào)用,它用來計(jì)算虛擬機(jī)在執(zhí)行棧楨上所用的空間,此時(shí)應(yīng)該沒有占用任何空間,我們打印一下used_stack:

發(fā)現(xiàn)這里的used_stack果然是0,然后進(jìn)入下一個(gè)if中,繼續(xù)執(zhí)行used_stack += func->op_array.last_var + func->op_array.T - MIN(func->op_array.num_args, num_args);這個(gè)與函數(shù)相關(guān),我們還沒有講,那么我們直接看這個(gè)函數(shù)外層返回的used_stack值,為112B:

那么繼續(xù)往下執(zhí)行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;
    }
}

同樣忽略復(fù)雜的函數(shù)參數(shù),只關(guān)注傳入的used_stack = 112即可。我們首先看第一行:把executor_globals中的vm_stack_top字段賦值給當(dāng)前的zend_execute_data指向自己的指針,說明zend_execute_data的起始地址為EG這個(gè)宏的返回值,查看這個(gè)值:

可以看到,zend_execute_data的起始地址為0x7ffff5e1c030,繼續(xù)往下執(zhí)行代碼:

下面的if是用來判斷棧上是否有足夠的空間,如果已經(jīng)使用的??臻g太多,那么需要重新分配??臻g,顯然我們這里沒有進(jìn)這個(gè)if,說明??臻g還是夠的,那么執(zhí)行下面的else。重點(diǎn)在于:

EG(vm_stack_top) = (zval*)((char*)call + used_stack);

現(xiàn)在這個(gè)棧頂?shù)奈恢米兂闪?x7ffff5e1c0a0,也就是0x7ffff5e1c030 + 112的結(jié)果。至于指針加法步長(zhǎng)的運(yùn)算,本質(zhì)上就是地址a + 步長(zhǎng) * sizeof(地址類型)(地址類型如果是char *,步長(zhǎng)就是1;如果是Int *,步長(zhǎng)就是4),舉例子:

int *p;
p+3;

假如p的地址是0x7ffff5e1c030,那么p+3的結(jié)果就應(yīng)該是0x7ffff5e1c030 + 3 * sizeof(int) = 0x7ffff5e1c03c

我們畫出此時(shí)棧上的結(jié)構(gòu)圖:

此時(shí)這個(gè)返回值call就是棧頂?shù)奈恢?,但是top指針并不指向棧頂,而是指向棧的中間:

接下來回到最外層的zend_execute函數(shù),繼續(xù)往下執(zhí)行:

可以看到,接下來將符號(hào)表中的內(nèi)容賦值給了execute_data中的symbol_table字段,這個(gè)符號(hào)表是一個(gè)zend_array,此時(shí)還只有幾個(gè)默認(rèn)的_GET這幾個(gè)預(yù)先添加的符號(hào),并沒有我們自己的$a:

那么我們繼續(xù)往下走,關(guān)注i_init_code_execute_data()函數(shù):

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宏對(duì)應(yīng)全局變量execute_data,EG宏對(duì)應(yīng)全局變量executor_globals,要區(qū)分開

重點(diǎn)關(guān)注zend_attach_symbol_table(execute_data)函數(shù):

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  */
     // 從符號(hào)表中拷貝真實(shí)的值到CV槽中,并且創(chuàng)建對(duì)符號(hào)表中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);
    }
}

我們此時(shí)的符號(hào)表只包含_GET這類默認(rèn)初始化的變量,并不包含我們自己的$a。首先進(jìn)入if,因?yàn)閘ast_var = 1($a),所以將str和end賦值,他們分別指向vars和vars后面1偏移量的位置,如圖:

接下來在符號(hào)表ht中遍歷,查找是否有$a這個(gè)CV型變量,現(xiàn)在肯定是沒有的,所以進(jìn)入else分支,執(zhí)行ZVAL_UNDEF(var)與zv = zend_hash_add_new(ht, *str, var);

上面 EX_VAR_NUM(0)這個(gè)宏是一個(gè)申請(qǐng)一個(gè)CV槽大小的空間,但是在這里我們沒有使用,所以ZVAL_UNDEF(var)將這個(gè)槽中的zval類型置為IS_UNDEF類型,然后通過zend_hash_add_new將$a加入到符號(hào)表這個(gè)zend_array中。那么如果下一次再引用$a的時(shí)候,就會(huì)走上面的if分支,這樣CV槽就有了用武之地。把$a拷貝到CV槽中,那么在符號(hào)表中通過間接引用找到它即可,就不用多次將其加入到符號(hào)表中,節(jié)省時(shí)間與空間。最后將str與var指針的位置往后挪,說明本次遍歷完成

回到i_init_code_execute_data函數(shù),下面幾行是用來操作運(yùn)行時(shí)緩存的代碼,我們暫時(shí)跳過,回到zend_execute主函數(shù),接下來會(huì)調(diào)用zend_execute()函數(shù),在這里真正執(zhí)行指令所對(duì)應(yīng)的handler邏輯:

賦值操作對(duì)應(yīng)的是ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,我們看看這個(gè)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數(shù)組中獲取op2對(duì)應(yīng)的值,也就是值2
      value = EX_CONSTANT(opline->op2);
        //在execute_data的符號(hào)表中獲取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);
      ...
}

這樣,一個(gè)賦值指令就被虛擬機(jī)執(zhí)行完畢,那么還有一個(gè)return 1默認(rèn)的腳本返回值的指令,也是同理,這里不再展開,那么最終的虛擬機(jī)執(zhí)行棧楨的情況如下:

回到zend_execute主函數(shù),最后調(diào)用了zend_vm_stack_free_call_frame(execute_data)函數(shù),最終釋放虛擬機(jī)占用的??臻g,完畢。

參考資料

【PHP7源碼分析】PHP7源碼研究之淺談Zend虛擬機(jī)

【PHP7源碼分析】如何理解PHP虛擬機(jī)(一)

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/31649.html

相關(guān)文章

  • 【LNMPR源碼學(xué)習(xí)】筆記匯總

    摘要:此文用于匯總跟隨陳雷老師及團(tuán)隊(duì)的視頻,學(xué)習(xí)源碼過程中的思考整理與心得體會(huì),此文會(huì)不斷更新視頻傳送門每日學(xué)習(xí)記錄使用錄像設(shè)備記錄每天的學(xué)習(xí)源碼學(xué)習(xí)源碼學(xué)習(xí)內(nèi)存管理筆記源碼學(xué)習(xí)內(nèi)存管理筆記源碼學(xué)習(xí)內(nèi)存管理筆記源碼學(xué)習(xí)基本變量筆記 此文用于匯總跟隨陳雷老師及團(tuán)隊(duì)的視頻,學(xué)習(xí)源碼過程中的思考、整理與心得體會(huì),此文會(huì)不斷更新 視頻傳送門:【每日學(xué)習(xí)記錄】使用錄像設(shè)備記錄每天的學(xué)習(xí) PHP7...

    Barrior 評(píng)論0 收藏0
  • PHP7源碼分析】如何理解PHP虛擬機(jī)(一)

    摘要:操作數(shù)本身并無數(shù)據(jù)類型,它的數(shù)據(jù)類型由操作碼確定任何架構(gòu)的計(jì)算機(jī)都會(huì)對(duì)外提供指令集合運(yùn)算器通過執(zhí)行指令直接發(fā)出控制信號(hào)控制計(jì)算機(jī)各項(xiàng)操作。 順風(fēng)車運(yùn)營(yíng)研發(fā)團(tuán)隊(duì) 李樂 1.從物理機(jī)說起 虛擬機(jī)也是計(jì)算機(jī),設(shè)計(jì)思想和物理機(jī)有很多相似之處; 1.1馮諾依曼體系結(jié)構(gòu) 馮·諾依曼是當(dāng)之無愧的數(shù)字計(jì)算機(jī)之父,當(dāng)前計(jì)算機(jī)都采用的是馮諾依曼體系結(jié)構(gòu);設(shè)計(jì)思想主要包含以下幾個(gè)方面: 指令和數(shù)據(jù)不加區(qū)別...

    tunny 評(píng)論0 收藏0
  • PHP7源碼分析】PHP7源碼研究之淺談Zend虛擬機(jī)

    摘要:中詞法語法分析,生成抽象語法樹,然后編譯成及被執(zhí)行均由虛擬機(jī)完成。通常情況下這部分是可選部分,主要為便于程序的讀寫方便而使用。指令虛擬機(jī)的指令稱為,每條指令對(duì)應(yīng)一個(gè)。 作者 陳雷編程語言的虛擬機(jī)是一種可以運(yùn)行中間語言的程序。中間語言是抽象出的指令集,由原生語言編譯而成,作為虛擬機(jī)執(zhí)行階段的輸入。很多語言都實(shí)現(xiàn)了自己的虛擬機(jī),比如Java、C#和Lua。PHP語言也有自己的虛擬機(jī),稱為Z...

    馬龍駒 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<