Windows 版 HSP スクリプトエディタに付随するデバッグウィンドウのかわりに GDB で HSP3DISH/CL でもデバッグ (ステップ実行等) する

Windows 版 HSP スクリプトエディタにはデバッグウィンドウという機能がついてきて、エディタ上でブレークポイントを張ったり、ステップ実行をしたりすることができます。 一方で、Linux で使用可能なスクリプトエディタ hsed はこの機能をサポートしません。 HSP3DISH/CL runtime on Linux で実行する HSP プログラムをデバッグする場合、また、 (#uselib,#func) を用いた外部ライブラリ呼び出し時のデバッグをする場合は GDB が使用できると便利です。 今回は GDB で HSP ランタイムを hook して、メモリ上に作成されるデバッグ情報領域を自分で確認しながら HSP プログラムをデバッグする方法を考えます。

“共通ランタイム” のソースコード (${OPENHSP}/src/hsp3/*) では現在ファイル・行や変数情報などを保持していそうなので、これを読み取ることで byte code (Intermediate Representation (IR), つまり .ax ファイル) とソースコードとの対応付けを取りながらデバッグすることができるはずです。 HSP (標準ランタイム) であろうが HSP3DISH/CL であろうが、また、Windows か Linux 版かに関わらず、デバッグ情報はメモリ上に作成されるようです。 まず、IR 展開・実行プログラム src/hsp3/hsp3code.cpp では実行の状態を global 領域に持ちます (PC を格納する static unsigned short *mcs や対応するソースファイルを格納する static int srcname など)。

/*------------------------------------------------------------*/
/*
		system data
*/
/*------------------------------------------------------------*/

static HSP3TYPEINFO *hsp3tinfo;	// HSP3 type info structure (strbuf)
static int tinfo_cur;			// Current type info ID
#define GetTypeInfoPtr( type ) (&hsp3tinfo[type])

static HSPCTX *hspctx;			// Current Context
static unsigned short *mcs;		// Current PC ptr
static unsigned short *mcsbak;
static int val,type,exflg;
static short csvalue, csvalue2;
static int hspevent_opt;		// Event enable flag
static MPModVarData modvar_init;
static int sptr_res;
static int arrayobj_flag;

static HSPEXINFO mem_exinfo;	// HSPEXINFO本体

#ifdef HSPDEBUG
static HSP3DEBUG dbginfo;
static int dbgmode;
#endif

PVal *plugin_pval;								// プラグインに渡される変数ポインタの実態
PVal *mpval;									// code_getで使用されたテンポラリ変数
static PVal *mpval_int;							// code_getで使用されたテンポラリ変数(int用)
static PVal prmvar;								// パラメーターテンポラリ変数の実態

static	unsigned char *mem_di_val;				// Debug VARS info ptr
static	int maxvar;								// Debug VARS id max
static	int srcname;
static	int funcres;							// 関数の戻り値型

基本的にはこれをデバッグ中に見てやればいいです。 つまり、命令が fetch されたタイミングでプログラムを pause し、その状況でデバッグ情報を print すれば実行の状態がわかるはずです。

Prerequisites

OpenHSP v3.7 (456fd2c8c20e8dd26eebfe1f00f1e6adeb64058b) で検証しました。 OpenHSP はデバッグシンボル付きでビルドしてください。Build OpenHSP via Docker Container も参考にしてください。

gdb は GNU gdb (Debian 16.3-1) 16.3 で検証しました。

今回の環境、Debian Trixie では、OpenHSP の依存である `libgpiod2` がパッケージマネージャから利用不可能なので (`libgpio3` は存在)、ビルドしました。

例を用いたデバッグ手順解説

以下の HSP プログラムをデバッグすることを考えます。 ランタイムは、dish と cl でデバッグ関連の部分は大きく違わないだろうという予想から、取り扱いが簡単な cl を使いました。

#include "hsp3cl.as"

goto *main

#deffunc show_otherthing int p1
    mes "Greetings: " + p1
    return

*show_something:
    mes "Hello"
    return

*main:
    mes "Beginning of the main"
    gosub *show_something
    show_otherthing 12345
    mes "End of the main"

src/hsp3/hsp3code.cpp:3152 にブレークポイントを張ります。これは、命令実行開始のエントリーポイントである code_execcmd 内の命令処理ループの最初の行です。 ここで continue すればほぼステップオーバーできます。 ほぼというのは、ソース行とバイトコードは対応していないため、continue 一回で必ずしもソースの一行分進むとは限りません。 call code_getdebug_name()call code_getdebug_line() とすれば現在行・ファイルを取得できます。

HSP コードの 10 行目 (*show_something 開始時点) にブレークポイントを張ることを想定してみます。conditional breakpoint を使って、プログラムカウンタを進める箇所、__code_next() 関数の 284 行目に、実行行・ファイルが 10 行目・“main.hsp” だったときに pause するようにします。 その結果、期待通りにプログラムが pause し、いくつかステップオーバーしてみると当該コマンド id が 15、つまり mes/print であり、これも期待する結果であることがわかります。

(gdb) b src/hsp3/hsp3code.cpp:284 if code_getdebug_line() == 10 && (int)strcmp(code_getdebug_name(), "main.hsp") == 0
Breakpoint 15 at 0x555555559fc0: file src/hsp3/hsp3code.cpp, line 284.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/user/Documents/OpenHSP/build/hsp3cl ./main.ax
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
gpiod initalize failed.
Beginning of the main

Breakpoint 15, __code_next () at src/hsp3/hsp3code.cpp:284
284             val = (int)(*mcs++);
(gdb) n
287     }
(gdb) 
code_next () at src/hsp3/hsp3code.cpp:293
293     }
(gdb) 
cmdfunc_extcmd (cmd=15) at src/hsp3/linux/hsp3gr_linux.cpp:221
221             switch( cmd ) {                                                 // サブコマンドごとの分岐
(gdb) p cmd
$13 = 15
今回 OpenHSP のコードはコンテナでビルドしたので、デバッグ中に gdb にソースを list させるには適切な `directory` コマンドの実行が必要です。 私の場合
directory ${HOME}/Documents/OpenHSP

としました。

今後の課題: HSP プログラムの変数の内容やスタックトレースを表示する

デバッグには、他の変数の状況やスタックトレースがわかると便利です。 しかしそれらはそう簡単ではなさそうでした。

他の変数の状況を見るためには char *code_dbgvarinf( char *target, int option ) が便利そうだったので、 gdb セッション中に無理やり呼び出してみましたが、グローバル領域で管理されている状態を適切に設定しないとうまく動作しなそうでした。

(gdb) set $dbgbuf = (char *)sbAlloc(0x4000)
(gdb) call code_dbgvarinf($dbgbuf, 0)

Program received signal SIGSEGV, Segmentation fault.
0x0000555555561d4b in code_dbgvarinf (target=0x5555555ca170 "", option=0) at src/hsp3/hsp3code.cpp:3985
3985            proc = HspVarCoreGetProc(pv->flag);
The program being debugged was signaled while in a function called from GDB.
GDB remains in the frame where the signal was received.
To change this behavior use "set unwind-on-signal on".
Evaluation of the expression containing the function
(code_dbgvarinf(char*, int)) will be abandoned.
When the function is done executing, GDB will silently stop.
(gdb) p pv
$16 = (PVal *) 0x0
(gdb) bt
#0  0x0000555555561d4b in code_dbgvarinf (target=0x5555555ca170 "", option=0) at src/hsp3/hsp3code.cpp:3985
#1  <function called from gdb>
#2  cmdfunc_extcmd (cmd=15) at src/hsp3/linux/hsp3gr_linux.cpp:221
#3  0x000055555555c836 in cmdfunc_gosub (subr=0x5555555c843c) at src/hsp3/hsp3code.cpp:1356
#4  0x000055555555e16f in cmdfunc_prog (cmd=1) at src/hsp3/hsp3code.cpp:2061
#5  0x00005555555605aa in code_execcmd () at src/hsp3/hsp3code.cpp:3155
#6  0x00005555555721e6 in hsp3cl_exec () at src/hsp3/linux/hsp3cl.cpp:304
#7  0x0000555555558af1 in main (argc=2, argv=0x7fffffffdba8) at src/hsp3/linux/main.cpp:82

src/hsp3/hsp3debug を再解釈して、デバッグ情報領域が適切な内容を持つようにする必要がありそうです.

または、一時的にやるなら、hspctx->mem_varPVal 型が入っていそうなので、これを解析してもいいかもしれません。 PVal に関しては【HSP】HSP用のプラグインHPIを作るまでの一通りの流れと寄り道 に少し解説があります。

変数やスタックトレースを確認するのに役立ちそうなメモ

まず、src/hsp3/hsp3debug.h にデバッグ用領域が定義されています。

typedef struct HSP3DEBUG
{
	//	[in/out] tranfer value
	//	(システムとの通信用)
	//
	int	flag;				// Flag ID
	int	line;				// 行番号情報
	char *fname;			// ファイル名情報
	void *dbgwin;			// Debug WindowのHandle
	char *dbgval;			// debug情報取得バッファ

	//	[in] system value
	//	(初期化後に設定されます)
	//
	struct HSPCTX 	*hspctx;
	//
	char *	(* get_value) (int);			// debug情報取得コールバック
	char *	(* get_varinf) (char *,int);	// 変数情報取得コールバック
	void	(* dbg_close) (char *);			// debug情報取得終了
	void	(* dbg_curinf)( void );			// 現在行・ファイル名の取得
	int		(* dbg_set) (int);				// debugモード設定
	char *  (* dbg_callstack) ( void );     // コールスタックの取得

} HSP3DEBUG;

これらはコンパイル時等にマクロ HSPDEBUG が defined であるようにしておけば (e.g., -DHSPDEBUG) HSP3DEBUG インスタンスが適切に初期化され、

#ifdef HSPDEBUG
static HSP3DEBUG dbginfo;
static int dbgmode;
#endif

...

#ifdef HSPDEBUG
	//		デバッグ情報の初期化
	//
	mem_di_val = NULL;
	dbgmode = HSPDEBUG_NONE;
	dbginfo.hspctx = hspctx;
	dbginfo.line = 0;
	dbginfo.fname = NULL;
	dbginfo.get_value = code_dbgvalue;
	dbginfo.get_varinf = code_dbgvarinf;
	dbginfo.dbg_close = code_dbgclose;
	dbginfo.dbg_curinf = code_dbgcurinf;
	dbginfo.dbg_set = code_dbgset;
	dbginfo.dbg_callstack = code_dbgcallstack;
#endif

命令実行時に適当に設定されます。

int code_execcmd( void )
{
	//		命令実行メイン
	//
	int i;
	hspctx->endcode = 0;

rerun:
	hspctx->looplev = 0;
	hspctx->sublev = 0;
	StackReset();

#ifdef HSPERR_HANDLE
	try {
#endif
#ifdef HSPEMSCRIPTEN
		{
#else
		while(1) {
#endif
			//Alertf( "#%d,%d line%d",type,val,code_getdebug_line() );
			//Alertf( "#%d,%d",type,val );
			//printf( "#%d,%d  line%d\n",type,val,code_getdebug_line() );
			//stack->Reset();
			//stack->StoreLevel();
			//stack->ResumeLevel();

#ifdef HSPDEBUG
			if ( dbgmode ) code_dbgtrace();					// トレースモード時の処理
#endif

実際にこれらに値を入れるには変数 dbgmode を適当に設定する必要がありますが、Windows 版スクリプトエディタのデバッグウィンドウがその設定をする処理をアタッチしているようでした。 これは特にデバッグウィンドウを生成する src/tools/win32/hsp3debug/hsp3dbgwin.cpp によって HSP3DEBUG->dbg_set がどこかから呼ばれることで達成されるようです。

今後の課題: GDB の組み込みコマンド (step, continue, etc.) で操作する

このままだと HSP コードそのものには break point を張れず、 命令をひとつひとつ step 実行するか、 breakpoint のようなことをするなら conditional break point や watch などを設定する必要があり煩雑です。 一方で、Python は pdb の他にも gdb でもデバッグができます。 特にマルチプロセスプログラミング等の複雑なデバッグをするときに pdb では力不足となってしまうのですが、 そういうときに gdb が使えます。 詳細な仕組みは未調査ですが、gdb にカスタムコマンドのようなものを定義できるようで、 gdb 内で

source /usr/share/gdb/auto-load/usr/bin/python3.13-dbg-gdb.py

というようなことをすると py-btpy-locals など Python 側の情報を取り扱えるコマンドが追加されるようです。 この仕組みを OpenHSP 用に応用すればより簡単にデバッグできるようになりそうです。

結論

OpenHSP を gdb でデバッグする方法を考え、 今回は step 実行ができるところまで確認しました。 ランタイムの関数 code_getdebug_name()code_getdebug_line() をうまく使うことで実質 HSP プログラム上にブレークポイントを張るような使い方ができました。 ただし、変数やスタックフレームを確認するにはもう少し調査が必要そうです。