成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

PHP內(nèi)核分析:Zend虛擬機(jī)

開(kāi)發(fā) 后端
PHP 是一門(mén)解釋型的語(yǔ)言。諸如 Java、Python、Ruby、Javascript 等解釋型語(yǔ)言,我們編寫(xiě)的代碼不會(huì)被編譯成機(jī)器碼運(yùn)行,而是會(huì)被編譯中間碼運(yùn)行在虛擬機(jī)(VM)上。運(yùn)行 PHP 的虛擬機(jī),稱之為 Zend 虛擬機(jī),今天我們將深入內(nèi)核,探究 Zend 虛擬機(jī)運(yùn)行的原理。

PHP 是一門(mén)解釋型的語(yǔ)言。諸如 Java、Python、Ruby、Javascript 等解釋型語(yǔ)言,我們編寫(xiě)的代碼不會(huì)被編譯成機(jī)器碼運(yùn)行,而是會(huì)被編譯中間碼運(yùn)行在虛擬機(jī)(VM)上。運(yùn)行 PHP 的虛擬機(jī),稱之為 Zend 虛擬機(jī),今天我們將深入內(nèi)核,探究 Zend 虛擬機(jī)運(yùn)行的原理。

OPCODE

什么是 OPCODE?它是一種虛擬機(jī)能夠識(shí)別并處理的指令。Zend 虛擬機(jī)包含了一系列的 OPCODE,通過(guò) OPCODE 虛擬機(jī)能夠做很多事情,列舉幾個(gè) OPCODE 的例子:

  • ZEND_ADD 將兩個(gè)操作數(shù)相加。
  • ZEND_NEW 創(chuàng)建一個(gè) PHP 對(duì)象。
  • ZEND_ECHO 將內(nèi)容輸出到標(biāo)準(zhǔn)輸出中。
  • ZEND_EXIT 退出 PHP。

諸如此類的操作,PHP 定義了186個(gè)(隨著 PHP 的更新,肯定會(huì)支持更多種類的 OPCODE),所有的 OPCODE 的定義和實(shí)現(xiàn)都可以在源碼的 zend/zend_vm_def.h 文件(這個(gè)文件的內(nèi)容并不是原生的 C 代碼,而是一個(gè)模板,后面會(huì)說(shuō)明原因)中查閱到。

我們來(lái)看下 PHP 是如何設(shè)計(jì) OPCODE 數(shù)據(jù)結(jié)構(gòu):

struct _zend_op {
	const void *handler;
	znode_op op1;
	znode_op op2;
	znode_op result;
	uint32_t extended_value;
	uint32_t lineno;
	zend_uchar opcode;
	zend_uchar op1_type;
	zend_uchar op2_type;
	zend_uchar result_type;
};

仔細(xì)觀察 OPCODE 的數(shù)據(jù)結(jié)構(gòu),是不是能找到匯編語(yǔ)言的感覺(jué)。每一個(gè) OPCODE 都包含兩個(gè)操作數(shù),op1和 op2handler 指針則指向了執(zhí)行該 OPCODE 操作的函數(shù),函數(shù)處理后的結(jié)果,會(huì)被保存在 result 中。

我們舉一個(gè)簡(jiǎn)單的例子:

<?php
$b = 1;
$a = $b + 2;

我們通過(guò) vld 擴(kuò)展看到,經(jīng)過(guò)編譯的后,上面的代碼生成了 ZEND_ADD 指令的 OPCODE。

compiled vars:  !0 = $b, !1 = $a
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   ASSIGN                                                   !0, 1
   3     1        ADD                                              ~3      !0, 2
         2        ASSIGN                                                   !1, ~3
   8     3      > RETURN                                                   1

