PHP7源碼:PHP虛擬機器的詳細解析

來源:互聯網
上載者:User
本篇文章給大家帶來的內容是關於PHP7源碼:PHP虛擬機器的詳細解析,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所協助。

1.從物理機說起

虛擬機器也是電腦,設計思想和物理機有很多相似之處;

1.1馮諾依曼體繫結構

馮·諾依曼是當之無愧的數字電腦之父,當前電腦都採用的是馮諾依曼體繫結構;設計思想主要包含以下幾個方面:

  • 指令和資料不加區別混合儲存在同一個儲存空間中,它們都是記憶體中的資料。現代CPU的保護模式,每個記憶體段都有段描述符,這個描述符記錄著這個記憶體段的存取權限(可讀,可寫,可執行)。這就變相的指定了哪些記憶體中儲存的是指令哪些是資料);

  • 儲存空間是按地址訪問的線性編址的一維結構,每個單元的位元是固定的;

  • 資料以二進位表示;

  • 指令由作業碼和運算元組成。作業碼指明本指令的操作類型,運算元指明運算元本身或者運算元的地址。運算元本身並無資料類型,它的資料類型由作業碼確定;任何架構的電腦都會對外提供指令集合;

  • 運算器通過執行指令直接發出控制訊號控制電腦各項操作。由指令計數器指明待執行指令所在的記憶體位址。指令計數器只有一個,一般按順序遞增,但執行順序可能因為運算結果或當時的外界條件而改變;

1.2組合語言簡介

任何架構的電腦都會提供一組指令集合;

指令由作業碼和運算元組成;作業碼即操作類型,運算元可以是一個立即數或者一個儲存地址;每條指令可以有0、1或2個運算元;

指令就是一串二進位;組合語言是二進位指令的文本形式;

push   %ebxmov    %eax, [%esp+8]mov    %ebx, [%esp+12]add    %eax, %ebxpop    %ebx

push、mov、add、pop等就是作業碼;
%ebx寄存器;[%esp+12]記憶體位址;
運算元只是一塊可存取資料的儲存區;運算元本身並無資料類型,它的資料類型由作業碼確定;
如movb傳送位元組,movw傳送字,movl傳送雙字等

1.3 函數調用棧

過程(函數)是對代碼的封裝,對外暴露的只是一組指定的參數和一個可選的傳回值;可以在程式中不同的地方調用這個函數;假設過程P調用過程Q,Q執行後返回過程P;為了實現這一功能,需要考慮三點:

  • 指令跳轉:進入過程Q的時候,程式計數器必須被設定為Q的代碼的起始地址;在返回時,程式計數器需要設定為P中調用Q後面那條指令的地址;

  • 資料傳遞:P能夠向Q提供一個或多個參數,Q能夠向P返回一個值;

  • 記憶體配置與釋放:Q開始執行時,可能需要為局部變數分配記憶體空間,而在返回前,又需要釋放這些記憶體空間;

大多數的語言程序呼叫都採用了棧資料結構提供的記憶體管理機制;如所示:

函數的調用與返回即對應的是一系列的入棧與出棧操作;
函數在執行時,會有自己私人的棧幀,局部變數就是分配在函數私人棧幀上的;
平時遇到的棧溢出就是因為調用函數層級過深,不斷入棧導致的;

2.PHP虛擬機器

虛擬機器也是電腦,參考物理機的設計,設計虛擬機器時,首先應該考慮三個要素:指令,資料存放區,函數棧幀;

下面從這三點詳細分析PHP虛擬機器的設計思路;

2.1指

2.1.1 指令類型

任何架構的電腦都需要對外提供一組指令集,其代表電腦支援的一組操作類型;

PHP虛擬機器對外提供186種指令,定義在zend_vm_opcodes.h檔案中;

//加、減、乘、除等#define ZEND_ADD                               1#define ZEND_SUB                               2#define ZEND_MUL                               3#define ZEND_p                               4#define ZEND_MOD                               5#define ZEND_SL                                6#define ZEND_SR                                7#define ZEND_CONCAT                            8#define ZEND_BW_OR                             9#define ZEND_BW_AND                           10……………………

2.1.2 指令

2.1.2.1指令的表示

指令由作業碼和運算元組成;作業碼指明本指令的操作類型,運算元指明運算元本身或者運算元的地址;

PHP虛擬機器定義指令格式為:作業碼 運算元1 運算元2 傳回值;其使用結構體_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;      //指令類型    zend_uchar op1_type;    //運算元1的類型(此類型並不代表字串、數組等資料類型;其表示此運算元是常量,臨時變數,編譯變數等)    zend_uchar op2_type;    //運算元2的類型    zend_uchar result_type; //傳回值的類型};

