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_data
は zend_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))