其中,第二行是 ZEND_ADD 指令的 OPCODE。我們看到,它接收2個(gè)操作數(shù),op1 是變量 $bop2 是數(shù)字常量1,返回的結(jié)果存入了臨時(shí)變量中。在 zend/zend_vm_def.h 文件中,我們可以找到 ZEND_ADD 指令對(duì)應(yīng)的函數(shù)實(shí)現(xiàn):

ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMPVAR|CV, CONST|TMPVAR|CV)
{
	USE_OPLINE
	zend_free_op free_op1, free_op2;
	zval *op1, *op2, *result;

	op1 = GE***_ZVAL_PTR_UNDEF(BP_VAR_R);
	op2 = GET_OP2_ZVAL_PTR_UNDEF(BP_VAR_R);
	if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_LONG)) {
		if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_LONG)) {
			result = EX_VAR(opline->result.var);
			fast_long_add_function(result, op1, op2);
			ZEND_VM_NEXT_OPCODE();
		} else if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_DOUBLE)) {
			result = EX_VAR(opline->result.var);
			ZVAL_DOUBLE(result, ((double)Z_LVAL_P(op1)) + Z_DVAL_P(op2));
			ZEND_VM_NEXT_OPCODE();
		}
	} else if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_DOUBLE)) {

	...
}

上面的代碼并不是原生的 C 代碼,而是一種模板。

為什么這樣做?因?yàn)?PHP 是弱類型語(yǔ)言,而其實(shí)現(xiàn)的 C 則是強(qiáng)類型語(yǔ)言。弱類型語(yǔ)言支持自動(dòng)類型匹配,而自動(dòng)類型匹配的實(shí)現(xiàn)方式,就像上述代碼一樣,通過(guò)判斷來(lái)處理不同類型的參數(shù)。試想一下,如果每一個(gè) OPCODE 處理的時(shí)候都需要判斷傳入的參數(shù)類型,那么性能勢(shì)必成為極大的問(wèn)題(一次請(qǐng)求需要處理的 OPCODE 可能能達(dá)到成千上萬(wàn)個(gè))。

哪有什么辦法嗎?我們發(fā)現(xiàn)在編譯的時(shí)候,已經(jīng)能夠確定每個(gè)操作數(shù)的類型(可能是常量還是變量)。所以,PHP 真正執(zhí)行時(shí)的 C 代碼,不同類型操作數(shù)將分成不同的函數(shù),供虛擬機(jī)直接調(diào)用。這部分代碼放在了 zend/zend_vm_execute.h 中,展開(kāi)后的文件相當(dāng)大,而且我們注意到還有這樣的代碼:

if (IS_CONST == IS_CV) {

完全沒(méi)有什么意義是吧?不過(guò)沒(méi)有關(guān)系,C 的編譯器會(huì)自動(dòng)優(yōu)化這樣判斷。大多數(shù)情況,我們希望了解某個(gè) OPCODE 處理的邏輯,還是通過(guò)閱讀模板文件 zend/zend_vm_def.h 比較容易。順便說(shuō)一下,根據(jù)模板生成 C 代碼的程序就是用 PHP 實(shí)現(xiàn)的。

執(zhí)行過(guò)程

準(zhǔn)確的來(lái)說(shuō),PHP 的執(zhí)行分成了兩大部分:編譯和執(zhí)行。這里我將不會(huì)詳細(xì)展開(kāi)編譯的部分,而是把焦點(diǎn)放在執(zhí)行的過(guò)程。

通過(guò)語(yǔ)法、詞法分析等一系列的編譯過(guò)程后,我們得到了一個(gè)名為 OPArray 的數(shù)據(jù),其結(jié)構(gòu)如下:

struct _zend_op_array {
	/* Common elements */
	zend_uchar type;
	zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
	uint32_t fn_flags;
	zend_string *function_name;
	zend_class_entry *scope;
	zend_function *prototype;
	uint32_t num_args;
	uint32_t required_num_args;
	zend_arg_info *arg_info;
	/* END of common elements */

	uint32_t *refcount;

	uint32_t last;
	zend_op *opcodes;

	int last_var;
	uint32_t T;
	zend_string **vars;

	int last_live_range;
	int last_try_catch;
	zend_live_range *live_range;
	zend_try_catch_element *try_catch_array;

	/* static variables support */
	HashTable *static_variables;

	zend_string *filename;
	uint32_t line_start;
	uint32_t line_end;
	zend_string *doc_comment;
	uint32_t early_binding; /* the linked list of delayed declarations */

	int last_literal;
	zval *literals;

	int  cache_size;
	void **run_time_cache;

	void *reserved[ZEND_MAX_RESERVED_RESOURCES];
};

內(nèi)容超多對(duì)吧?簡(jiǎn)單的理解,其本質(zhì)就是一個(gè) OPCODE 數(shù)組外加執(zhí)行過(guò)程中所需要的環(huán)境數(shù)據(jù)的集合。介紹幾個(gè)相對(duì)來(lái)說(shuō)比較重要的字段:

  • opcodes 存放 OPCODE 的數(shù)組。
  • filename 當(dāng)前執(zhí)行的腳本的文件名。
  • function_name 當(dāng)前執(zhí)行的方法名稱。
  • static_variables 靜態(tài)變量列表。
  • last_try_catch try_catch_array 當(dāng)前上下文中,如果出現(xiàn)異常 try-catch-finally 跳轉(zhuǎn)所需的信息。
  • literals 所有諸如字符串 foo 或者數(shù)字23,這樣的常量字面量集合。

為什么需要生成這樣龐大的數(shù)據(jù)?因?yàn)榫幾g時(shí)期生成的信息越多,執(zhí)行時(shí)期所需要的時(shí)間就越少。

