PHP 7 Zend 虚拟机(ZVM)

370 阅读9分钟

⒈ OPCode

  OPCode 的底层数据结构:

struct _zend_op {
	const void *handler;
	znode_op op1;
	znode_op op2;
	znode_op result;
	uint32_t extended_value;
	uint32_t lineno;
	zend_uchar opcode; /*存储 opcode number,相应的 opcode number 定义在 /Zend/zend_vm_opcodes.h 中*/
	zend_uchar op1_type;
	zend_uchar op2_type;
	zend_uchar result_type;
};

  其中,handler 为 OPCode 实际执行的 C 语言函数。op1、op2、result 统称为操作数(op1 和 op2 为输入参数,result 为运行结果)。

  并不是所有的 OPCode 指令都会用到全部的操作数。例如,ZEND_ADD 会用到全部操作数,但 ZEND_BOOL_NOT 只会用到 op1 和 result,而 ECHO 则只会用到 op1。还有一些 OPCode 指令所用到的操作数的个数需要根据上下文确定,以 DO_FCALL 为例,如果被调用的函数有返回值,则会用到 result,否则不会用到 result。而对于所需要的输入参数多于两个的 OPCode 指令,则需要额外使用一个特定的 OPCode ZEND_OP_DATA 来存储其他的输入参数。

  extended_value 用于存储附加指令修饰符。以 ZEND_CAST 指令为例,extended_value 存储了转换的目标类型。

  OPCode 的每一个操作数都有其对应的类型,存储在 op1_type、op2_type、result_type 中。可能的类型有 IS_UNUSEDIS_CONSTIS_TMPVARIS_VARIS_CV。其中,IS_CONST 为常量类型;IS_UNUSED 有两种情况:要么确实是 unused,要么是 32 位的数值类型(ZEND_JMP 会将跳转的目标地址存储为这种类型);后三种为变量类型。

⒉ 变量类型

  ZVM 中的变量类型有三种:CV、VAR、TMPVAR。在 PHP 7 中,这三种变量类型在 ZVM 栈中的存储机制已经非常相近,但这三种变量类型各自所能存储的值却仍有很大的不同。

CV 类型:

  • CV 即编译变量,指的是 PHP 代码中实际定义的变量,例如 $a
  • CV 中存储的值可以是 UNDEF;在代码中使用 UNDEF 的变量会抛出 “undefined variable” 异常;一个函数中,出参数外,所有其他变量都会被初始化为 UNDEF
  • 所有 CV 类型的变量只有在退出当前作用域时才会被销毁

TMPVAR/VAR 类型:

  • 只有在 ZVM 内部使用,且通常用来存储 OPCode 中 result 的值
  • 在使用前先定义,所以不能存储 UNDEF
  • 用完之后立即销毁(在大多数情况下,TMPVAR/VAR 只有在其被使用的那条指令的作用域中有效)

TMPVAR 和 VAR 的区别:

在 PHP 5 中,TMPVAR 在 ZVM 的栈中分配,VAR 在堆中分配,所以 VAR 可以在多条 OPCode 之间重复使用

在 PHP 7 中,TMPVAR 和 VAR 都在 ZVM 的栈中分配,但只有 VAR 类型的变量可以存储引用类型。此外,VAR 还可以存储 zend_class_entry可变变量

⒊ OPArray

  PHP 脚本、PHP 中定义的函数(方法)、eval() 函数中的代码都会被编译成 OPArray。OPArray 对应的结构为 zend_op_array

struct _zend_op_array {
	/* Common elements */
	zend_uchar type;
	zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
	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;
	HashTable *attributes;
	/* END of common elements */

	int cache_size;     /* number of run_time_cache_slots * sizeof(void*) */
	int last_var;       /* number of CV variables */
	uint32_t T;         /* number of temporary variables */
	uint32_t last;      /* number of opcodes */

	zend_op *opcodes; /* OPCode 数组 */
	ZEND_MAP_PTR_DEF(void **, run_time_cache);
	ZEND_MAP_PTR_DEF(HashTable *, static_variables_ptr);
	HashTable *static_variables; /* 静态变量 */
	zend_string **vars; /* names of CV variables */

	uint32_t *refcount; /* 引用计数 */

	int last_live_range; /* live_range 的数量 */
	int last_try_catch; /* try catch 的数量 */
	zend_live_range *live_range; /* 各个变量的作用区间 */
	zend_try_catch_element *try_catch_array; /* try catch finally 相关信息 */

	zend_string *filename; /* 脚本名称 */
	uint32_t line_start;
	uint32_t line_end;
	zend_string *doc_comment; /* 注释信息 */

	int last_literal;
	zval *literals;

	void *reserved[ZEND_MAX_RESERVED_RESOURCES];
};

  literals 为字面量数组,last_literal 为字面量的个数。CONST 类型的操作数即是对 literals 中值的引用。CONST 操作数中要么存储 literals 中值的指针,要么存储字面量值相对于 literals 开始位置的偏移量。

⒋ 调用栈内存分布

  ZVM 栈中,每个页的大小为 256 KB,各个页之间通过链表连接。当发生函数调用时,会在 ZVM 栈中分配一个调用栈: 调用栈内存结构   调用栈开始部分为 zend_execute_data 结构,其后是一个数组,分别存放不同用途的变量。其中,包含在 CV 部分中的 ARG 部分为函数参数,而 extra_args 部分则用来存放通过 func_get_args() 获得的参数。

  CV 和 TMPVAR/VAR 类型的操作数,被编译为相对于调用栈开始位置的偏移量,所以要获取这些变量,只需要从调用栈的开始位置偏移一定的量即可。

  zend_execute_data 的数据结构定义如下:

