首頁 >後端開發 >php教程 >PHP7原始碼:PHP虛擬機器的詳細解析

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

不言
不言原創
2018-08-08 14:04:584537瀏覽

這篇文章帶給大家的內容是關於PHP7原始碼:PHP虛擬機的詳細解析,有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

1.從物理機說起

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

1.1馮諾依曼體系結構

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

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

  • 記憶體是按位址存取的線性編址的一維結構,每個單元的位數是固定的;

  • 資料以二進位表示;

  • #指令由操作碼和運算元組成。操作碼指明本指令的操作類型,操作數指明操作數本身或操作數的位址。操作數本身並非資料型,它的資料型別由操作碼決定;任何架構的計算機都會對外提供指令集合;

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

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

##1.2組譯語言簡介

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

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

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

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

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

push、mov、add、pop等就是操作碼;
�x暫存器;[%esp 12]記憶體位址;
運算元只是一塊可存取資料的儲存區;運算元本身並無資料型別,它的資料型別由操作碼決定;如movb傳送字節,movw傳送字,movl傳送雙字等

1.3 函式呼叫堆疊

#過程(函數)是對程式碼的封裝,對外暴露的只是一組指定的參數和一個可選的返回值;可以在程式中不同的地方呼叫這個函數;假設過程P呼叫過程Q,Q執行後回到過程P;為了實現這個功能,需要考慮三點:
  • 指令跳轉:進入過程Q的時候,程式計數器必須被設定為Q的程式碼的起始位址;在返回時,程式計數器需要設定為P中呼叫Q後面那條指令的位址;
  • 資料傳遞:P能夠提供Q一個或多個參數,Q能夠向P傳回一個值;
  • 記憶體分配與釋放:Q開始執行時,可能需要為局部變數分配記憶體空間,而在返回前,又需要釋放這些記憶體空間;

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

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


函數的呼叫與返回即對應的是一系列的入棧與出棧操作;
函數在執行時,會有自己私有的棧幀,局部變數就是分配在函數私有棧幀上的;

平常遇到的堆疊溢位就是因為呼叫函數層級過深,不斷入堆疊所導致的;

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中字符串结构图:

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

2.3 再谈指令

2.1.2.1指出,指令使用结构体_zend_op表示;其中最主要2个属性:操作函数,操作数(两个操作数和一个返回值);

操作数的类型(常量、临时变量等)不同,同一个指令对应的handler函数也会不同;操作数类型定义在 Zend/zend_compile.h文件:

//常量
#define IS_CONST    (1<p>操作函数命名规则为:ZEND_[opcode]_SPEC_(操作数1类型)_(操作数2类型)_(返回值类型)_HANDLER</p><p>比如赋值语句就有以下多种操作函数:</p><pre class="brush:php;toolbar:false">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;
}

从上面分析可以得到函数栈帧结构图如下所示:

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

总结

PHP虚拟机也是计算机,有三点是我们需要重点关注的:指令集(包含指令处理函数)、数据存储(zval)、函数栈帧;

此时虚拟机已可以接受指令并执行指令代码;

但是,PHP虚拟机是专用执行PHP代码的,PHP代码如何能转换为PHP虚拟机可以识别的指令呢——编译;

PHP虚拟机同时提供了编译器,可以将PHP代码转换为其可以识别的指令集合;

理论上你可以自定义任何语言,只要实现编译器,能够将你自己的语言转换为PHP可以识别的指令代码,就能被PHP虚拟机执行;

相關文章推薦:

PHP7.0和php7.1中的語法新功能的總結

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

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

以上是PHP7原始碼:PHP虛擬機器的詳細解析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn