この記事の内容は、PHP7 のソース コードに関するものです。PHP 仮想マシンの詳細な分析です。一定の参考価値があります。必要な友人は参照してください。お役に立てば幸いです。
#1. 物理マシンから始める
仮想マシンもコンピュータであり、その設計思想には物理マシンと多くの類似点があります;1.1 フォン ノイマン アーキテクチャ
フォン ノイマンは、当然のデジタル コンピュータの父です。現在のコンピュータはフォン ノイマン アーキテクチャを使用しており、設計上のアイデアには主に次のようないくつかの側面が含まれています:
どのようなアーキテクチャのコンピュータでも一連の命令が提供されます。命令はオペコードとオペランドで構成されます。オペコードは操作です。オペランドは即値またはストレージ アドレスです。各命令は 0、1、または 2 つのオペランドを持つことができます。
命令はバイナリの文字列です。アセンブリ言語はバイナリ命令のテキスト形式です。 push %ebx
mov %eax, [%esp+8]
mov %ebx, [%esp+12]
add %eax, %ebx
pop %ebx
オペランドはデータにアクセスできる単なる記憶領域です; オペランド自体はデータ型ではなく、そのデータ型はオペレーション コードによって決まります。
たとえば、movb はバイトを送信し、movw はワードを送信し、movl はダブル ワードを送信します。1.3 関数呼び出しスタック
プロセス (関数) はコードのカプセル化であり、外部に公開されるのは指定されたパラメーターのセットとオプションの戻り値のみです。この関数はプログラム内のさまざまな場所で呼び出すことができます。プロセス P がプロセス Q を呼び出すと仮定すると、Q が実行され、その後プロセス P に戻ります。この関数を実装するには、次の 3 つの点を考慮する必要があります:
命令ジャンプ: プロセス Q に入るとき、プログラム カウンタは Q のコード アドレスの先頭に設定する必要があります。戻るとき、プログラム カウンタは P で Q を呼び出した後の命令のアドレスに設定する必要があります。
データ転送: P は 1 つ以上のパラメーターを Q に提供でき、Q は P に値を返すことができます。
メモリの割り当てと解放: Q の開始時実行時に、ローカル変数にメモリ空間を割り当てる必要がある場合があり、戻る前にこれらのメモリ空間を解放する必要があります。
ほとんどの言語プロシージャ呼び出しは、スタック データ構造; 次の図に示すように:
通常発生するスタック オーバーフローは、深すぎる関数を呼び出すことによって発生します。 stack;
2.PHP 仮想マシン
仮想マシン これもコンピュータです。物理マシンの設計を参照して、仮想マシンを設計するときは、次のことを行う必要があります。まず、命令、データ ストレージ、関数スタック フレームの 3 つの要素を検討します。
以下は、これら 3 つの点から PHP 仮想マシンの設計アイデアを詳細に分析したものです。
# 2.1 は次のことを指します
##2.1.1 命令タイプ##あらゆるアーキテクチャのコンピュータは、一連の外部命令を提供する必要があります。これは、によってサポートされる一連の操作タイプを表します。コンピューター; PHP 仮想マシンは、zend_vm_opcodes.h ファイルで定義されている 186 の命令を外部に提供します;//加、减、乘、除等 #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.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类型的,这怎么表示一个操作数呢?(既不是指针不能代表地址,也无法表示所有数据类型); 2.2 数据存储 PHP虚拟机支持多种数据类型:整型、浮点型、字符串、数组,对象等;PHP虚拟机如何存储和表示多种数据类型? 2.1.2.2节指出结构体_znode_op代表一个操作数;操作数可以是一个偏移量(计算得到一个地址,即zval结构体的首地址),或者一个zval指针;PHP虚拟机使用zval结构体表示和存储多种数据; zval.u1.type表示数据类型, zend_types.h文件定义了以下类型: zend_value存储具体的数据内容,结构体定义如下: _zend_value占16字节内存;long、double类型会直接存储在结构体;引用、字符串、数组等类型使用指针存储; 代码中根据zval.u1.type字段,判断数据类型,以此决定操作_zend_value结构体哪个字段; 可以看出,字符串使用zend_string表示,数组使用zend_array表示… 如下图为PHP7中字符串结构图:
2.3 再谈指令 2.1.2.1指出,指令使用结构体_zend_op表示;其中最主要2个属性:操作函数,操作数(两个操作数和一个返回值); 操作数的类型(常量、临时变量等)不同,同一个指令对应的handler函数也会不同;操作数类型定义在 Zend/zend_compile.h文件: 对于$a=1,其操作函数为: ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER;函数实现为: 2.4 函数栈帧 2.4.1指令集 上面分析了指令的结构与表示,PHP虚拟机使用_zend_op_array表示指令的集合: 注意: 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类型的指针; 考虑如何设计函数执行时候的帧结构:当前函数执行时,需要存储函数编译后的指令,需要存储函数内部的局部变量等(2.1.2.2节指出,操作数使用结构体znode_op表示,其内部使用uint32_t表示操作数,此时表示的就是当前zval变量相对于当前函数栈帧首地址的偏移量); PHP虚拟机使用结构体_zend_execute_data存储当前函数执行所需数据; 函数开始执行时,需要为函数分配相应的函数栈帧并入栈,代码如下: 从上面分析可以得到函数栈帧结构图如下所示: 总结 PHP虚拟机也是计算机,有三点是我们需要重点关注的:指令集(包含指令处理函数)、数据存储(zval)、函数栈帧; 此时虚拟机已可以接受指令并执行指令代码; 但是,PHP虚拟机是专用执行PHP代码的,PHP代码如何能转换为PHP虚拟机可以识别的指令呢——编译; PHP虚拟机同时提供了编译器,可以将PHP代码转换为其可以识别的指令集合; 理论上你可以自定义任何语言,只要实现编译器,能够将你自己的语言转换为PHP可以识别的指令代码,就能被PHP虚拟机执行; おすすめ関連記事:
其实,操作数大多情况采用的相对地址表示方式,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;
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;
};
#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
…………
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;
//常量
#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,
…
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();
}
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;
…
};
struct _zend_vm_stack {
zval *top;
zval *end;
zend_vm_stack prev;
};
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 中国語 Web サイトの他の関連記事を参照してください。