struct _zend_execute_data {
    const zend_op       *opline;           /* executed opline                */
    zend_execute_data   *call;             /* current call                   */
    zval                *return_value;
    zend_function       *func;             /* executed function              */
    zval                 This;             /* this + call_info + num_args    */
    zend_class_entry    *called_scope;
    zend_execute_data   *prev_execute_data;
    zend_array          *symbol_table;
    void               **run_time_cache;   /* cache op_array->run_time_cache */
};

  opline 为当前执行的 OPCode,func 为当前调用的函数,return_value 为指向存储函数返回值的 zval 的指针,prev_execute_data 为当前函数调用完成后要返回的 zend_execute_data symbol_table 仅仅是为了以防出现使用可变变量的情况。

⒌ 函数调用

  在 PHP 函数执行的过程中,不同的函数对应的具体指令会有所不同,但所有函数的执行流程都是 INIT、SEND、DO。以 var_dump 为例

L0 (3):     ASSIGN CV0($a) int(0)
L1 (4):     ASSIGN CV1($b) int(1)
L2 (6):     INIT_FCALL 2 112 string("var_dump")
L3 (6):     SEND_VAR CV0($a) 1
L4 (6):     SEND_VAR CV1($b) 2
L5 (6):     DO_FCALL
L6 (7):     RETURN int(1)

  INIT 指令会在 ZVM 栈中创建一个新的调用栈,同时初始化 func、This、called_scope。这个新创建的调用栈会有足够的空间来存储函数参数以及在函数中用到的变量。新创建的调用栈的地址会存储在 execute_data->call 中,execute_data 即对应创建当前这个新调用栈的函数调用栈,亦即 EX(call)。新创建的调用栈中的 prev_execute_data 中存储的即为发起此次调用的 EX(call)。在此情况下,prev_execute_data 会组成一个未完成调用的链表,提供回溯功能。

  SEND 指令会将函数参数存入 EX(call) 中为其分配的内存空间中,但由于 PHP 中允许函数通过 func_get_args 的方式接收参数,所以参数实际在 EX(call) 中存储时,已申明的参数会存储在 EX(call) 中为 CV 类型变量分配的内存空间的开始位置,而未申明的变量则只能存储在 TMPVAR/VAR 类型变量以后的空间中。

  DO 指令用来执行实际的函数调用过程,此时 EX(call) 指向了新创建的调用栈。在函数调用过程中,如果出现了内部函数的调用,则只需要执行相应的处理程序即可;如果出现了用户级别的函数调用,则又需要初始化新的函数调用栈。

综上所述,每一次函数调用都只是在 ZVM 中创建一个调用栈,所以 PHP 代码中的递归在 ZVM 中并不会真正的进行递归调用,而仅仅是在函数的调用栈之间进行切换。所以,无限递归通常会产生内存溢出(Out Of Memory)的错误

⒍ 参数传递

  SEND_VALSEND_VAR 用于传递在编译时已知的采用值传递方式的参数。SEND_VAL 处理 CONST 和 TMPVAR 类型的操作数,SEND_VAR 处理 VAR 和 CV 类型的操作数。

  SEND_REF 用于传递在编译时已知的采用引用传递方式的参数。由于只有变量类型才可以采用引用传递的方式,所以 SEND_REF 只能处理 VAR 和 CV 类型的操作数。

  当无法确定是该用值传递还是引用传递的时候,需要使用 SEND_VAL_EXSEND_VAR_EX 来处理。它们会根据 arginfo 来检查参数的类型,然后采用适当的方式进行参数传递。但在大多数情况下,检查参数类型使用的是 zend_function 中的 arg_flags

union _zend_function {
        zend_uchar type;        /* MUST be the first element of this struct! */
        uint32_t   quick_arg_flags;

        struct {
                zend_uchar type;  /* never used */
                zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
                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;
        } common;

        zend_op_array op_array;
        zend_internal_function internal_function;
};

typedef struct _zend_arg_info {
        zend_string *name;
        zend_type type;
        zend_uchar pass_by_reference;
        zend_bool is_variadic;
} zend_arg_info;

  SEND_UNPACKSEND_ARRAY 分别用来处理参数解包和内联的 call_user_func_array 调用。它们都是将参数从数组中推入调用栈。但前者支持遍历而后者却不支持。当使用 SEND_UNPACK 时,有可能会导致已经分配好的调用栈溢出,此时需要移动栈顶指针以保证调用栈有足够的空间存放参数。而这个操作有可能会导致调用栈跨页(ZVM 中每个内存页的大小为 256 KB),此时就需要将整个调用栈移动到新分配的内存页中,因为目前 ZVM 无法处理跨页的调用栈。

  SEND_USER 用于处理内联的 call_user_func 调用。

⒎ 写操作与内存安全

  对于指针类型的变量,任何对指针所指向的目的地址的其他写操作都可能导致该变量无效。为了避免这种情况的出现,在指针变量的创建与销毁操作之间不允许其他用户级别的 PHP 代码执行。

$arr[a()][b()] = c();

