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

順風車運營研發團隊 李樂node

1.從物理機提及

虛擬機也是計算機,設計思想和物理機有不少類似之處;數組

1.1馮諾依曼體系結構

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

  • 指令和數據不加區別混合存儲在同一個存儲器中,它們都是內存中的數據。現代CPU的保護模式,每一個內存段都有段描述符,這個描述符記錄着這個內存段的訪問權限(可讀,可寫,可執行)。這就變相的指定了哪些內存中存儲的是指令哪些是數據);
  • 存儲器是按地址訪問的線性編址的一維結構,每一個單元的位數是固定的;
  • 數據以二進制表示;
  • 指令由操做碼和操做數組成。操做碼指明本指令的操做類型,操做數指明操做數自己或者操做數的地址。操做數自己並沒有數據類型,它的數據類型由操做碼肯定;任何架構的計算機都會對外提供指令集合;
  • 運算器經過執行指令直接發出控制信號控制計算機各項操做。由指令計數器指明待執行指令所在的內存地址。指令計數器只有一個,通常按順序遞增,但執行順序可能由於運算結果或當時的外界條件而改變;

clipboard.png

1.2彙編語言簡介

任何架構的計算機都會提供一組指令集合;架構

指令由操做碼和操做數組成;操做碼即操做類型,操做數能夠是一個當即數或者一個存儲地址;每條指令能夠有0、1或2個操做數;函數

指令就是一串二進制;彙編語言是二進制指令的文本形式;ui

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

push、mov、add、pop等就是操做碼;
%ebx寄存器;[%esp+12]內存地址;
操做數只是一塊可存取數據的存儲區;操做數自己並沒有數據類型,它的數據類型由操做碼肯定;
如movb傳送字節,movw傳送字,movl傳送雙字等this

1.3 函數調用棧

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

  • 指令跳轉:進入過程Q的時候,程序計數器必須被設置爲Q的代碼的起始地址;在返回時,程序計數器須要設置爲P中調用Q後面那條指令的地址;
  • 數據傳遞:P可以向Q提供一個或多個參數,Q可以向P返回一個值;
  • 內存分配與釋放:Q開始執行時,可能須要爲局部變量分配內存空間,而在返回前,又須要釋放這些內存空間;

大多數的語言過程調用都採用了棧數據結構提供的內存管理機制;以下圖所示:設計

clipboard.png

函數的調用與返回即對應的是一系列的入棧與出棧操做;
函數在執行時,會有本身私有的棧幀,局部變量就是分配在函數私有棧幀上的;
平時遇到的棧溢出就是由於調用函數層級過深,不斷入棧致使的;指針

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_DIV                               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中字符串結構圖:

clipboard.png

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

從上面分析能夠獲得函數棧幀結構圖以下所示:

clipboard.png

總結

  • PHP虛擬機也是計算機,有三點是咱們須要重點關注的:指令集(包含指令處理函數)、數據存儲(zval)、函數棧幀;
  • 此時虛擬機已能夠接受指令並執行指令代碼;
  • 可是,PHP虛擬機是專用執行PHP代碼的,PHP代碼如何能轉換爲PHP虛擬機能夠識別的指令呢——編譯;
  • PHP虛擬機同時提供了編譯器,能夠將PHP代碼轉換爲其能夠識別的指令集合;
  • 理論上你能夠自定義任何語言,只要實現編譯器,可以將你本身的語言轉換爲PHP能夠識別的指令代碼,就能被PHP虛擬機執行;
相關文章
相關標籤/搜索