接下來(lái),我們看下 PHP 是如何執(zhí)行 OPCODE。OPCODE 的執(zhí)行被放在一個(gè)大循環(huán)中,這個(gè)循環(huán)位于 zend/zend_vm_execute.h 中的 execute_ex 函數(shù):

ZEND_API void execute_ex(zend_execute_data *ex) {
	DCL_OPLINE

	zend_execute_data *execute_data = ex;

	LOAD_OPLINE();
	ZEND_VM_LOOP_INTERRUPT_CHECK();

	while (1) {
		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;
			}
		}
	}

	zend_error_noreturn(E_CORE_ERROR, "Arrived at end of main loop which shouldn't happen");
}

這里,我去掉了一些環(huán)境變量判斷分支,保留了運(yùn)行的主流程。可以看到,在一個(gè)***循環(huán)中,虛擬機(jī)會(huì)不斷調(diào)用 OPCODE 指定的 handler 函數(shù)處理指令集,直到某次指令處理的結(jié)果 ret 小于0。注意到,在主流程中并沒(méi)有移動(dòng) OPCODE 數(shù)組的當(dāng)前指針,而是把這個(gè)過(guò)程放到指令執(zhí)行的具體函數(shù)的結(jié)尾。所以,我們?cè)诖蠖鄶?shù) OPCODE 的實(shí)現(xiàn)函數(shù)的末尾,都能看到調(diào)用這個(gè)宏:

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

在之前那個(gè)簡(jiǎn)單例子中,我們看到 vld 打印出的執(zhí)行 OPCODE 數(shù)組中,***有一項(xiàng)指令為 ZEND_RETURN 的 OPCODE。但我們編寫(xiě)的 PHP 代碼中并沒(méi)有這樣的語(yǔ)句。在編譯時(shí)期,虛擬機(jī)會(huì)自動(dòng)將這個(gè)指令加到 OPCODE 數(shù)組的結(jié)尾。ZEND_RETURN 指令對(duì)應(yīng)的函數(shù)會(huì)返回 -1,判斷執(zhí)行的結(jié)果小于0時(shí),就會(huì)退出循環(huán),從而結(jié)束程序的運(yùn)行。

方法調(diào)用

如果我們調(diào)用一個(gè)自定義的函數(shù),虛擬機(jī)會(huì)如何處理呢?

<?php
function foo() {
    echo 'test';
}

foo();

我們通過(guò) vld 查看生成的 OPCODE。出現(xiàn)了兩個(gè) OPCODE 指令執(zhí)行棧,是因?yàn)槲覀冏远x了一個(gè) PHP 函數(shù)。在***個(gè)執(zhí)行棧上,調(diào)用自定義函數(shù)會(huì)執(zhí)行兩個(gè) OPCODE 指令:INIT_FCALL 和 DO_FCALL

compiled vars:  none
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   NOP
   6     1        INIT_FCALL                                               'foo'
         2        DO_FCALL                                      0
         3      > RETURN                                                   1

compiled vars:  none
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   3     0  E >   ECHO                                                     'test'
   4     1      > RETURN                                                   null