L0 (2):     INIT_FCALL_BY_NAME 0 string("a")
L1 (2):     V1 = DO_FCALL_BY_NAME
L2 (2):     INIT_FCALL_BY_NAME 0 string("b")
L3 (2):     V3 = DO_FCALL_BY_NAME
L4 (2):     INIT_FCALL_BY_NAME 0 string("c")
L5 (2):     V5 = DO_FCALL_BY_NAME
L6 (2):     V2 = FETCH_DIM_W CV0($arr) V1
L7 (2):     ASSIGN_DIM V2 V3
L8 (2):     OP_DATA V5
L9 (4):     RETURN int(1)

  在以上 OPCode 中,a()、b()、c() 三个函数优先执行,之后创建用于写操作的指针型变量 V2。在 V2 创建完成之后一直到代码执行完成,再没有其他用户级别的 PHP 代码执行,保证了代码的安全性。

$arr[0] =& $arr[1];

L0 (2):     V2 = FETCH_DIM_W CV0($arr) int(1)
L1 (2):     V3 = MAKE_REF V2
L2 (2):     V1 = FETCH_DIM_W CV0($arr) int(0)
L3 (2):     ASSIGN_REF V1 V3
L4 (4):     RETURN int(1)

  与前例不同,本例中表达式两边都需要进行写操作。如果先对 arr[0]进行写操作,那么对arr[0] 进行写操作,那么对 arr[1] 的写操作会导致 arr[0]的值无效。所以在代码实际执行的过程中,首先对arr[0] 的值无效。所以在代码实际执行的过程中,首先对 arr[1] 进行写操作,然后取 arr[1]的地址,最后对arr[1] 的地址,最后对 arr[0] 进行写操作。

⒏ 异常处理

  几乎所有的 OPCode 都会直接或间接的产生异常。当有异常被写入 EG(exception) 时即产生一个新的异常。ZVM 在抛出异常之前会先将当前产生异常的 opline(zend_execute_data 中的 opline)存入 EG(opline_before_exception),然后再将当前的 opline 替换为 EG(exception_op)。

/* 部分代码省略 */

ZEND_API ZEND_COLD void zend_throw_exception_internal(zend_object *exception)
{
	/*... ...*/
	
	if (exception != NULL) {
		zend_object *previous = EG(exception);
		zend_exception_set_previous(exception, EG(exception));
		EG(exception) = exception;
		if (previous) {
			ZEND_ASSERT(is_handle_exception_set() && "HANDLE_EXCEPTION not set?");
			return;
		}
	}
	/*... ...*/
	
	EG(opline_before_exception) = EG(current_execute_data)->opline;
	EG(current_execute_data)->opline = EG(exception_op);
}

static void zend_init_exception_op(void)
{
	memset(EG(exception_op), 0, sizeof(EG(exception_op)));
	EG(exception_op)[0].opcode = ZEND_HANDLE_EXCEPTION;
	ZEND_VM_SET_OPCODE_HANDLER(EG(exception_op));
	EG(exception_op)[1].opcode = ZEND_HANDLE_EXCEPTION;
	ZEND_VM_SET_OPCODE_HANDLER(EG(exception_op)+1);
	EG(exception_op)[2].opcode = ZEND_HANDLE_EXCEPTION;
	ZEND_VM_SET_OPCODE_HANDLER(EG(exception_op)+2);
}

  之后 ZVM 会调用 ZEND_HANDLE_EXCEPTION 来处理异常抛出相关的工作。

static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_HANDLE_EXCEPTION_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
	const zend_op *throw_op = EG(opline_before_exception);
	uint32_t throw_op_num = throw_op - EX(func)->op_array.opcodes;
	int i, current_try_catch_offset = -1;

	if ((throw_op->opcode == ZEND_FREE || throw_op->opcode == ZEND_FE_FREE)
		&& throw_op->extended_value & ZEND_FREE_ON_RETURN) {
		/* exceptions thrown because of loop var destruction on return/break/...
		 * are logically thrown at the end of the foreach loop, so adjust the
		 * throw_op_num.
		 */
		const zend_live_range *range = find_live_range(
			&EX(func)->op_array, throw_op_num, throw_op->op1.var);
		throw_op_num = range->end;
	}

	/* Find the innermost try/catch/finally the exception was thrown in */
	for (i = 0; i < EX(func)->op_array.last_try_catch; i++) {
		zend_try_catch_element *try_catch = &EX(func)->op_array.try_catch_array[i];
		if (try_catch->try_op > throw_op_num) {
			/* further blocks will not be relevant... */
			break;
		}
		if (throw_op_num < try_catch->catch_op || throw_op_num < try_catch->finally_end) {
			current_try_catch_offset = i;
		}
	}

	cleanup_unfinished_calls(execute_data, throw_op_num);

	if (throw_op->result_type & (IS_VAR | IS_TMP_VAR)) {
		switch (throw_op->opcode) {
			case ZEND_ADD_ARRAY_ELEMENT:
			case ZEND_ADD_ARRAY_UNPACK:
			case ZEND_ROPE_INIT:
			case ZEND_ROPE_ADD:
				break; /* exception while building structures, live range handling will free those */

			case ZEND_FETCH_CLASS:
			case ZEND_DECLARE_ANON_CLASS:
				break; /* return value is zend_class_entry pointer */

			default:
				/* smart branch opcodes may not initialize result */
				if (!zend_is_smart_branch(throw_op)) {
					zval_ptr_dtor_nogc(EX_VAR(throw_op->result.var));
				}
		}
	}

	ZEND_VM_TAIL_CALL(zend_dispatch_try_catch_finally_helper_SPEC(current_try_catch_offset, throw_op_num ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_CC));
}

  在处理异常抛出的过程中,ZVM 首先会找到抛出异常的位置(即抛出异常的 OPCode 在 OPArray 中的偏移量 throw_op_num)。

  通常情况下,OPArray 中的临时变量在生成后紧接着即会被后续的指令使用然后销毁。但在一些特殊的情况下(例如循环语句),临时变量的生命周期会跨越几个 OPCode。在这种情况下,zend_op_array 中需要用到特殊的数据结构来记录某个特定变量的生命周期。

