2022-04-09

Xdebugコードリーディング

PHPerKaigi 2022でXdebugのデバッガの話をするのですが、その調べ物ついでにXdebugのコードリーディングをしたのでその備忘録。

ちなみに発表資料はこちら

Xdebugのバージョンは3.1.3です。

Zend拡張の登録

xdebug_zend_startup() が初期設定です。xdebug_zend_startupは zend_startup_module(&xdebug_module_entry) を呼び出しています。

ZEND_DLEXPORT zend_extension zend_extension_entry = {
	(char*) XDEBUG_NAME,
	(char*) XDEBUG_VERSION,
	(char*) XDEBUG_AUTHOR,
	(char*) XDEBUG_URL_FAQ,
	(char*) XDEBUG_COPYRIGHT_SHORT,
	xdebug_zend_startup,
	xdebug_zend_shutdown,
	NULL,           /* activate_func_t */
	NULL,           /* deactivate_func_t */
	NULL,           /* message_handler_func_t */
	NULL,           /* op_array_handler_func_t */
	xdebug_statement_call, /* statement_handler_func_t */
	NULL,           /* fcall_begin_handler_func_t */
	NULL,           /* fcall_end_handler_func_t */
	xdebug_init_oparray,   /* op_array_ctor_func_t */
	NULL,           /* op_array_dtor_func_t */
	STANDARD_ZEND_EXTENSION_PROPERTIES
};

https://github.com/xdebug/xdebug/blob/3.1.3/xdebug.c#L806-L824

xdebug_module_entryはこんな感じです。

zend_module_entry xdebug_module_entry = {
	STANDARD_MODULE_HEADER,
	"xdebug",
	ext_functions,
	PHP_MINIT(xdebug),
	PHP_MSHUTDOWN(xdebug),
	PHP_RINIT(xdebug),
	PHP_RSHUTDOWN(xdebug),
	PHP_MINFO(xdebug),
	XDEBUG_VERSION,
	NO_MODULE_GLOBALS,
	ZEND_MODULE_POST_ZEND_DEACTIVATE_N(xdebug),
	STANDARD_MODULE_PROPERTIES_EX
};

https://github.com/xdebug/xdebug/blob/3.1.3/xdebug.c#L80-L93

zend_extension_entryxdebug_statement_call がstatement_handlerでPHPのステートメント実行ごとにフックする関数です。 これによってブレイクポイントを実現しています。こちらは後ほどご紹介します。

ちなみにstatement_handlerは常に呼ばれるものではなく compiler_options に値が設定されている場合呼ばれるものになります。
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L9959-L9961

ZEND_EXT_STMTのオペコードでstatement_handlerが呼ばれるところ
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_vm_def.h#L7600

zend_execute_ex

PHP_MINIT_FUNCTION(xdebug) がモジュールを読み込んだときに行われる初期設定です。 この関数内で xdebug_base_minit() が呼び出され、関数ポインタ zend_execute_ex の差し替えを行っています。

void xdebug_base_minit(INIT_FUNC_ARGS)
{
	// snip...
	xdebug_old_execute_ex = zend_execute_ex;
	zend_execute_ex = xdebug_execute_ex;

	xdebug_old_execute_internal = zend_execute_internal;
	zend_execute_internal = xdebug_execute_internal;

https://github.com/xdebug/xdebug/blob/3.1.3/src/base/base.c#L1132-L1136

zend_execute_ex は通常 execute_ex() というopcodeを実行する関数がセットされています。

zend_execute_ex は最初にスクリプトを実行するときに呼ばれます。 また、zend_execute_exが差し替えられた場合、関数実行のたびに呼ばれることになります。

例↓

if (EXPECTED(zend_execute_ex == execute_ex)) {
	LOAD_OPLINE_EX();


	ZEND_VM_ENTER_EX();
} else {
	SAVE_OPLINE_EX();

	execute_data = EX(prev_execute_data);
	LOAD_OPLINE();
	ZEND_ADD_CALL_FLAG(call, ZEND_CALL_TOP);
	zend_execute_ex(call);
}

https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_vm_def.h#L4118-L4130

xdebugはzend_execute_exを差し替えるので関数実行のたびに呼ばれることになり、関数実行前後で処理を挟むことによりDBGpのTCP接続、initパケットの送信、スタックの管理を行うことができます。

差し替える関数は xdebug_execute_ex() です。

stackが無い場合、つまり最初のzend_execute_exの呼び出しでTCP接続をinitパケットの送信をしています。 xdebug_execute_ex()xdebug_debug_init_if_requested_at_startup() を呼び出します。
https://github.com/xdebug/xdebug/blob/3.1.3/src/base/base.c#L698-L702

xdebug_debug_init_if_requested_at_startup()xdebug_init_debugger() を呼び出します。
https://github.com/xdebug/xdebug/blob/3.1.3/src/debugger/com.c#L679:L679

xdebug_init_debugger() ではまだTCP接続されていない場合は remote_init() を呼び出します。remote_initは関数ポインタで xdebug_dbgp_init() がセットされています。
https://github.com/xdebug/xdebug/blob/3.1.3/src/debugger/com.c#L497-L501

xdebug_dbgp_init() ではinitのXML文字列を生成して送信するなど、初期化処理を行っています。
https://github.com/xdebug/xdebug/blob/3.1.3/src/debugger/handler_dbgp.c#L2346:L2346

最後にコマンドループでIDEからコマンドが送信されるのを待ち、コマンドのハンドリングを行います
https://github.com/xdebug/xdebug/blob/3.1.3/src/debugger/handler_dbgp.c#L2443

コールスタックの管理も xdebug_execute_ex() で行っています。

pushするところ↓
https://github.com/xdebug/xdebug/blob/3.1.3/src/base/base.c#L721

popするところ↓
https://github.com/xdebug/xdebug/blob/3.1.3/src/base/base.c#L805-L807

zend_execute_ex でcall breakpointの処理もする

関数実行にフックするタイプのbreakpoint(call)も zend_execute_ex() 内でハンドリングされます。
https://github.com/xdebug/xdebug/blob/3.1.3/src/base/base.c#L772

xdebug_debugger_handle_breakpoints()handle_breakpoints() を呼び出します
https://github.com/xdebug/xdebug/blob/3.1.3/src/debugger/debugger.c#L481-L488

handle_breakpoints()XG_DBG(context).do_break などの設定を行います。
https://github.com/xdebug/xdebug/blob/3.1.3/src/debugger/debugger.c#L425-L479

do_break などのフラグはstatement_handlerでハンドリングされます。

statement_handler

IDEからstep_overのコマンドを送信すると以下の関数が実行され、 do_next do_step do_finish next_level などの値がセットされます。
https://github.com/xdebug/xdebug/blob/3.1.3/src/debugger/handler_dbgp.c#L1129-L1142

statement_handlerとして設定した xdebug_debugger_statement_call() では do_next が1の場合、関数ポインタ remote_breakpoint が実行されます。 https://github.com/xdebug/xdebug/blob/3.1.3/src/debugger/debugger.c#L280-L288

next_level はコールスタックの深さを表していて、step_overは同じ階層以上のstatementで止まり、step_intoは階層関係なく止まるように条件分岐しています。

remote_breakpointxdebug_dbgp_breakpoint() がセットされます。処理としてはXMLのレスポンスを返して、コマンドループという感じです。
https://github.com/xdebug/xdebug/blob/3.1.3/src/debugger/handler_dbgp.c#L2624:L2624

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