其中,INIT_FCALL 準(zhǔn)備了執(zhí)行函數(shù)時(shí)所需要的上下文數(shù)據(jù)。DO_FCALL 負(fù)責(zé)執(zhí)行函數(shù)。DO_FCALL 的處理函數(shù)根據(jù)不同的調(diào)用情況處理了大量邏輯,我摘取了其中執(zhí)行用戶定義的函數(shù)的邏輯部分:

ZEND_VM_HANDLER(60, ZEND_DO_FCALL, ANY, ANY, SPEC(RETVAL))
{
    USE_OPLINE
    zend_execute_data *call = EX(call);
    zend_function *fbc = call->func;
    zend_object *object;
    zval *ret;

    ...

    if (EXPECTED(fbc->type == ZEND_USER_FUNCTION)) {
        ret = NULL;
        if (RETURN_VALUE_USED(opline)) {
            ret = EX_VAR(opline->result.var);
            ZVAL_NULL(ret);
        }

        call->prev_execute_data = execute_data;
        i_init_func_execute_data(call, &fbc->op_array, ret);

        if (EXPECTED(zend_execute_ex == execute_ex)) {
            ZEND_VM_ENTER();
        } else {
            ZEND_ADD_CALL_FLAG(call, ZEND_CALL_TOP);
            zend_execute_ex(call);
        }
    }

    ...

    ZEND_VM_SET_OPCODE(opline + 1);
    ZEND_VM_CONTINUE();
}

可以看到,DO_FCALL 首先將調(diào)用函數(shù)前的上下文數(shù)據(jù)保存到 call->prev_execute_data,然后調(diào)用 i_init_func_execute_data 函數(shù),將自定義函數(shù)對(duì)象中的 op_array(每個(gè)自定義函數(shù)會(huì)在編譯的時(shí)候生成對(duì)應(yīng)的數(shù)據(jù),其數(shù)據(jù)結(jié)構(gòu)中包含了函數(shù)的 OPCODE 數(shù)組) 賦值給新的執(zhí)行上下文對(duì)象。

然后,調(diào)用 zend_execute_ex 函數(shù),開(kāi)始執(zhí)行自定義的函數(shù)。zend_execute_ex 實(shí)際上就是前面提到的 execute_ex 函數(shù)(默認(rèn)是這樣,但擴(kuò)展可能重寫(xiě) zend_execute_ex 指針,這個(gè) API 讓 PHP 擴(kuò)展開(kāi)發(fā)者可以通過(guò)覆寫(xiě)函數(shù)達(dá)到擴(kuò)展功能的目的,不是本篇的主題,不準(zhǔn)備深入探討),只是上下文數(shù)據(jù)被替換成當(dāng)前函數(shù)所在的上下文數(shù)據(jù)。

我們可以這樣理解,最外層的代碼就是一個(gè)默認(rèn)存在的函數(shù)(類似 C 語(yǔ)言中的 main()函數(shù)),和用戶自定義的函數(shù)本質(zhì)上是沒(méi)有區(qū)別的。

邏輯跳轉(zhuǎn)

我們知道指令都是順序執(zhí)行的,而我們的程序,一般都包含不少的邏輯判斷和循環(huán),這部分又是如何通過(guò) OPCODE 實(shí)現(xiàn)的呢?

<?php
$a = 10;
if ($a == 10) {
    echo 'success';
} else {
    echo 'failure';
}

我們還是通過(guò) vld 查看 OPCODE(不得不說(shuō) vld 擴(kuò)展是分析 PHP 的神器)。

compiled vars:  !0 = $a
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   ASSIGN                                                   !0, 10
   3     1        IS_EQUAL                                         ~2      !0, 10
         2      > JMPZ                                                     ~2, ->5
   4     3    >   ECHO                                                     'success'
         4      > JMP                                                      ->6
   6     5    >   ECHO                                                     'failure'
   7     6    > > RETURN                                                   1

我們看到,JMPZ 和 JMP 控制了執(zhí)行流程。JMP 的邏輯非常簡(jiǎn)單,將當(dāng)前的 OPCODE 指針指向需要跳轉(zhuǎn)的 OPCODE。

ZEND_VM_HANDLER(42, ZEND_JMP, JMP_ADDR, ANY)
{
	USE_OPLINE  	ZEND_VM_SET_OPCODE(OP_JMP_ADDR(opline, opline->op1));
	ZEND_VM_CONTINUE();
}