typedef struct _zend_live_range {
    uint32_t var; /* 临时变量 */
    uint32_t start; /* 临时变量开始起作用的 OPCode 的偏移量(不包括生成临时变量的 OPCode) */
    uint32_t end; /* 临时变量被最后使用的 OPCode 的偏移量 */
} zend_live_range;

  在循环控制语句中,return/break 语句会导致临时变量提前被销毁,在某些情况下,这可能会抛出异常

class Custom implements IteratorAggregate {
        public $p1 = 1;
        public $p2 = 2;
        public $p3 = 3;
        
        public function __construct() {
        
        }
        
        public function __destruct() {
                throw new \Exception();
        }
        
        public function getIterator() {
                return new ArrayIterator($this);
        }
}

$obj = new Custom();

foreach ($obj as $k) {
        try {
                echo 'return';
                return;
        } catch (\Exception $e) {
                echo 'catch';
                echo $e->getMessage();
        }
}

L0 (3):     DECLARE_CLASS string("custom")
L1 (21):    V3 = NEW 0 string("Custom")
L2 (21):    DO_FCALL
L3 (21):    ASSIGN CV0($obj) V3
L4 (23):    V6 = FE_RESET_R CV0($obj) L16
L5 (23):    FE_FETCH_R V6 CV1($k) L16
L6 (25):    ECHO string("return")
L7 (26):    FE_FREE V6
L8 (26):    RETURN null
L9 (26):    JMP L15
L10 (27):   CV2($e) = CATCH string("Exception")
L11 (28):   ECHO string("catch")
L12 (29):   INIT_METHOD_CALL 0 CV2($e) string("getMessage")
L13 (29):   V7 = DO_FCALL
L14 (29):   ECHO V7
L15 (23):   JMP L5
L16 (23):   FE_FREE V6
L17 (32):   RETURN int(1)

  在以上代码中,try/catch 中的 return 会导致 $k 被提前销毁(L7),从而导致 destruct 方法中抛出异常。但 L7 的 FE_FREE 是对 L16 的 FE_FREE 的拷贝。所以真正产生异常的是 L16 处的 OPCode,而 foreach 中的 catch 语句并不会被执行,所以运行代码实际会抛出 Uncaught Exception 的异常。

  为了处理这种情况下产生的异常,找到真正产生异常的 OPCode 的位置, ZEND_HANDLE_EXCEPTION 首先会找到产生异常的临时变量的 zend_live_rangezend_live_range 中的 end 即为实际产生异常的 OPCode 的位置。

static const zend_live_range *find_live_range(const zend_op_array *op_array, uint32_t op_num, uint32_t var_num)
{
        int i;
        for (i = 0; i < op_array->last_live_range; i++) {
                const zend_live_range *range = &op_array->live_range[i];
                if (op_num >= range->start && op_num < range->end
                                && var_num == (range->var & ~ZEND_LIVE_MASK)) {
                        return range;
                }
        }
        return NULL;
}

  在找到抛出异常的 OPCode 之后,ZVM 会判断抛出异常的代码是否在 try/catch 语句块中。为了达到这一目的,OPArray 中的 try_catch_elements 记录了 try/catch/finally 在 opcodes 中的位置。如果抛出异常的代码位于 try/catch 语句块中,则 ZVM 会找到该 try/catch 语句块的位置。

typedef struct _zend_try_catch_element {
        uint32_t try_op;
        uint32_t catch_op;  /* ketchup! */
        uint32_t finally_op;
        uint32_t finally_end;
} zend_try_catch_element;

  之后 ZVM 首先会清理在异常抛出之前开始但未完成的操作,包括释放调用栈以及相关的变量。

