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

PHP內核分析:Zend虛擬機

開發 后端
PHP 是一門解釋型的語言。諸如 Java、Python、Ruby、Javascript 等解釋型語言,我們編寫的代碼不會被編譯成機器碼運行,而是會被編譯中間碼運行在虛擬機(VM)上。運行 PHP 的虛擬機,稱之為 Zend 虛擬機,今天我們將深入內核,探究 Zend 虛擬機運行的原理。

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

OPCODE

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

  • ZEND_ADD 將兩個操作數相加。
  • ZEND_NEW 創建一個 PHP 對象。
  • ZEND_ECHO 將內容輸出到標準輸出中。
  • ZEND_EXIT 退出 PHP。

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

我們來看下 PHP 是如何設計 OPCODE 數據結構:

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

仔細觀察 OPCODE 的數據結構,是不是能找到匯編語言的感覺。每一個 OPCODE 都包含兩個操作數,op1和 op2,handler 指針則指向了執行該 OPCODE 操作的函數,函數處理后的結果,會被保存在 result 中。

我們舉一個簡單的例子:

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

我們通過 vld 擴展看到,經過編譯的后,上面的代碼生成了 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個操作數,op1 是變量 $b,op2 是數字常量1,返回的結果存入了臨時變量中。在 zend/zend_vm_def.h 文件中,我們可以找到 ZEND_ADD 指令對應的函數實現:

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 代碼,而是一種模板。

為什么這樣做?因為 PHP 是弱類型語言,而其實現的 C 則是強類型語言。弱類型語言支持自動類型匹配,而自動類型匹配的實現方式,就像上述代碼一樣,通過判斷來處理不同類型的參數。試想一下,如果每一個 OPCODE 處理的時候都需要判斷傳入的參數類型,那么性能勢必成為極大的問題(一次請求需要處理的 OPCODE 可能能達到成千上萬個)。

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

