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);