static void cleanup_unfinished_calls(zend_execute_data *execute_data, uint32_t op_num) /* {{{ */
{
	if (UNEXPECTED(EX(call))) {
		zend_execute_data *call = EX(call);
		zend_op *opline = EX(func)->op_array.opcodes + op_num;
		int level;
		int do_exit;

		if (UNEXPECTED(opline->opcode == ZEND_INIT_FCALL ||
			opline->opcode == ZEND_INIT_FCALL_BY_NAME ||
			opline->opcode == ZEND_INIT_NS_FCALL_BY_NAME ||
			opline->opcode == ZEND_INIT_DYNAMIC_CALL ||
			opline->opcode == ZEND_INIT_USER_CALL ||
			opline->opcode == ZEND_INIT_METHOD_CALL ||
			opline->opcode == ZEND_INIT_STATIC_METHOD_CALL ||
			opline->opcode == ZEND_NEW)) {
			ZEND_ASSERT(op_num);
			opline--;
		}

		do {
			/* If the exception was thrown during a function call there might be
			 * arguments pushed to the stack that have to be dtor'ed. */

			/* find the number of actually passed arguments */
			level = 0;
			do_exit = 0;
			do {
				switch (opline->opcode) {
					case ZEND_DO_FCALL:
					case ZEND_DO_ICALL:
					case ZEND_DO_UCALL:
					case ZEND_DO_FCALL_BY_NAME:
						level++;
						break;
					case ZEND_INIT_FCALL:
					case ZEND_INIT_FCALL_BY_NAME:
					case ZEND_INIT_NS_FCALL_BY_NAME:
					case ZEND_INIT_DYNAMIC_CALL:
					case ZEND_INIT_USER_CALL:
					case ZEND_INIT_METHOD_CALL:
					case ZEND_INIT_STATIC_METHOD_CALL:
					case ZEND_NEW:
						if (level == 0) {
							ZEND_CALL_NUM_ARGS(call) = 0;
							do_exit = 1;
						}
						level--;
						break;
					case ZEND_SEND_VAL:
					case ZEND_SEND_VAL_EX:
					case ZEND_SEND_VAR:
					case ZEND_SEND_VAR_EX:
					case ZEND_SEND_FUNC_ARG:
					case ZEND_SEND_REF:
					case ZEND_SEND_VAR_NO_REF:
					case ZEND_SEND_VAR_NO_REF_EX:
					case ZEND_SEND_USER:
						if (level == 0) {
							/* For named args, the number of arguments is up to date. */
							if (opline->op2_type != IS_CONST) {
								ZEND_CALL_NUM_ARGS(call) = opline->op2.num;
							}
							do_exit = 1;
						}
						break;
					case ZEND_SEND_ARRAY:
					case ZEND_SEND_UNPACK:
					case ZEND_CHECK_UNDEF_ARGS:
						if (level == 0) {
							do_exit = 1;
						}
						break;
				}
				if (!do_exit) {
					opline--;
				}
			} while (!do_exit);
			if (call->prev_execute_data) {
				/* skip current call region */
				level = 0;
				do_exit = 0;
				do {
					switch (opline->opcode) {
						case ZEND_DO_FCALL:
						case ZEND_DO_ICALL:
						case ZEND_DO_UCALL:
						case ZEND_DO_FCALL_BY_NAME:
							level++;
							break;
						case ZEND_INIT_FCALL:
						case ZEND_INIT_FCALL_BY_NAME:
						case ZEND_INIT_NS_FCALL_BY_NAME:
						case ZEND_INIT_DYNAMIC_CALL:
						case ZEND_INIT_USER_CALL:
						case ZEND_INIT_METHOD_CALL:
						case ZEND_INIT_STATIC_METHOD_CALL:
						case ZEND_NEW:
							if (level == 0) {
								do_exit = 1;
							}
							level--;
							break;
					}
					opline--;
				} while (!do_exit);
			}

			zend_vm_stack_free_args(EX(call)); // 释放函数参数

			if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) {
				OBJ_RELEASE(Z_OBJ(call->This)); // 释放对象
			}
			if (call->func->common.fn_flags & ZEND_ACC_CLOSURE) {
				zend_object_release(ZEND_CLOSURE_OBJECT(call->func));
			} else if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) {
				zend_string_release_ex(call->func->common.function_name, 0);
				zend_free_trampoline(call->func);
			}

			EX(call) = call->prev_execute_data;
			zend_vm_stack_free_call_frame(call); // 释放调用栈
			call = EX(call);
		} while (call);
	}
}

  在清理完未完成的操作之后,如果抛出异常的 OPCode 的 result 为 VAR/TMPVAR 类型,则需要清理 result。最后清理 try/catch 相关的信息。

static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL zend_dispatch_try_catch_finally_helper_SPEC(uint32_t try_catch_offset, uint32_t op_num ZEND_OPCODE_HANDLER_ARGS_DC)
{
	/* May be NULL during generator closing (only finally blocks are executed) */
	zend_object *ex = EG(exception);

	/* Walk try/catch/finally structures upwards, performing the necessary actions */
	while (try_catch_offset != (uint32_t) -1) {
		zend_try_catch_element *try_catch =
			&EX(func)->op_array.try_catch_array[try_catch_offset];

		if (op_num < try_catch->catch_op && ex) {
			/* Go to catch block */
			cleanup_live_vars(execute_data, op_num, try_catch->catch_op);
			ZEND_VM_JMP_EX(&EX(func)->op_array.opcodes[try_catch->catch_op], 0);

		} else if (op_num < try_catch->finally_op) {
			/* Go to finally block */
			zval *fast_call = EX_VAR(EX(func)->op_array.opcodes[try_catch->finally_end].op1.var);
			cleanup_live_vars(execute_data, op_num, try_catch->finally_op);
			Z_OBJ_P(fast_call) = EG(exception);
			EG(exception) = NULL;
			Z_OPLINE_NUM_P(fast_call) = (uint32_t)-1;
			ZEND_VM_JMP_EX(&EX(func)->op_array.opcodes[try_catch->finally_op], 0);

		} else if (op_num < try_catch->finally_end) {
			zval *fast_call = EX_VAR(EX(func)->op_array.opcodes[try_catch->finally_end].op1.var);

			/* cleanup incomplete RETURN statement */
			if (Z_OPLINE_NUM_P(fast_call) != (uint32_t)-1
			 && (EX(func)->op_array.opcodes[Z_OPLINE_NUM_P(fast_call)].op2_type & (IS_TMP_VAR | IS_VAR))) {
				zval *return_value = EX_VAR(EX(func)->op_array.opcodes[Z_OPLINE_NUM_P(fast_call)].op2.var);

				zval_ptr_dtor(return_value);
			}

			/* Chain potential exception from wrapping finally block */
			if (Z_OBJ_P(fast_call)) {
				if (ex) {
					zend_exception_set_previous(ex, Z_OBJ_P(fast_call));
				} else {
					EG(exception) = Z_OBJ_P(fast_call);
				}
				ex = Z_OBJ_P(fast_call);
			}
		}

		try_catch_offset--;
	}

	/* Uncaught exception */
	cleanup_live_vars(execute_data, op_num, 0);
	if (UNEXPECTED((EX_CALL_INFO() & ZEND_CALL_GENERATOR) != 0)) {
		zend_generator *generator = zend_get_running_generator(EXECUTE_DATA_C);
		zend_generator_close(generator, 1);
		ZEND_VM_RETURN();
	} else {
		/* We didn't execute RETURN, and have to initialize return_value */
		if (EX(return_value)) {
			ZVAL_UNDEF(EX(return_value));
		}
		ZEND_VM_TAIL_CALL(zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));
	}
}

