2022-04-02

[php-src読書録]その3: 変数アサイン

php-srcのコードリーディングした内容をコツコツ残すテスト。その3

今回見ていくスクリプトはこちら

<?php

$i = "hoge";

変数アサイン

opcode

$_main:
; (lines=2, args=0, vars=1, tmps=1)
; (before optimizer)
; /path/to/3.php:1-4
; return  [] RANGE[0..0]
0000 ASSIGN CV0($i) string("hoge")
0001 RETURN int(1)

gdbでhandlerをデバッグ

(gdb) p execute_data->opline->handler
$1 = (const void *) 0x100517cb0 <ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER>

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();
	value = RT_CONSTANT(opline, opline->op2);
	variable_ptr = EX_VAR(opline->op1.var);

	value = zend_assign_to_variable(variable_ptr, value, IS_CONST, EX_USES_STRICT_TYPES());
	if (UNEXPECTED(0)) {
		ZVAL_COPY(EX_VAR(opline->result.var), value);
	}

	/* zend_assign_to_variable() always takes care of op2, never free it! */

	ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
}

RT_CONSTANTは定数のzvalを取得している。後述。

EX_VAR() は変数のzvalのポインタを取得する。

#define EX_VAR(n)	ZEND_CALL_VAR(execute_data, n)
#define ZEND_CALL_VAR(call, n) \
	((zval*)(((char*)(call)) + ((int)(n))))

opline->op1.var はexecute_dataのアドレスからの差分を示している。 このあたりは、Call Frameの話も絡みそうなので後述。

zend_assign_to_variable()zend_copy_to_variable() を呼び出す
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_execute.h#L169:L169

zend_copy_to_variable()ZEND_COPY_VALUE で変数領域に値をコピーしている。
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_execute.h#L121:L121

各マクロはこんな感じで定義されている

#define ZVAL_COPY_VALUE(z, v)							\
	do {												\
		zval *_z1 = (z);								\
		const zval *_z2 = (v);							\
		zend_refcounted *_gc = Z_COUNTED_P(_z2);		\
		uint32_t _t = Z_TYPE_INFO_P(_z2);				\
		ZVAL_COPY_VALUE_EX(_z1, _z2, _gc, _t);			\
	} while (0)
#define Z_COUNTED_P(zval_p)			Z_COUNTED(*(zval_p))
#define Z_TYPE_INFO_P(zval_p)		Z_TYPE_INFO(*(zval_p))
# define ZVAL_COPY_VALUE_EX(z, v, gc, t)				\
	do {												\
		Z_COUNTED_P(z) = gc;							\
		Z_TYPE_INFO_P(z) = t;							\
	} while (0)
#define Z_COUNTED(zval)				(zval).value.counted
#define Z_TYPE_INFO(zval)			(zval).u1.type_info

諸々マクロを展開すると

(*variable_ptr).value.counted = (*value).value.counted
(*variable_ptr).u1.type_info = (*value).u1.type_info

あれ、value全体じゃなくてcountedだけしかコピーしてないじゃん。 と思いましたが、valueは共用体なのでcountedを更新すると (*variable_ptr).value の中身の参照先が (*value).value の値を見ることになります。 同じ疑問を持った人がいてStackoverflowにかかれていた↓
why the marco “ZVAL_COPY_VALUE(z,v)” seems to work unexpectly in PHP Internal?

Call Frame

変数の定義場所はこんな感じでexecute_dataにvarを足したところにある。

(zval*)((char*)(execute_data) + (int)(opline->op1.var))

execute_dataは zend_execute_data+変数領域 の先頭を示すポインタなので 関数の引数が無く1番目の変数を取得する場合、sizeof(zend_execute_data) がvarに入ることになる。

execute_datazend_vm_stack_push_call_frame() の戻り値が入る。

execute_data = zend_vm_stack_push_call_frame(call_info,
	(zend_function*)op_array, 0, object_or_called_scope);

zend_vm_stack_push_call_frame()zend_vm_calc_used_stack() を呼び出し

static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame(uint32_t call_info, zend_function *func, uint32_t num_args, void *object_or_called_scope)
{
	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, object_or_called_scope);
}

zend_vm_calc_used_stack() の処理はこんな感じ
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_execute.h#L245-L253

ZEND_CALL_FRAME_SLOT はざっくりいうと zend_execute_ex のサイズ(正確にはアラインメント考慮して sizeof(zval)で除算したサイズ?)

