php-srcのコードリーディングした内容をコツコツ残すテスト。その4
デバッグの効率化
今までgdbを使っていたんだけどTUIモードというのがあるのを初めて知った。
gdb -tui
で実行するとTUIモードでの実行になる。
gdb
を実行してから C-x C-a
でもOK。
tuiモードを使わないとステップ実行のたびに list
を実行するなどしてソースコードを表示する必要があって大変…
そして、それよりもVSCodeのGUIデバッガの方がはるかに楽だった。
C/C++ のプラグインをインストールして launch.json
をこんな感じで設定してデバッグ実行すればOK
{
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) Launch",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/sapi/cli/php",
"args": ["test.php"],
"stopAtEntry": true,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": true,
"MIMode": "gdb",
"miDebuggerPath": "/usr/local/bin/gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}
ADDとTMPVAR
今回はこちらのコードを追っていく
<?php
$i = 1;
$i = 1 + $i;
opcode
$_main:
; (lines=4, args=0, vars=1, tmps=3)
; (before optimizer)
; /path/to/4.php:1-5
; return [] RANGE[0..0]
0000 ASSIGN CV0($i) int(1)
0001 T2 = ADD int(1) CV0($i)
0002 ASSIGN CV0($i) T2
0003 RETURN int(1)
ASSIGNは前回説明済みなので割愛。
ADDは今回のケースでは ZEND_ADD_SPEC_CONST_TMPVARCV_HANDLER
が呼ばれる。
ハンドラの命名としては、第1オペランドがCONST => 定数、第2オペランドがTMPVARCV => 変数のときに呼ばれるハンドラという意味。
今回のケースの場合、以下の処理が実行される
result = EX_VAR(opline->result.var);
fast_long_add_function(result, op1, op2);
ZEND_VM_NEXT_OPCODE();
fast_long_add_functionの中はインラインアセンブリで記述されている
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_operators.h#L634-L650
- movqでraxレジストリにop1->valueの値をコピー
- addqでraxレジストリにop2->valueの値を加算
- joでオーバフローがある場合はoverflowラベルにジャンプ
- movqでresult->valueにraxレジストリの値をコピー
- IS_LONGの値をresult->value.type_infoにコピー
zend_compile_expr_inner()
は zend_compile_binary_op()
を呼び出す
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L10113
zend_compile_binary_op()
はleft_node, right_nodeのコンパイルを行う
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L8578-L8579
その後、 zend_emit_op_tmp()
を呼び出す
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L8656
TMPVARは内部的な一時変数を意味し、以下の3つが作成される
- $i = 1 の評価値: ASSIGN
- $i + 1の評価値: BINARY_OP
- $i = (2の値)の評価値: ASSIGN
opcodesをdumpしたときのtmps=3がTMPVARの数になる。ちなみにvarsはユーザーが定義した変数(CV)の数、argsは関数の引数の数。
(lines=4, args=0, vars=1, tmps=3)
評価値の変数設定は zend_emit_op_tmp()
の zend_make_tmp_result()
関数で実行される
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L2109
zend_make_tmp_result()
はこんな感じ
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L2067-L2072
typeに IS_TMP_VAR
を設定しつつ、 get_temporary_variable()
で result.var
にインデックスをセットする。
get_temporary_variable()
は compiler_globals.active_op_array->T
というTMPVarの数を管理する変数をインクリメントして返す。
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L483
ここではインデックスをセットしておいて pass_two()
でexecute_dataからのオフセットに変換している
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_opcode.c#L1147:L1147
op_array->last_var
を加算しているのはスタックフレームは
zend_execute_data => CVの領域 => TMPVARの領域
という感じで並ぶので、CVの領域サイズ+TMPVARのインデックスがTMPVARの変数領域になるため。
- $i = 1 の評価値: ASSIGN
- $i + 1の評価値: BINARY_OP
- $i = (2の値)の評価値: ASSIGN
sizeof(zend_execute_data)が80で関数の引数ゼロ、CV1つ、一時変数3つなので、オフセットは以下のようになる
execute_dataからのオフセット | 変数 |
---|---|
80 | $i |
96 | TMPVAR1 |
112 | TMPVAR2 |
各処理のoplineをデバッグするとこんな感じ
処理 | op1.var | op2.var | result.var |
---|---|---|---|
$i = 1 | 80: CV0 | 128: literals[0] | 0 |
T1 = 1 + $i | 112: literals[1] | 80: CV0 | 112: T1 |
$i = T1 | 80: CV0 | 112: T1 | 2 |
RETURN | 64 | 0 | 0 |
result.varで $i = 1
と $i = TMP1
で result.var
がインデックスからオフセットに変換されていないのは未使用だから。
↓ここでstmtなexprに関しては未使用になる。(pass_twoでは IS_UNUSED
ではオフセット変換されない)
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L743:L743
評価値を利用する場合と未使用な場合でハンドラが違うので、未使用な場合はresult.varの変換がそもそも不要、という感じっぽい。
ちなみに、2行目でliterals[1]のオフセットが112なのは、opcodesのサイズが128(zend_op x 4), zend_opのサイズが32, zvalのサイズが16なのでoplineからの距離が 128 + 16 - 32 = 112
となるため