2.1.2.2 運算元的表示

從上面可以看到,運算元使用結構體znode_op表示,定義如下:

constant、var、num等都是uint32_t類型的,這怎麼表示一個運算元呢?(既不是指標不能代表地址,也無法表示所有資料類型);
其實,運算元大多情況採用的相對位址表示方式,constant等表示的是相對於執行棧幀首地址的位移量;
另外,_znode_op結構體中有個zval *zv欄位,其也可以表示一個運算元,這個欄位是一個指標,指向的是zval結構體,PHP虛擬機器支援的所有資料類型都使用zval結構體表示;

typedef union _znode_op {        uint32_t      constant;        uint32_t      var;        uint32_t      num;        uint32_t      opline_num;    #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;

2.2 資料存放區

PHP虛擬機器支援多種資料類型:整型、浮點型、字串、數組,對象等;PHP虛擬機器如何儲存和表示多種資料類型?

2.1.2.2節指出結構體_znode_op代表一個運算元;運算元可以是一個位移量(計算得到一個地址,即zval結構體的首地址),或者一個zval指標;PHP虛擬機器使用zval結構體表示和儲存多種資料;

struct _zval_struct {    zend_value        value;            //儲存實際的value值    union {        struct {                        //一些標誌位            ZEND_ENDIAN_LOHI_4(                zend_uchar    type,         //重要;表示變數類型                zend_uchar    type_flags,                zend_uchar    const_flags,                zend_uchar    reserved)     /* call info for EX(This) */        } v;        uint32_t type_info;    } u1;    union {                                 //其他有用資訊        uint32_t     next;                 /* hash collision chain */        uint32_t     cache_slot;           /* literal cache slot */        uint32_t     lineno;               /* line number (for ast nodes) */        uint32_t     num_args;             /* arguments number for EX(This) */        uint32_t     fe_pos;               /* foreach position */        uint32_t     fe_iter_idx;          /* foreach iterator index */        uint32_t     access_flags;         /* class constant access flags */        uint32_t     property_guard;       /* single property guard */    } u2;};

zval.u1.type表示資料類型, zend_types.h檔案定義了以下類型:

#define IS_UNDEF                    0#define IS_NULL                     1#define IS_FALSE                    2#define IS_TRUE                     3#define IS_LONG                     4#define IS_DOUBLE                   5#define IS_STRING                   6#define IS_ARRAY                    7#define IS_OBJECT                   8#define IS_RESOURCE                 9#define IS_REFERENCE                10…………

zend_value儲存具體的資料內容,結構體定義如下:

_zend_value佔16位元組記憶體;long、double類型會直接儲存在結構體;引用、字串、數組等類型使用指標儲存;

代碼中根據zval.u1.type欄位,判斷資料類型,以此決定操作_zend_value結構體哪個欄位;

可以看出,字串使用zend_string表示,數組使用zend_array表示…

typedef union _zend_value {    zend_long         lval;                double            dval;                zend_refcounted  *counted;    zend_string      *str;    zend_array       *arr;    zend_object      *obj;    zend_resource    *res;    zend_reference   *ref;    zend_ast_ref     *ast;    zval             *zv;    void             *ptr;    zend_class_entry *ce;    zend_function    *func;    struct {        uint32_t w1;        uint32_t w2;    } ww;} zend_value;

如為PHP7中字串結構圖:

2.3 再談指令

2.1.2.1指出,指令使用結構體_zend_op表示;其中最主要2個屬性:操作函數,運算元(兩個運算元和一個傳回值);

運算元的類型(常量、臨時變數等)不同,同一個指令對應的handler函數也會不同;運算元類型定義在 Zend/zend_compile.h檔案:

//常量#define IS_CONST    (1<<0) //臨時變數,用於操作的中間結果;不能被其他指令對應的handler重複使用#define IS_TMP_VAR  (1<<1) //這個變數並不是PHP代碼中聲明的變數,常見的是返回的臨時變數,比如$a=time(), 函數time傳回值的類型就是IS_VAR,這種類型的變數是可以被其他指令對應的handler重複使用的#define IS_VAR      (1<<2)#define IS_UNUSED   (1<<3)  /* Unused variable */ //編譯變數;即PHP中聲明的變數;#define IS_CV       (1<<4)  /* Compiled variable */

操作函數命名規則為:ZEND_[opcode]_SPEC_(運算元1類型)_(運算元2類型)_(傳回值類型)_HANDLER

比如指派陳述式就有以下多種操作函數:

ZEND_ASSIGN_SPEC_VAR_CONST_RETVAL_UNUSED_HANDLER,ZEND_ASSIGN_SPEC_VAR_TMP_RETVAL_UNUSED_HANDLER,ZEND_ASSIGN_SPEC_VAR_VAR_RETVAL_UNUSED_HANDLER,ZEND_ASSIGN_SPEC_VAR_CV_RETVAL_UNUSED_HANDLER,…

對於$a=1,其操作函數為: 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(execute_data類似函數棧幀,後面詳細分析)    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);    if (UNEXPECTED(0)) {        ZVAL_COPY(EX_VAR(opline->result.var), value);    }     ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();}

