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