【⾛进php内核】之Zend引擎执⾏过程
Zend引擎主要包含两个核⼼部分:编译、执⾏:
前⾯分析了Zend的编译过程以及PHP⽤户函数的实现,接下来分析下Zend引擎的执⾏过程。
1 数据结构
执⾏流程中有⼏个重要的数据结构,先看下这⼏个结构。
1.1 opcode
opcode是将PHP代码编译产⽣的Zend虚拟机可识别的指令,php7共有173个opcode,定义在zend_vm_opcodes.h中,PHP中的所有语法实现都是由这些opcode组成的。
struct _zend_op {
const void *handler; //对应执⾏的C语⾔function,即每条opcode都有⼀个C function处理
znode_op op1;  //操作数1
znode_op op2;  //操作数2
znode_op result; //返回值
uint32_t extended_value;
uint32_t lineno;
zend_uchar opcode;  //opcode指令
zend_uchar op1_type; //操作数1类型
zend_uchar op2_type; //操作数2类型
zend_uchar result_type; //返回值类型
};
1.2 zend_op_array
zend_op_array是Zend引擎执⾏阶段的输⼊,整个执⾏阶段的操作都是围绕着这个结构,关于其具体结构前⾯我们已经讲过了。
这⾥再重复说下zend_op_array⼏个核⼼组成部分:
opcode指令:即PHP代码具体对应的处理动作,与⼆进制程序中的代码段对应
字⾯量存储:PHP代码中定义的⼀些变量初始值、调⽤的函数名称、类名称、常量名称等等称之为字⾯量,这些值⽤于执⾏时初始化变量、函数调⽤等等
变量分配情况:与字⾯量类似,这⾥指的是当前opcodes定义了多少变量、临时变量,每个变量都有⼀个对应的编号,执⾏初始化按照总的数⽬⼀次性分配zval,使⽤时也完全按照编号索引,⽽不是根据变量名索引
1.3 zend_executor_globals
zend_executor_globals executor_globals是PHP整个⽣命周期中最主要的⼀个结构,是⼀个全局变量,在main执⾏前分配(⾮ZTS下),直到PHP退出,它记录着当前请求全部的信息,经常见到的⼀个宏EG操作的就是这个结构。
//zend_compile.c
#ifndef ZTS
ZEND_API zend_compiler_globals compiler_globals;
ZEND_API zend_executor_globals executor_globals;
#endif
//zend_globals_macros.h
# define EG(v) (executor_globals.v)
zend_executor_globals结构⾮常⼤,定义在zend_globals.h中,⽐较重要的⼏个字段含义如下图所⽰:
1.4 zend_execute_data
zend_execute_data是执⾏过程中最核⼼的⼀个结构,每次函数的调⽤、include/require、eval等都会⽣成⼀个新的结构,它表⽰当前的作⽤域、代码的执⾏位置以及局部变量的分配等等,等同于机器码执⾏过程中stack的⾓⾊,后⾯分析具体执⾏流程的时候会详细分析其作⽤。
#define EX(element)            ((execute_data)->element)
//zend_compile.h
struct _zend_execute_data {
const zend_op      *opline;  //指向当前执⾏的opcode,初始时指向zend_op_array起始位置
zend_execute_data  *call;            /* current call                  */
zval                *return_value;  //返回值指针
zend_function      *func;          //当前执⾏的函数(⾮函数调⽤时为空)
zval                This;          //这个值并不仅仅是⾯向对象的this,还有另外两个值也通过这个记录:call_info + num_args,分别存在served、zval.u2.num_args
zend_class_entry    *called_scope;  //当前call的类
zend_execute_data  *prev_execute_data; //函数调⽤时指向调⽤位置作⽤空间
zend_array          *symbol_table; //全局变量符号表
#if ZEND_EX_USE_RUN_TIME_CACHE
void              **run_time_cache;  /* cache op_array->run_time_cache */
#endif
#if ZEND_EX_USE_LITERALS
zval                *literals;  //字⾯量数组,与func.op_array->literals相同
#endif
};
zend_execute_data与zend_op_array的关联关系:
2 执⾏流程
Zend的executor与linux⼆进制程序执⾏的过程是⾮常类似的,在C程序执⾏时有两个寄存器ebp、esp分别指向当前作⽤栈的栈顶、栈底,局部变量全部分配在当前栈,函数调⽤、返回通过call、ret指令完成,调⽤时call将当前执⾏位置压⼊栈中,返回时ret将之前执⾏位置出栈,跳回旧的位置继续执⾏,在Zend VM中zend_execute_data就扮演了这两个⾓⾊,zend_execute_data.prev_execute_data保存的是调⽤⽅的信息,实现了call/ret,zend_execute_data后⾯会分配额外的内存空间⽤于局部变量的存储,实现了ebp/esp的作⽤。
注意:在执⾏前分配内存时并不仅仅是分配了zend_execute_data⼤⼩的空间,除了sizeof(zend_execute_data)外还会额外申请⼀块空间,⽤于分配局部变量、临时(中间)变量等,具体的分配过程下⾯会讲到。
Zend执⾏opcode的简略过程:
step1: 为当前作⽤域分配⼀块内存,充当运⾏栈,zend_execute_data结构、所有局部变量、中间变量等等都在此内存上分配step2: 初始化全局变量符号表,然后将全局执⾏位置指针EG(current_execute_data)指向step1新分配的zend_execute_data,然后将zend_execute_data.opline指向op_array的起始位置
step3: 从EX(opline)开始调⽤各opcode的C处理handler(即_zend_op.handler),每执⾏完⼀条opcode
将EX(opline)++继续执⾏下⼀条,直到执⾏完全部opcode,函数/类成员⽅法调⽤、if的执⾏过程:
step3.1: if语句将根据条件的成⽴与否决定EX(opline) + offset所加的偏移量,实现跳转
step3.2: 如果是函数调⽤,则⾸先从EG(function_table)中根据function_name取出此function对应的编译完成的
zend_op_array,然后像step1⼀样新分配⼀个zend_execute_data结构,将EG(current_execute_data)赋值给新结构的
prev_execute_data,再将EG(current_execute_data)指向新的zend_execute_data,最后从新的zend_execute_data.opline 开始执⾏,切换到函数内部,函数执⾏完以后将EG(current_execute_data)重新指向EX(prev_execute_data),释放分配的运⾏栈,销毁局部变量,继续从原来函数调⽤的位置执⾏
step3.3: 类⽅法的调⽤与函数基本相同,后⾯分析对象实现的时候再详细分析
step4: 全部opcode执⾏完成后将step1分配的内存释放,这个过程会将所有的局部变量"销毁",执⾏阶段结束
接下来详细看下整个流程。
Zend执⾏⼊⼝为位于zend_vm_execute.h⽂件中的__zend_execute()__:
ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value)
{
zend_execute_data *execute_data;
if (EG(exception) != NULL) {
return;
}
//分配zend_execute_data
execute_data = zend_vm_stack_push_call_frame(ZEND_CALL_TOP_CODE,
(zend_function*)op_array, 0, zend_get_called_scope(EG(current_execute_data)), zend_get_this_object(EG(current_execute_data)));
if (EG(current_execute_data)) {
execute_data->symbol_table = zend_rebuild_symbol_table();
} else {
execute_data->symbol_table = &EG(symbol_table);
}
EX(prev_execute_data) = EG(current_execute_data); //=> execute_data->prev_execute_data = EG(current_execute_data);
i_init_execute_data(execute_data, op_array, return_value); //初始化execute_data
zend_execute_ex(execute_data); //执⾏opcode
zend_vm_stack_free_call_frame(execute_data); //释放execute_data:销毁所有的PHP变量
}
上⾯的过程分为四步:
(1)分配stack
由zend_vm_stack_push_call_frame函数分配⼀块⽤于当前作⽤域的内存空间,返回结果是zend_execute_data的起始位置。
//zend_execute.h
static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame(uint32_t call_info, zend_function *func, uint32_t num_args, ...)
{
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);
}
⾸先根据zend_execute_data、当前zend_op_array中局部/临时变量数计算需要的内存空间:
//zend_execute.hphp8兼容php7吗
static zend_always_inline uint32_t zend_vm_calc_used_stack(uint32_t num_args, zend_function *func)
{
uint32_t used_stack = ZEND_CALL_FRAME_SLOT + num_args; //内部函数只⽤这么多,临时变量是编译过程中根据PHP的代码优化出的值,⽐
如:`"hi~".time()`,⽽在内部函数中则没有这种情况
if (EXPECTED(ZEND_USER_CODE(func->type))) { //在php脚本中写的function
used_stack += func->op_array.last_var + func->op_array.T - MIN(func->op_array.num_args, num_args);
}
return used_stack * sizeof(zval);
}
//zend_compile.h
#define ZEND_CALL_FRAME_SLOT \
((int)((ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval)) - 1) /
ZEND_MM_ALIGNED_SIZE(sizeof(zval))))
回想下前⾯编译阶段zend_op_array的结果,在编译过程中已经确定当前作⽤域下有多少个局部变量(func->op_array.last_var)、临时/中间/⽆⽤变量(func->op_array.T),从⽽在执⾏之初就将他们全部分配完成:
last_var:PHP代码中定义的变量数,zend_op.op{1|2}_type = IS_CV 或 result_type & IS_CV的全部数量
T:表⽰⽤到的临时变量、⽆⽤变量等,zend_op.op{1|2}_type = IS_TMP_VAR|IS_VAR 或resulte_type &
(IS_TMP_VAR|IS_VAR)的全部数量

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。