2.4 函數棧幀

2.4.1指令集

上面分析了指令的結構與表示,PHP虛擬機器使用_zend_op_array表示指令的集合:

struct _zend_op_array {    …………    //last表示指令總數;opcodes為儲存指令的數組;    uint32_t last;    zend_op *opcodes;    //變數類型為IS_CV的個數    int last_var;    //變數類型為IS_VAR和IS_TEMP_VAR的個數    uint32_t T;    //存放IS_CV類型變數的數組    zend_string **vars;     …………         //靜態變數    HashTable *static_variables;     //常量個數;常量數組    int last_literal;    zval *literals;     …};

注意: last_var代表IS_CV類型變數的個數,這種類型變數存放在vars數組中;在整個編譯過程中,每次遇到一個IS_CV類型的變數(類似於$something),就會去遍曆vars數組,檢查是否已經存在,如果不存在,則插入到vars中,並將last_var的值設定為該變數的運算元;如果存在,則使用之前分配的運算元

2.4.2 函數棧幀

PHP虛擬機器實現了與1.3節物理機類似的函數棧幀結構;

使用 _zend_vm_stack表示棧結構;多個棧之間使用prev欄位形成單向鏈表;top和end指向棧低和棧頂,分別為zval類型的指標;

struct _zend_vm_stack {    zval *top;    zval *end;    zend_vm_stack prev;};

考慮如何設計函數執行時候的幀結構:當前函數執行時,需要儲存函數編譯後的指令,需要儲存函數內部的局部變數等(2.1.2.2節指出,運算元使用結構體znode_op表示,其內部使用uint32_t表示運算元,此時表示的就是當前zval變數相對於當前函數棧幀首地址的位移量);

PHP虛擬機器使用結構體_zend_execute_data儲存當前函數執行所需資料;

struct _zend_execute_data {    //當前指令指令    const zend_op       *opline;     //當前函數執行棧幀    zend_execute_data   *call;     //函數返回資料              zval                *return_value;    zend_function       *func;                zval                 This;      /* this + call_info + num_args */    //調用當前函數的棧幀           zend_execute_data   *prev_execute_data;    //符號表    zend_array          *symbol_table;#if ZEND_EX_USE_RUN_TIME_CACHE    void               **run_time_cache;  #endif#if ZEND_EX_USE_LITERALS    //常量數組    zval                *literals;        #endif};

函數開始執行時,需要為函數分配相應的函數棧幀併入棧,代碼如下:

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);} //計算函數棧幀大小static zend_always_inline uint32_t zend_vm_calc_used_stack(uint32_t num_args, zend_function *func){    //_zend_execute_data大小(80位元組/16位元組=5)+參數數目    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);    }     //乘以16位元組    return used_stack * sizeof(zval);} //入棧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);     //移動函數調用棧top指標    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;}

從上面分析可以得到函數棧幀結構圖如下所示:

總結

PHP虛擬機器也是電腦,有三點是我們需要重點關注的:指令集(包含指令處理函數)、資料存放區(zval)、函數棧幀;

此時虛擬機器已可以接受指令並執行指令代碼;

但是,PHP虛擬機器是專用執行PHP代碼的,PHP代碼如何能轉換為PHP虛擬機器可以識別的指令呢——編譯;

PHP虛擬機器同時提供了編譯器,可以將PHP代碼轉換為其可以識別的指令集合;

理論上你可以自訂任何語言,只要實現編譯器,能夠將你自己的語言轉換為PHP可以識別的指令代碼,就能被PHP虛擬機器執行;

相關文章推薦:

PHP7.0和php7.1中的文法新特性的總結

PHP中如何將session存入資料庫並使用(附代碼)

PHP中時間函數strtotime() 函數的原理講解

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.