static void cleanup_live_vars(zend_execute_data *execute_data, uint32_t op_num, uint32_t catch_op_num)
{
	int i;

	for (i = 0; i < EX(func)->op_array.last_live_range; i++) {
		const zend_live_range *range = &EX(func)->op_array.live_range[i];
		if (range->start > op_num) {
			/* further blocks will not be relevant... */
			break;
		} else if (op_num < range->end) {
			if (!catch_op_num || catch_op_num >= range->end) {
				uint32_t kind = range->var & ZEND_LIVE_MASK;
				uint32_t var_num = range->var & ~ZEND_LIVE_MASK;
				zval *var = EX_VAR(var_num);

				if (kind == ZEND_LIVE_TMPVAR) {
					zval_ptr_dtor_nogc(var);
				} else if (kind == ZEND_LIVE_NEW) {
					zend_object *obj;
					ZEND_ASSERT(Z_TYPE_P(var) == IS_OBJECT);
					obj = Z_OBJ_P(var);
					zend_object_store_ctor_failed(obj);
					OBJ_RELEASE(obj);
				} else if (kind == ZEND_LIVE_LOOP) {
					if (Z_TYPE_P(var) != IS_ARRAY && Z_FE_ITER_P(var) != (uint32_t)-1) {
						zend_hash_iterator_del(Z_FE_ITER_P(var));
					}
					zval_ptr_dtor_nogc(var);
				} else if (kind == ZEND_LIVE_ROPE) {
					zend_string **rope = (zend_string **)var;
					zend_op *last = EX(func)->op_array.opcodes + op_num;
					while ((last->opcode != ZEND_ROPE_ADD && last->opcode != ZEND_ROPE_INIT)
							|| last->result.var != var_num) {
						ZEND_ASSERT(last >= EX(func)->op_array.opcodes);
						last--;
					}
					if (last->opcode == ZEND_ROPE_INIT) {
						zend_string_release_ex(*rope, 0);
					} else {
						int j = last->extended_value;
						do {
							zend_string_release_ex(rope[j], 0);
						} while (j--);
					}
				} else if (kind == ZEND_LIVE_SILENCE) {
					/* restore previous error_reporting value */
					if (!EG(error_reporting) && Z_LVAL_P(var) != 0) {
						EG(error_reporting) = Z_LVAL_P(var);
					}
				}
			}
		}
	}
}

⒐ 异常处理中的 finally

  • 无论是否有异常抛出,finally 中的代码都会执行
  • 即使 try 或 catch 中有 return 语句,finally 中的代码仍会执行。只不过 return 语句的值在 finally 之前计算,但在 finally 中的代码执行完成之后返回
  • 如果 finally 中有 return 语句,则最终返回的结果为 finally 中的结果
  • PHP 中禁止直接跳入 finally 或直接从 finally 中跳出
static void zend_check_finally_breakout(zend_op_array *op_array, uint32_t op_num, uint32_t dst_num)
{
	int i;

	for (i = 0; i < op_array->last_try_catch; i++) {
		if ((op_num < op_array->try_catch_array[i].finally_op ||
					op_num >= op_array->try_catch_array[i].finally_end)
				&& (dst_num >= op_array->try_catch_array[i].finally_op &&
					 dst_num <= op_array->try_catch_array[i].finally_end)) {
			CG(in_compilation) = 1;
			CG(active_op_array) = op_array;
			CG(zend_lineno) = op_array->opcodes[op_num].lineno;
			zend_error_noreturn(E_COMPILE_ERROR, "jump into a finally block is disallowed");
		} else if ((op_num >= op_array->try_catch_array[i].finally_op
					&& op_num <= op_array->try_catch_array[i].finally_end)
				&& (dst_num > op_array->try_catch_array[i].finally_end
					|| dst_num < op_array->try_catch_array[i].finally_op)) {
			CG(in_compilation) = 1;
			CG(active_op_array) = op_array;
			CG(zend_lineno) = op_array->opcodes[op_num].lineno;
			zend_error_noreturn(E_COMPILE_ERROR, "jump out of a finally block is disallowed");
		}
	}
}
  •   finally 的底层实现涉及到两个 OPCode:FAST_CALLFAST_RET 。其中 FAST_CALL 用于进入 finally 语句块,FAST_RET 用于跳出 finally 语句块。
try {
    echo "try";
} finally {
    echo "finally";
}
echo "finished";