#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))))
#define ZEND_MM_ALIGNED_SIZE(size)	(((size) + ZEND_MM_ALIGNMENT - 1) & ZEND_MM_ALIGNMENT_MASK)

used_stackでスタックに確保する領域のサイズを取得。

zend_vm_stack_push_call_frame_ex() で領域確保。すでにメモリ確保済みの場合は、vm_stack_topを移動するだけ。
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_execute.h#L239:L239

vm_stack vm_stack_top vm_stack_end はこのあたりで初期化されている。
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_execute.c#L187:L187

ちなみにこのコールスタックの一番最初の部分にはヘッダが入っている
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_execute.h#L212:L212

#define ZEND_VM_STACK_HEADER_SLOTS \
	((ZEND_MM_ALIGNED_SIZE(sizeof(struct _zend_vm_stack)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval)) - 1) / ZEND_MM_ALIGNED_SIZE(sizeof(zval)))

#define ZEND_VM_STACK_ELEMENTS(stack) \
	(((zval*)(stack)) + ZEND_VM_STACK_HEADER_SLOTS)

AST側

zend_compile_stmt()zend_compile_expr() を呼び出し
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L10057

zend_compile_expr()zend_compile_expr_inner() を呼び出し
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L10215

zend_compile_expr_inner()zend_compile_assign() を呼び出し
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L10098

zend_compile_assign() はこの辺で定義されている
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L3209-L3214

値(右側のノード)は zend_compile_expr() で再帰的にコンパイルされる。

今回は定数なので zend_compile_expr_inner_() が呼び出され op2->u.constant に定数のzvalが代入される
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L10079-L10082

変数(左側のノード)は zend_delayed_compile_var() を呼び出してコンパイルされる。 zend_delayed_compile_var()zend_compile_simple_var() を呼び出し
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L10269

zend_compile_simple_var()zend_try_compile_cv() を呼び出す
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L2773

CVというのはCompiled Variableのことでユーザーが定義した変数のこと。

zend_try_compile_cv() で変数のopcodesを設定する感じになる。 zend_try_compile_cv()lookup_cv() を呼び出す。
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L2677

lookup_cv() はすでに op_array->vars に存在すればそのインデックスを返し、存在しなければvarsに追加する
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L487-L508

インデックスというのはCall Frameからのポインタ的な距離。実行時はexecute_dataにこの値を加算して変数の領域を参照している。

#define EX_NUM_TO_VAR(n)	((uint32_t)(((n) + ZEND_CALL_FRAME_SLOT) * sizeof(zval)))
#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_emit_op_tmp() はノードからopcodeを作る処理
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L2095-L2113

SET_NODEでノードからoplineに設定している。

#define SET_NODE(target, src) do { \
		target ## _type = (src)->op_type; \
		if ((src)->op_type == IS_CONST) { \
			target.constant = zend_add_literal(&(src)->u.constant); \
		} else { \
			target = (src)->u.op; \
		} \
	} while (0)

マクロを展開するとこんな感じ

opline->op1_type = op1->op_type;
if ((src)->op_type == IS_CONST) {
	opline->op1.constant = zend_add_literal(&(op1)->u.constant);
} else {
	opline->op1 = op1->u.op;
}

変数の場合はop->u.opがoplineにセットされる。 定数の場合は zend_add_literal() を呼び出して戻り値をセットする。

zend_add_literal()zend_insert_literal() を呼び出し op_array->last_literal を返す。
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L547

zend_insert_literal() でliteralsの配列要素にセットしている
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L524-L528

zend_emit_op_tmp() した段階ではliteralsのインデックスを示しているが、 pass_two() の処理で変換を入れている。
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_opcode.c#L1142:L1142

ZEND_PASS_TWO_UPDATE_CONSTANTでoplineから定数参照先のポインタの距離に変換している。

# define CT_CONSTANT_EX(op_array, num) \
	((op_array)->literals + (num))
# define ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline, node) do { \
		(node).constant = \
			(((char*)CT_CONSTANT_EX(op_array, (node).constant)) - \
			((char*)opline)); \
	} while (0)

これはopcodesの後ろにliteralsが入るように配置しているためっぽい
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_opcode.c#L1019-L1027

参照するときはRT_CONSTANTのマクロを使う。展開するとこんな感じでoplineにポインタを加算している。

((zval*)(((char*)(opline)) + (int32_t)(opline->op2).constant))
このエントリーをはてなブックマークに追加