2022-03-31

[php-src読書録]その1: CLI実行からスクリプト実行まで

php-srcのコードリーディングした内容をコツコツ残すテスト。

参考にした記事

デバッグするにあたって

コード読むだけで理解しきるのは難しいので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_exzend_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));
		}
// ...

「定数の」と書いたのは「変数」や「一時変数」バージョンのハンドラがあり、引数によってハンドラーを変更して処理を最適化しているっぽい

このエントリーをはてなブックマークに追加