php-srcのコードリーディングした内容をコツコツ残すテスト。
参考にした記事
- PHP Internals Book
- PHPの内部のことが色々書いてある。
- PHPのビルド周りは、コード読みつつgdbで確認しつつ、という感じで理解を深めるために抑えておいたほうが良いかも。
- PHP による hello world 入門
- SAPIを含めたPHPの実行フローについてまとまっている記事
- mod_phpのapacheのハンドラ周りも書いてあって流石すぎる…
- PHP の関数実行とその計測(記事版)
- 関数実行の計測の切り口でZend Engineについて説明されている記事。コールスタック周りの理解にめっちゃ有用でした。超感謝。
- PHP 7 Virtual Machine
- nikic大先生によるZend Engineの解説。
- Implementing a Range Operator into PHP
- 実装して理解しようシリーズ(?)
- Lexer, Parser, ASTからopcodeへのコンパイル, VM実行と一通り処理を追えるのでコピペでも良いからやってみると良さそう。
- ちなみに実装したときの備忘録はこちら
- 本物のC
- C言語初心者には大変ありがたいサイト。マクロの定義や作法がわかってよき。
デバッグするにあたって
コード読むだけで理解しきるのは難しいのでgdbで実際の変数の中身を見ながらステップ実行していくと理解しやすい。 デバッグするにはsrcから
./buildconf
./configure --enable-opcache --enable-debug
make
という感じでビルドしてgdbを実行すればOK
gdb --args sapi/cli/php test.php
Macの場合は、brewでgdbをインストールできる
brew install gdb
このままだと利用できないので以下の記事を参考にコード署名して利用する
OS XでGDBを使う(ためにコード署名をする)
sudo codesign -s gdb-cert /usr/local/bin/gdb
C言語を読むための知識
特に読むのに苦労しているのがマクロ…。
マクロはプリプロセッサで文字列置換をするものっぽい。
例えば
dim = RT_CONSTANT(opline, opline->op2);
は
dim = ((zval*)(((char*)(opline)) + (int32_t)(opline->op2).constant))
という感じで置換される。
これぐらいだとまだ脳内で置換できるのだが
container = _get_zval_ptr_var(opline->op1.var EXECUTE_DATA_CC);
などCの構造無視したものが出てきて?となるがこれも置換され
container = _get_zval_ptr_var(opline->op1.var, execute_data);
と引数化したりする。マクロが出まくるのである程度脳にキャッシュしないとスムーズに読めないw
トークンの連結をする場合は ## で行う。これもなかなか読みづらい…。
#define ZEND_MAP_PTR(ptr) ptr ## __ptr
ちなみに do while(0)が入っていることがあるがマクロ展開で正常に動かすため1ステートメントにまとめるテクニックっぽい。
CLI実行からスクリプト実行まで雑にコードリーディング
本題のコードリーディング。雑に重要っぽいところを追っていく。
CLIのSAPIから追っていく。php_cli.cの main() 関数からスタート
https://github.com/php/php-src/blob/PHP-8.1.4/sapi/cli/php_cli.c#L1159
このあたりで do_cli() を呼び出し
https://github.com/php/php-src/blob/PHP-8.1.4/sapi/cli/php_cli.c#L1363-L1373
php_execute_script() を呼び出し
https://github.com/php/php-src/blob/PHP-8.1.4/sapi/cli/php_cli.c#L965:L965
zend_execute_scripts() を呼び出し
https://github.com/php/php-src/blob/PHP-8.1.4/main/main.c#L2538:L2538
prepend_file_p, primary_file, append_file_pの順にスクリプトを実行している。
これがauto_prepend_file, auto_append_fileが動く仕組み。
ここから先はスクリプトを実行する処理
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend.c#L1754-L1759
zend_compile_file() でコンパイルしてop_array(VMの命令列)を作って zend_execute() で実行している。
zend_compile_file は関数ポインタ。opcache無効でCLIの場合は compile_file() がセットされている。
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend.c#L915:L915
compile_file() は zend_compile() を呼び出す
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_language_scanner.l#L654
zendparse() して zend_compile_top_stmt() でコンパイルしてop_array作って pass_two() で調整したり
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_language_scanner.l#L599-L618
zendparse() がgrepしても見つからなかったが、bisonなコードでprefixに zend を入れているので生成される関数は zendparse() になるっぽい
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_language_parser.y#L45:L45
zendparse() には戻り値が無いが、呼び出すと内部で CG(ast) = $1 でグローバルなところに値を設定している。
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_language_parser.y#L295
zendparse() で設定された CG(ast) を引数に zend_compile_top_stmt() を呼び出している。
zend_compile_top_stmt() でASTに応じてコンパイルがされて、 CG(active_op_erray) の opcodes をセットしている
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L112-L128
zend_compile() から呼び出される pass_two() ではopcodesにハンドラーを設定したりvarの調整をしたりしてる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_opcode.c#L1136-L1149
zend_execute() はCall Frameをプッシュしてzend_execute_ex()を呼び出す
execute_data = zend_vm_stack_push_call_frame(call_info, (zend_function*)op_array, 0, object_or_called_scope);
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_vm_execute.h#L61598:L61598
zend_execute_ex も zend_compile_file と同様に関数ポインタで差し替え可能。差し替えるモチベーションはデバッガやプロファイルなどの用途。
CLIでは execute_ex() が呼び出される。
execute_ex() はopcodesを順に実行してハンドラーを実行していく感じになる。
例えば定数のechoはこういうハンドラーが実行される。
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ECHO_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
zval *z;
SAVE_OPLINE();
z = RT_CONSTANT(opline, opline->op1);
if (Z_TYPE_P(z) == IS_STRING) {
zend_string *str = Z_STR_P(z);
if (ZSTR_LEN(str) != 0) {
zend_write(ZSTR_VAL(str), ZSTR_LEN(str));
}
// ...
「定数の」と書いたのは「変数」や「一時変数」バージョンのハンドラがあり、引数によってハンドラーを変更して処理を最適化しているっぽい