if (IS_CONST == IS_CV) {

完全沒有什么意義是吧?不過沒有關系,C 的編譯器會自動優化這樣判斷。大多數情況,我們希望了解某個 OPCODE 處理的邏輯,還是通過閱讀模板文件 zend/zend_vm_def.h 比較容易。順便說一下,根據模板生成 C 代碼的程序就是用 PHP 實現的。

執行過程

準確的來說,PHP 的執行分成了兩大部分:編譯和執行。這里我將不會詳細展開編譯的部分,而是把焦點放在執行的過程。

通過語法、詞法分析等一系列的編譯過程后,我們得到了一個名為 OPArray 的數據,其結構如下:

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

內容超多對吧?簡單的理解,其本質就是一個 OPCODE 數組外加執行過程中所需要的環境數據的集合。介紹幾個相對來說比較重要的字段:

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

為什么需要生成這樣龐大的數據?因為編譯時期生成的信息越多,執行時期所需要的時間就越少。

接下來,我們看下 PHP 是如何執行 OPCODE。OPCODE 的執行被放在一個大循環中,這個循環位于 zend/zend_vm_execute.h 中的 execute_ex 函數:

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

這里,我去掉了一些環境變量判斷分支,保留了運行的主流程??梢钥吹?,在一個***循環中,虛擬機會不斷調用 OPCODE 指定的 handler 函數處理指令集,直到某次指令處理的結果 ret 小于0。注意到,在主流程中并沒有移動 OPCODE 數組的當前指針,而是把這個過程放到指令執行的具體函數的結尾。所以,我們在大多數 OPCODE 的實現函數的末尾,都能看到調用這個宏:

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

在之前那個簡單例子中,我們看到 vld 打印出的執行 OPCODE 數組中,***有一項指令為 ZEND_RETURN 的 OPCODE。但我們編寫的 PHP 代碼中并沒有這樣的語句。在編譯時期,虛擬機會自動將這個指令加到 OPCODE 數組的結尾。ZEND_RETURN 指令對應的函數會返回 -1,判斷執行的結果小于0時,就會退出循環,從而結束程序的運行。

方法調用

如果我們調用一個自定義的函數,虛擬機會如何處理呢?

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

foo();

我們通過 vld 查看生成的 OPCODE。出現了兩個 OPCODE 指令執行棧,是因為我們自定義了一個 PHP 函數。在***個執行棧上,調用自定義函數會執行兩個 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 準備了執行函數時所需要的上下文數據。DO_FCALL 負責執行函數。DO_FCALL 的處理函數根據不同的調用情況處理了大量邏輯,我摘取了其中執行用戶定義的函數的邏輯部分:

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 首先將調用函數前的上下文數據保存到 call->prev_execute_data,然后調用 i_init_func_execute_data 函數,將自定義函數對象中的 op_array(每個自定義函數會在編譯的時候生成對應的數據,其數據結構中包含了函數的 OPCODE 數組) 賦值給新的執行上下文對象。

然后,調用 zend_execute_ex 函數,開始執行自定義的函數。zend_execute_ex 實際上就是前面提到的 execute_ex 函數(默認是這樣,但擴展可能重寫 zend_execute_ex 指針,這個 API 讓 PHP 擴展開發者可以通過覆寫函數達到擴展功能的目的,不是本篇的主題,不準備深入探討),只是上下文數據被替換成當前函數所在的上下文數據。

我們可以這樣理解,最外層的代碼就是一個默認存在的函數(類似 C 語言中的 main()函數),和用戶自定義的函數本質上是沒有區別的。

邏輯跳轉

我們知道指令都是順序執行的,而我們的程序,一般都包含不少的邏輯判斷和循環,這部分又是如何通過 OPCODE 實現的呢?

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

我們還是通過 vld 查看 OPCODE(不得不說 vld 擴展是分析 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 控制了執行流程。JMP 的邏輯非常簡單,將當前的 OPCODE 指針指向需要跳轉的 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 僅僅是多了一次判斷,根據結果選擇是否跳轉,這里就不再重復列舉了。而處理循環的方式與判斷基本上是類似的。

<?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

循環只需要 JMP 指令即可完成,通過 FE_FETCH_R 指令判斷是否已經到達數組的結尾,如果到達則退出循環。

結語

通過了解 Zend 虛擬機,相信你對 PHP 是如何運行的,會有更深刻的理解。想到我們寫的一行行代碼,***機器執行的時候會變成數不勝數的指令,每個指令又建立在復雜的處理邏輯之上。那些從前隨意寫下的代碼,現在會不會在腦海里不自覺的轉換成 OPCODE 再品味一番呢?

責任編輯:張燕妮 來源: Joshua Nie
相關推薦

2009-12-24 15:09:16

Linux內核版本

2009-06-12 16:15:42

死鎖Java虛擬機

2012-05-18 10:22:23

2013-07-17 09:32:58

2012-08-16 09:07:57

Erlang

2010-07-26 09:02:38

2009-11-24 09:15:54

Linux內核虛擬機KVM架構

2021-01-26 09:30:32

加密虛擬機攻擊

2009-08-18 21:57:59

2017-09-14 10:11:24

OpenStack虛擬機過程分析

2012-06-14 10:17:12

虛擬機

2010-12-23 14:05:12

虛擬機

2014-02-21 11:20:34

KVMXen虛擬機

2012-04-10 10:29:29

2023-09-03 17:05:20

虛擬機

2020-01-17 10:52:37

無服務器容器技術

2015-05-15 10:36:13

2022-09-05 21:46:36

VirtualBox虛擬機開源

2009-12-09 13:41:50

PHP Zend框架

2023-04-26 07:51:36

虛擬機操作系統進程
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 成人av在线播放 | 狠狠av| 国产精品爱久久久久久久 | 亚洲免费观看 | 久久99成人| 麻豆av电影网 | 久久精品久久精品久久精品 | 欧美男人天堂 | 久色视频在线观看 | 黄网站免费入口 | 日韩精品久久 | 黑人精品欧美一区二区蜜桃 | 精品一区二区三区四区在线 | 日韩五月天 | 国产精品成人一区二区 | 久久69精品久久久久久久电影好 | 午夜黄色影院 | 欧美一区二区三区在线观看 | 精品一区二区在线观看 | 丁香久久| 日韩久草 | 性做久久久久久免费观看欧美 | 99国产精品99久久久久久 | 亚洲乱码一区二区 | 亚洲欧美精品国产一级在线 | 日韩精品免费视频 | wwwsihu| 成人精品 | jizz在线免费观看 | 在线观看中文字幕dvd播放 | 久久久在线视频 | 成人免费看片又大又黄 | 精品综合久久久 | 黄色视频a级毛片 | 男人av在线 | 国产91在线播放精品91 | 国产精品久久久久久久白浊 | 久久国产精品久久久久 | 国精产品一区二区三区 | 黄免费在线| 日韩欧美专区 |