深入理解PHP原理之PHP脚本执行原理(1)

本文深入探讨了PHP的执行过程,从词法分析、语法分析到opcode的生成,解析了Zend Engine如何将PHP源代码转化为指令序列并执行,揭示了PHP脚本在不同阶段的转换和处理细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

往期精选(欢迎转发~~)

PHP是一个被广泛应用的脚本语言,因为它的成功,所以很多时候,我们应用PHP的时候是不需要考虑底层到底是怎么实现的,我相信大多数的PHP程序员是不会去考虑这一点的,在这篇文章中,我会从整个PHP的执行期入手,大致的介绍下各个阶段,包括词法分析、语法分析和opcode。从最初我们编写的PHP脚本->到最后脚本被执行->得到执行结果,这个过程可以分为如下几个阶段:
  1. Zend Engine(ZE)调用词法分析器(Lex生成的,源码路径:php/Zend/zend_language_sanner.l), 将我们要执行的PHP源文件去掉空格和注释后,分割成一个个的token;
  2. ZE会将得到的token forward给语法分析器(yacc生成, 源码路径:php/Zend/zend_language_parser.y),将Tokens转换成简单而有意义的表达式;
  3. ZE会将转换后的表达式,编译为一个个opcode,opcode一般会以op_array的形式存在,它是PHP执行的中间语言。
  4. ZE调用zend_executor来执行op_array,输出结果。
  下面是PHP脚本执行的整个流程:
  这里写图片描述
  ZE是一个虚拟机,正是由于它的存在,所以才能使得我们写PHP脚本,完全不需要考虑所在的操作系统类型是什么。ZE是一个CISC(复杂指令处理器),它支持173条指令(源码路径:php/Zend/zend_vm_opcodes.h),包括从最简单的ZEND_ECHO(echo)到复杂的 ZEND_INCLUDE_OR_EVAL(include,require),所有我们编写的PHP都会最终被处理为这173条指令(op code)的序列,从而最终被执行。 
  上面初步讲解了语法分析、词法分析和PHP脚本执行的整个流程,下面让我看具体看看opcode的结构( 源码路径:php/Zend/zend_compile.h):

struct _zend_op {
	opcode_handler_t handler;  	// 执行该opcode时调用的处理函数
	znode result;				// 执行完后的返回结果
	znode op1;					// 操作数1
	znode op2;					// 操作数2
	ulong extended_value;		// 执行时可能还需要用到的其它信息
	uint lineno;
	zend_uchar opcode;        	// opcode代码
};

该结构有一个标示指令字段zend_op.opcode,以及这个opcode所操作的操作数op1、op2,extended_value字段保存了脚本实际执行的时候可能还需要的其他信息,result字段保存该指令执行完成后的结果,handler字段指明了对应的处理函数,例如如下代码是在编译器遇到print语句的时候进行编译的函数:

void zend_do_print(znode *result,const znode *arg TSRMLS_DC)
{
	zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);
	opline->result.op_type = IS_TMP_VAR;
	opline->result.u.var = get_temporary_variable(CG(active_op_array));
	opline->opcode = ZEND_PRINT;
	opline->op1 = *arg;
	SET_UNUSED(opline->op2);
	*result = opline->result;
}

当遇到print时,该函数先创建一个zend_op(整个是opcode的最小片段),然后将zend_op.result类型设置为临时变量IS_TMP_VAR,并为该临时变量申请空间,随后指定zend_op.opcode的值为ZEND_PRINT(很奇怪,该值在opcode的指令库中没有查询到),并将传递进来的参数赋值给zend_op.op1,这样在最终执行这条opcode的时候,Zend引擎能获取到对应的信息以便内容输出。
   生成的opcode最终会保存在op_array数组中,然后交给Zend处理,op_array的结构如下(源码路径:php/Zend/zend_compile.h):

struct _zend_op_array {
    /* Common elements */
    zend_uchar type;
    zend_uchar arg_flags[3];
    uint32_t fn_flags;
    zend_string *function_name;
    zend_class_entry *scope;
    zend_function *prototype;
    uint32_t num_args;
    uint32_t required_num_args;
    zend_arg_info *arg_info;
    /* END of common elements */

    uint32_t *refcount;

    uint32_t this_var;

    uint32_t last;
    zend_op *opcodes;

    int last_var;
    uint32_t T;
    zend_string **vars;

    int last_brk_cont;
    int last_try_catch;
    zend_brk_cont_element *brk_cont_array;
    zend_try_catch_element *try_catch_array;

    /* static variables support */
    HashTable *static_variables;

    zend_string *filename;
    uint32_t line_start;
    uint32_t line_end;
    zend_string *doc_comment;
    uint32_t early_binding; /* the linked list of delayed declarations */
    int last_literal;
    zval *literals;
    int  cache_size;
    void **run_time_cache;
	void *reserved[ZEND_MAX_RESERVED_RESOURCES];
};

zend_op_array.opcodes保存了属于这个op_array的opcode数组,zend会根据将zend_op_array作为入参,通过下面的执行函数execute,逐条解释执行每条opcode, 从而实现我们PHP脚本想要的结果。

ZEND_API void execute(zend_op_array *op_array TSRMLS_DC)
{
	// ... 循环执⾏op_array中的opcode或者执⾏其他op_array中的opcode
}

总结一下,php脚本经过语法分析和词法分析,生产一条条op_line,zend将每条op_line编译成opcode,就产生大量opcode,这些opcode会通过一定形式保存在数组zend_op_array中, 最后以该数组为入参,通过Zend提供的方法execute(zend_op_array *op_array TSRMLS_DC)得到我们想要的结果。

参考1: http://www.php-internals.com/
参考2: http://www.laruence.com/2008/08/11/147.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值