JMPZ 僅僅是多了一次判斷,根據(jù)結(jié)果選擇是否跳轉(zhuǎn),這里就不再重復(fù)列舉了。而處理循環(huán)的方式與判斷基本上是類似的。

<?php
$a = [1, 2, 3];
foreach ($a as $n) {
    echo $n;
}
compiled vars:  !0 = $a, !1 = $n
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   ASSIGN                                                   !0, <array>
   3     1      > FE_RESET_R                                       $3      !0, ->5
         2    > > FE_FETCH_R                                               $3, !1, ->5
   4     3    >   ECHO                                                     !1
         4      > JMP                                                      ->2
         5    >   FE_FREE                                                  $3
   5     6      > RETURN                                                   1

循環(huán)只需要 JMP 指令即可完成,通過(guò) FE_FETCH_R 指令判斷是否已經(jīng)到達(dá)數(shù)組的結(jié)尾,如果到達(dá)則退出循環(huán)。

結(jié)語(yǔ)

通過(guò)了解 Zend 虛擬機(jī),相信你對(duì) PHP 是如何運(yùn)行的,會(huì)有更深刻的理解。想到我們寫(xiě)的一行行代碼,***機(jī)器執(zhí)行的時(shí)候會(huì)變成數(shù)不勝數(shù)的指令,每個(gè)指令又建立在復(fù)雜的處理邏輯之上。那些從前隨意寫(xiě)下的代碼,現(xiàn)在會(huì)不會(huì)在腦海里不自覺(jué)的轉(zhuǎn)換成 OPCODE 再品味一番呢?

責(zé)任編輯:張燕妮 來(lái)源: Joshua Nie
相關(guān)推薦

2009-12-24 15:09:16

Linux內(nèi)核版本

2009-06-12 16:15:42

死鎖Java虛擬機(jī)

2012-05-18 10:22:23

2012-08-16 09:07:57

Erlang

2010-07-26 09:02:38

2013-07-17 09:32:58

2009-11-24 09:15:54

Linux內(nèi)核虛擬機(jī)KVM架構(gòu)

2021-01-26 09:30:32

加密虛擬機(jī)攻擊

2009-08-18 21:57:59

2017-09-14 10:11:24

OpenStack虛擬機(jī)過(guò)程分析

2012-06-14 10:17:12

虛擬機(jī)

2014-02-21 11:20:34

KVMXen虛擬機(jī)

2023-09-03 17:05:20

虛擬機(jī)

2012-04-10 10:29:29

2010-12-23 14:05:12

虛擬機(jī)

2020-01-17 10:52:37

無(wú)服務(wù)器容器技術(shù)

2015-05-15 10:36:13

2022-09-05 21:46:36

VirtualBox虛擬機(jī)開(kāi)源

2009-12-09 13:41:50

PHP Zend框架

2010-02-04 10:10:34

Dalvik虛擬機(jī)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 国产小视频在线观看 | 一级在线观看 | 欧美一区不卡 | 日韩成人在线免费观看 | 久久精品国产一区二区电影 | 一区不卡在线观看 | 全免费a级毛片免费看视频免 | 在线观看你懂的网站 | 国产精品三级 | 蜜月aⅴ免费一区二区三区 99re在线视频 | 精品一区二区三区91 | 国产区在线免费观看 | 国产精品午夜电影 | 91精品国产91久久久久久最新 | 国产伦精品一区二区三区高清 | 成人网在线观看 | 91高清在线| 精品麻豆剧传媒av国产九九九 | 亚洲狠狠丁香婷婷综合久久久 | 国产精品久久久久久影视 | 亚洲成人播放器 | 日韩视频一级 | 日本免费视频在线观看 | 一级黄色绿像片 | 日韩精品在线网站 | 看特级黄色片 | 久久精品1 | 国产免费观看一区 | 成人在线观看网址 | 欧美精品成人一区二区三区四区 | 美日韩精品 | 一区二区精品视频 | 午夜一区二区三区视频 | 日韩在线免费视频 | 亚洲3p| 亚洲精色| 国产亚洲精品久久午夜玫瑰园 | 日韩在线三级 | 亚洲电影中文字幕 | 国产区精品视频 | 国产高清在线精品 |