ブレークポイントの実装

ブレークポイントは,デバッギ(デバッグ対象)からデバッガへ処理を遷移させるための仕掛けである.基本的な動作の流れを書くと,まず,止めたい命令をメモリ上に保存しておいて,ブレークポイント命令(x86ならばINT 3)に置き換える.デバッギを実行すると,ブレークポイント命令を実行した時点で例外が発生するので,(カーネル経由で)デバッガのハンドラに処理が移る.ユーザがcont()などで実行の再開を要求すると,前に保存しておいた命令に書き戻し,デバッギの実行を再開する.

acidでこの辺をどう実装していくのか見ていこうと思う.gdbの実装がどうなっているのかわからないが,Cでインタプリタのコードをゴリゴリ書いてあるんだと思う.acidはわずかなプリミティブ以外は専用のスクリプト言語で書かれている.ブレークポイントの設定(bpset)や解除(bpdel)もスクリプト言語で書かれている.

bpset()の実装は次のようになっている.関数の定義はdefnから始まる.重要なのは"*fmt(addr,..."の行.bpfmtとbpinstはアーキテクチャ依存のグローバル変数で,前者がブレークポイント命令のサイズ,後者がブレークポイント命令(x86であれば,INT 3,つまり0xCC)そのものを示している.fmtプリミティブによってaddrからbpfmtサイズ分のメモリにbpinstを書き込んでいる.元々あった命令を保存してないが,これについては後で述べる.

defn bpset(addr)				// set a breakpoint
{
	if status(pid) != "Stopped" then {
		print("Waiting...\n");
		stop(pid);
	}
	if match(addr, bplist) >= 0 then
		print("breakpoint already set at ", fmt(addr, 'a'), "\n");
	else {
		*fmt(addr, bpfmt) = bpinst;
		bplist = append bplist, addr;
	}
}

次にstep()を見てみよう.最初のif文は,PCにブレークポイントが設定されていたら,ブレークポイントを取り除く処理である.実際に命令を書き戻している部分は,"*bput = @bput;"である.*オペレータはメモリ上に値を書き込むために利用されるのに対して,@オペレータは,メモリ上ではなく,ファイル上のテキストを参照するために使われる.このようになっているので,スクリプトを書く人は元の命令を保存しておく必要がない.

defn step()					// single step the process
{
	local lst, lpl, addr, bput;

	bput = 0;
	if match(*PC, bplist) >= 0 then {	// Sitting on a breakpoint
		bput = fmt(*PC, bpfmt);
		*bput = @bput;
	}

	lst = follow(*PC);

	lpl = lst;
	while lpl do {				// place break points
		*(head lpl) = bpinst;
		lpl = tail lpl;
	}

	startstop(pid);				// do the step

	while lst do {				// remove the breakpoints
		addr = fmt(head lst, bpfmt);
		*addr = @addr;
		lst = tail lst;
	}
	if bput != 0 then
		*bput = bpinst;
}