php-srcのコードリーディングした内容をコツコツ残すテスト。その5
今回はfunction
<?php
function hoge($i) {}
hoge(123);
opcode
$_main:
; (lines=4, args=0, vars=0, tmps=1)
; (before optimizer)
; /path/to/5.php:1-6
; return [] RANGE[0..0]
0000 INIT_FCALL 1 96 string("hoge")
0001 SEND_VAL int(123) 1
0002 DO_UCALL
0003 RETURN int(1)
hoge:
; (lines=2, args=1, vars=1, tmps=0)
; (before optimizer)
; /path/to/5.php:3-3
; return [] RANGE[0..0]
0000 CV0($i) = RECV 1
0001 RETURN null
functionのコンパイル
zend_compile_top_stmt() から zend_compile_func_decl() が呼ばれる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L9936
zend_compile_func_decl() では関数用のop_arrayを作成する
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L7172-L7292
zend_compile_func_decl() は zend_begin_func_decl() を呼び出し
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L7214:L7214
top_levelの場合は zend_hash_add_ptr() を呼び出し、 CG(function_table) のHashの中にop_arrayをセットしている。
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L7150:L7150
さらに zend_compile_params() を呼び出し関数のパラメータのコンパイルを行います
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L7256
パラメータはその関数のCVとしてコンパイルされる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L6618-L6619
opcodeは ZEND_RECV で
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L6673-L6675
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L6720:L6720
zend_compile_stmt() で関数内のステートメントをコンパイル
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L7268
呼び出し部分のコンパイル
ノードのtypeとしては ZEND_AST_CALL なので↓の部分を通る
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L10095
最終的に zend_compile_call() が呼ばれてここでコンパイルされる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L10234
zend_compile_call() では zend_hash_find_ptr() を使って CG(function_table) から zend_function を取得する
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L4458:L4458
ZEND_INIT_FCALL のopcodeを生成
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L4490-L4491
op1.num に zend_vm_calc_used_stack() の値をセットする
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L3710:L3710
最後に ZEND_DO_UCALL のopcodeがコンパイルされる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L3667:L3667
OPCODE
INIT_FCALLでは ZEND_INIT_FCALL_SPEC_CONST_HANDLER を呼び出します。
EX(run_time_cache) にキャッシュされていたらそれを使い、キャッシュされていなければ EG(function_table) からzend_functionを取得します。
fbc = CACHED_PTR(opline->result.num);
if (UNEXPECTED(fbc == NULL)) {
fname = (zval*)RT_CONSTANT(opline, opline->op2);
func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(fname));
if (UNEXPECTED(func == NULL)) {
ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));
}
fbc = Z_FUNC_P(func);
if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) {
init_func_run_time_cache(&fbc->op_array);
}
CACHE_PTR(opline->result.num, fbc);
}
その後、呼び出す関数のCall Frameをプッシュし、呼び出し元のcallをprev_execute_dataに格納します。
call = _zend_vm_stack_push_call_frame_ex(
opline->op1.num, ZEND_CALL_NESTED_FUNCTION,
fbc, opline->extended_value, NULL);
call->prev_execute_data = EX(call);
EX(call) = call;
| フィールド | 意味 |
|---|---|
| op1.num | 確保するスタックのサイズ |
| op2->constant | 関数名のliteral文字列 |
| result.num | cache_slot(多分キャッシュ用の何か) |
SEND_VAL のopcodeでは ZEND_SEND_VAL_SPEC_CONST_UNUSED_HANDLER が呼ばれる。
arg = ZEND_CALL_VAR(EX(call), opline->result.var);
value = RT_CONSTANT(opline, opline->op1);
ZVAL_COPY_VALUE(arg, value);
op1の変数の値をresult.varに入れる。ZEND_CALL_VAR の引数が EX(call) になっていることで、
呼び出し先の関数のコールスタックのところに変数を代入する処理になっている。
DO_UCALL のopcodeでは ZEND_DO_UCALL_SPEC_RETVAL_UNUSED_HANDLER が呼ばれる。
call->prev_execute_data = execute_data;
execute_data = call;
i_init_func_execute_data(&fbc->op_array, ret, 0 EXECUTE_DATA_CC);
呼び出し先のコールスタックの prev_execute_data に呼び出し元のコールスタックを紐付け、
execute_dataを呼び出し先のコールスタックに変更している。
型情報がない場合や引数が不足していない場合、RECVはスキップされる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_execute.c#L3915-L3926
ZEND_RECV_SPEC_UNUSED_HANDLER では呼び出し引数が少なくないかどうかや、型チェックをしている。
static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_RECV_SPEC_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
uint32_t arg_num = opline->op1.num;
zval *param;
if (UNEXPECTED(arg_num > EX_NUM_ARGS())) {
ZEND_VM_TAIL_CALL(zend_missing_arg_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));
}
param = EX_VAR(opline->result.var);
if (UNEXPECTED(!(opline->op2.num & (1u << Z_TYPE_P(param))))) {
ZEND_VM_TAIL_CALL(zend_verify_recv_arg_type_helper_SPEC(param ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_CC));
}
ZEND_VM_NEXT_OPCODE();
}
RETURNでは zend_leave_helper_SPEC が呼ばれ、以下の処理が実行される。
EG(current_execute_data) = EX(prev_execute_data);
EG(vm_stack_top) = (zval*)execute_data;
current_execute_dataを呼び出し元のCall Frameに戻し、vm_stack_topをexecute_dataに戻す
(設定前は execute_data + 関数のCall Frameのサイズ のアドレスを指している)
ちなみに return 文が入る場合は、i_init_func_execute_data() で EX(return_value) にreturn用の変数アドレスをセットし、
ZVAL_COPY_VALUE(return_value, retval_ptr); で呼び出し先からreturn用変数にセットしている。
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_execute.c#L3910:L3910