php-srcのコードリーディングした内容をコツコツ残すテスト。その8
今日はforループ
<?php
for ($i = 0; $i < 2; $i++) {
echo $i;
}
opcodes
$_main:
; (lines=7, args=0, vars=1, tmps=3)
; (before optimizer)
; /path/to/8.php:1-6
; return [] RANGE[0..0]
0000 ASSIGN CV0($i) int(0)
0001 JMP 0004
0002 ECHO CV0($i)
0003 PRE_INC CV0($i)
0004 T3 = IS_SMALLER CV0($i) int(2)
0005 JMPNZ T3 0002
0006 RETURN int(1)
AST
zend_compile_for() でforがコンパイルされる。
zend_compile_expr_list() で $i = 0; の部分がコンパイルされる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L5258:L5258
zend_emit_jump() でJMPのopcodeが生成される。ジャンプ先は後で設定する。
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L5261
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L2138-L2144
opnum_jmpはopcodesのindex
zend_compile_stmt() でループ内のstmtのコンパイル
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L5266
zend_compile_expr_list() でループの加算処理 $i++ をコンパイル
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L5269
zend_update_jump_target_to_next() で最初のJMPオペコード opnum_jmp のジャンプ先を設定している
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L5272
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L2204
zend_compile_expr_list() で条件 $i < 2 をコンパイル
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L5273
zend_emit_cond_jump() で条件を満たした場合にstmtの最初に飛ぶようにJMPNZを入れる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L5276
pass_two() でゴニョる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_opcode.c#L1074-L1076
JMPやJMPNZに関してはopline_numにジャンプ先にインデックスがセットされているので、ジャンプ先〜ジャンプ元の差分を入れている
((char*)&(op_array)->opcodes[opline_num] - (char*)(opline))
opcode
ZEND_JMP_SPEC_HANDLER ではoplineを加算してジャンプする。
ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op1), 0);
今回はopline->op1 には 96=3 opcode分が入っていて、次はIS_SMALLERのopcodeが動く
ZEND_IS_SMALLER_SPEC_TMPVARCV_CONST_JMPNZ_HANDLER では条件に合致する場合 ZEND_VM_SMART_BRANCH_TRUE_JMPNZ() が呼ばれる。
中身はこれ↓
ZEND_VM_SET_OPCODE(OP_JMP_ADDR(opline + 1, (opline+1)->op2));
次のoplineからoplineのop2の分ジャンプすることになる。今回はJMPNZからop2.jmp_offset = -96 = 3 opcode分戻ることになり、最初のechoのopコードに到達する。
条件に合致しない場合は ZEND_VM_SMART_BRANCH_FALSE_JMPNZ() が呼ばれて中身はこれで、2 opcode分進んでループを抜ける。
ZEND_VM_SET_NEXT_OPCODE(opline + 2)
break/continue
breakはこのあたりでコンパイルされる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L5077-L5079
CG(context).current_brk_cont は zend_begin_loop() で作られたforのスタック
zend_begin_loop() で brk_cont_element というスタックを作っている
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L668-L670
zend_end_loop() で作ったスタックにcontinueとbreakのアドレスを入れる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L695-L696
continueは opnum_loop の値=ループ条件のopcodesのindex、
breakはループ終わりの次の命令(今回はJMPNZの次の命令)のopcodesのindexを示す。
例のごとく、これらは pass_two() でゴニョられる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_opcode.c#L1057-L1065
zend_get_brk_cont_target() でforループの brk_cont_array に入れたbrk, contのindexがopline_numにセットされる。
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_opcode.c#L658-L670
さらにZEND_JMP に変換され、opline_numは ZEND_PASS_TWO_UPDATE_JMP_TARGET() によって相対パスになる。
(BREAKやCONTINUEといったopcodeは存在せず JMP を使って実現している)
インクリメント
↓のオペコードもついでに調べた
PRE_INC CV0($i)
zend_compile_post_incdec() でコンパイルされる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L8827-L8830
え、post_incdecなのにpre_inc?ってなって調べてみたら zend_do_free() がやってた
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L719-L728
resultがどこにも使われていない場合はPREになって未使用フラグが立つっぽい
ZEND_PRE_INC_SPEC_CV_RETVAL_UNUSED_HANDLER がハンドラー。
fast_long_increment_function() を呼び出してインラインアセンブラで対応。
__asm__ goto(
"addq $1,(%0)\n\t"
"jo %l1\n"
:
: "r"(&op1->value)
: "cc", "memory"
: overflow);
return;
overflow: ZEND_ATTRIBUTE_COLD_LABEL
ZVAL_DOUBLE(op1, (double)ZEND_LONG_MAX + 1.0);