L0 (4):     ECHO string("try")
L1 (5):     T0 = FAST_CALL L3
L2 (5):     JMP L5
L3 (6):     ECHO string("finally")
L4 (6):     FAST_RET T0
L5 (8):     ECHO string("finished")
L6 (9):     RETURN int(1)

  在上例中,FAST_CALL 将当前位置存入 T0,同时跳转进入 finally 语句快(L3)。FAST_RET 跳出 finally 语句块,同时返回 T0 中存储的位置(实际为 T0 的下一行,即 L2)。

  在处理异常抛出时,异常抛出的位置不同,相应的处理逻辑也会发生变化:

  • 异常从 try 语句中抛出,并且有相应的 catch 语句捕获该异常,则程序会进入 catch 语句
  • 异常从 catch 语句中抛出,或从 try 语句中抛出但没有捕获该异常的 catch 语句,此时如果存在 finally 语句,则程序会进入 finally 语句,并且将抛出的异常存入 FAST_CALL 的临时变量中
  • 当有异常从 finally 语句中抛出时,如果此时 FAST_CALL 的临时变量中还有异常,那么临时变量中的异常和新产生的异常会形成一个链表,然后一起抛出
  • 上述抛出的异常会继续向上冒泡进入父级的 try/catch/finally 语句中
try {
    try {
        throw new Exception("try");
    } finally {}
} catch (Exception $e) {
    try {
        throw new Exception("catch");
    } finally {}
} finally {
    try {
        throw new Exception("finally");
    } finally {}
}

  当 finally 语句块中有 return 时,代码的执行会稍微发生变化

try {
    throw new Exception("try");
} finally {
    return 42;
}

L0 (3):     NOP
L1 (4):     V1 = NEW 1 string("Exception")
L2 (4):     SEND_VAL_EX string("try") 1
L3 (4):     DO_FCALL
L4 (4):     THROW V1
L5 (5):     T0 = FAST_CALL L8
L6 (5):     JMP L12
L7 (6):     DISCARD_EXCEPTION T0
L8 (6):    RETURN int(42)
L9 (6):    FAST_RET T0
L10 (8):    RETURN int(1)

  上例中,DISCARD_EXCEPTION 会忽略 try 语句中抛出的异常(L9),而 finally 中的 return 42 会被执行(L10),代码最终会返回 42。

try {
    $a = 42;
    return $a;
} finally {
    ++$a;
}

L0 (3):     NOP
L1 (4):     ASSIGN CV0($a) int(42)
L2 (5):     T3 = QM_ASSIGN CV0($a)
L3 (5):     T1 = FAST_CALL L9 T3
L4 (5):     RETURN T3
L5 (6):     T1 = FAST_CALL L9
L6 (6):     JMP L12
L7 (7):    PRE_INC CV0($a)
L8 (7):    FAST_RET T1
L9 (9):    RETURN int(1)

  上例中,在 FAST_CALL 进入 finally 之前,a的值会被存入T3L4)。在FASTCALL进入finally之后,a 的值会被存入 T3(L4)。在 `FAST_CALL` 进入 finally 之后,a 的值会自增(L10),然后 FAST_RET 会返回 T1 继续往下执行,最终代码返回的是 T3(L6),即 42,而不是 $a 自增以后的值 43。

  另外,FAST_CALL 在进入 finally 时,T3 的值也传给了 FAST_CALL ,存入了 T1。这样,如果 finally 中有新的异常抛出或有 return 语句导致 try 语句中的 return 被忽略,方便销毁误用的临时变量(T3)。

⒑ 生成器

  PHP 的生成器提供了一种简便、轻量的方式实现迭代

function gen($x) {
    foo(yield $x);
}

L0 (3):     EXT_NOP
L1 (3):     CV0($x) = RECV 1
L2 (3):     GENERATOR_CREATE
L3 (4):     EXT_STMT
L4 (4):     INIT_FCALL_BY_NAME 1 string("foo")
L5 (4):     V1 = YIELD CV0($x)
L6 (4):     SEND_VAR_NO_REF_EX V1 1
L7 (4):     DO_FCALL
L8 (5):     EXT_STMT
L9 (5):     GENERATOR_RETURN null

  在上例中,在 GENERATOR_CREATE (L2)之前,代码都正常执行。GENERATOR_CREATE 会生成一个 Generator 对象,同时在堆区为 execute_data 分配一块内存,然后将 ZVM 中正在运行的 execute_data 复制到这块内存中。

static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_GENERATOR_CREATE_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
        zval *return_value = EX(return_value);
        if (EXPECTED(return_value)) {
                USE_OPLINE
                zend_generator *generator;
                zend_execute_data *gen_execute_data;
                uint32_t num_args, used_stack, call_info;

                object_init_ex(return_value, zend_ce_generator);
                
                /*
                 * Normally the execute_data is allocated on the VM stack (because it does
                 * not actually do any allocation and thus is faster). For generators
                 * though this behavior would be suboptimal, because the (rather large)
                 * structure would have to be copied back and forth every time execution is
                 * suspended or resumed. That's why for generators the execution context
                 * is allocated on heap.
                 */
                num_args = EX_NUM_ARGS();
                if (EXPECTED(num_args <= EX(func)->op_array.num_args)) {
                        used_stack = (ZEND_CALL_FRAME_SLOT + EX(func)->op_array.last_var + EX(func)->op_array.T) * sizeof(zval);
                        gen_execute_data = (zend_execute_data*)emalloc(used_stack);
                        used_stack = (ZEND_CALL_FRAME_SLOT + EX(func)->op_array.last_var) * sizeof(zval);
                } else {
                        used_stack = (ZEND_CALL_FRAME_SLOT + num_args + EX(func)->op_array.last_var + EX(func)->op_array.T - EX(func)->op_array.num_args) * sizeof(zval);
                        gen_execute_data = (zend_execute_data*)emalloc(used_stack);
                }
                memcpy(gen_execute_data, execute_data, used_stack);
                
                /* Save execution context in generator object. */
                generator = (zend_generator *) Z_OBJ_P(EX(return_value));
                generator->execute_data = gen_execute_data;
                generator->frozen_call_stack = NULL;
                generator->execute_fake.opline = NULL;
                generator->execute_fake.func = NULL;
                generator->execute_fake.prev_execute_data = NULL;
                ZVAL_OBJ(&generator->execute_fake.This, (zend_object *) generator);
                /* ... ... */
        }
}

  当生成器再次恢复运行时,函数 foo 的调用栈已经在 ZVM 中分配好,此时如果继续执行生成器,那么 foo 函数的调用栈会被打断。为了解决这个问题,在 yield 时,foo 函数的调用栈会被复制到在堆区为生成器分配的内存中(zend_generator->frozen_call_stack),在 yield 之后再被加载到 ZVM 中。

struct _zend_generator {
	zend_object std;

	/* The suspended execution context. */
	zend_execute_data *execute_data;

	/* Frozen call stack for "yield" used in context of other calls */
	zend_execute_data *frozen_call_stack;

	/* Current value */
	zval value;
	/* Current key */
	zval key;
	/* Return value */
	zval retval;
	/* Variable to put sent value into */
	zval *send_target;
	/* Largest used integer key for auto-incrementing keys */
	zend_long largest_used_integer_key;

	/* Values specified by "yield from" to yield from this generator.
	 * This is only used for arrays or non-generator Traversables.
	 * This zval also uses the u2 structure in the same way as
	 * by-value foreach. */
	zval values;

	/* Node of waiting generators when multiple "yield from" expressions
	 * are nested. */
	zend_generator_node node;

	/* Fake execute_data for stacktraces */
	zend_execute_data execute_fake;

	/* ZEND_GENERATOR_* flags */
	zend_uchar flags;
};

11. 智能分支

  在 PHP 代码中,通常比较表达式的后面紧跟着条件跳转分支

$a = 1;
$b = 1;
if ($a == $b) {
    echo 'EQUAL';
} else {
    echo 'NO';
}

L0 (3):     ASSIGN CV0($a) int(1)
L1 (4):     ASSIGN CV1($b) int(1)
L2 (5):     T4 = IS_EQUAL CV0($a) CV1($b)
L3 (5):     JMPZ T4 L6
L4 (6):     ECHO string("EQUAL")
L5 (6):     JMP L7
L6 (8):     ECHO string("NO")
L7 (10):    RETURN int(1)

  在这种常见的代码结构中,参与比较运算的 OPCode(IS_EQUAL)会实现一种智能分支机制:如果紧接着下一条 OPCode 为 JMPZ 或 JMPNZ,那么参与比较运算的 OPCode 自身根据比较运算的结果实现跳转。

#define ZEND_VM_SMART_BRANCH_JMPZ(_result, _check) do { \
		if ((_check) && UNEXPECTED(EG(exception))) { \
			OPLINE = EX(opline); \
		} else if (_result) { \
			ZEND_VM_SET_NEXT_OPCODE(opline + 2); \
		} else { \
			ZEND_VM_SET_OPCODE(OP_JMP_ADDR(opline + 1, (opline+1)->op2)); \
		} \
		ZEND_VM_CONTINUE(); \
	} while (0)

#define ZEND_VM_SMART_BRANCH_JMPNZ(_result, _check) do { \
		if ((_check) && UNEXPECTED(EG(exception))) { \
			OPLINE = EX(opline); \
		} else if (!(_result)) { \
			ZEND_VM_SET_NEXT_OPCODE(opline + 2); \
		} else { \
			ZEND_VM_SET_OPCODE(OP_JMP_ADDR(opline + 1, (opline+1)->op2)); \
		} \
		ZEND_VM_CONTINUE(); \
	} while (0)
	
#define OP_JMP_ADDR(opline, node) \
    (node).jmp_addr

  其中,参数 _result 即为比较运算的结果(T4)。如果结果为 TRUE,则 opline + 2,直接从 L4 开始运行;如果结果为 FALSE,则 opline + 1 为 JMPZ,此时 JMPZ 的 op2 为 L6,代码从 L6 开始运行。

  需要特别注意的是,这种机制只检查进行比较运算的 OPCode 的下一条 OPCode 是否为 JMPZJMPNZ,并不关心比较运算的结果是否作为 JMPZJMPNZ 的操作数。所以,如果 JMPZJMPNZ 的操作数与比较运算的结果无关,这种情况需要特别留意。

($a == $b) + ($d ? $e : $f)

L0 (3):     T5 = IS_EQUAL CV0($a) CV1($b)
L1 (3):     NOP
L2 (3):     JMPZ CV2($d) L5
L3 (3):     T6 = QM_ASSIGN CV3($e)
L4 (3):     JMP L6
L5 (3):     T6 = QM_ASSIGN CV4($f)
L6 (3):     T7 = ADD T5 T6
L7 (3):     FREE T7
L8 (4):     RETURN int(1)

  上例中,在 JMPZ 之前添加了一条 OPCode(NOP),避免了智能分支机制的误用。