AcidはPlan 9またはPlan9portで使えるデバッガです。おそらくAcidの用途で最も多いのは、suicide*1したプロセスをアタッチして、stk()
やlstk()
を使って落ちた様子をみてCtl-dで抜ける、ような使い方だと思いますが、Acidはこれ自体がシェルとCの中間みたいなプログラミング言語になっていて、やれることは非常に多いです。が、そこまで書くのは大変なので、今回の記事ではブレークポイントで止めて値を眺めるまでの基本的な使い方を書きました。
使い方
acidコマンドの引数にデバッグしたいファイルを渡すと、まだプロセスが作られていない状態でデバッガが起動します。acid:というプロンプトが表示されるので、引数が必要であればprogargsにスペース区切りで設定してプロセスを作成しましょう。
% acid /bin/git /bin/git: 386 plan 9 executable /sys/lib/acid/port /sys/lib/acid/386 // 引数を設定(必要なら) acid: progargs = "stash save Debugging" // プロセス生成 acid: new() 1415: system call _main SUBL $0xc,SP 1415: breakpoint main+0x3 CALL trace2_initialize_clock(SB) // プロセスの処理を開始 acid: cont()
acidはプロセスIDを与えると直接そのプロセスをアタッチします。この場合は、プロセスはすでに動いているためnew()
でプロセスを作成する必要はありません。
% acid <プロセスID> /bin/git: 386 plan 9 executable /sys/lib/acid/port /sys/lib/acid/386 // スタックトレース取得 acid: stk()
Acidの変数
変数はプログラムのシンボルテーブルで扱うものがそのまま参照できます*2。具体的に書いた方が分かりやすいと思うので、以下のプログラムを説明のために使います。
#include <u.h> #include <libc.h> typedef struct Addr Addr; struct Addr { char *host; int port; }; char *network = "tcp"; void main(void) { Addr a; a.host = "localhost"; a.port = 9000; print("%s!%s!%d\n", network, a.host, a.port); exits(nil); }
Acidはシンボルテーブルを参照します。このため、デバッグ対象プログラムの変数を参照するには、ローカル変数の場合はfunc:name
のように関数名と変数名を:で繋げてアクセスします。グローバル変数ならname
のように変数名だけでアクセス可能です。
acid: network 0x00006010
ただし、Acidから参照した変数はそのアドレスを表します。Cで言うと(void*)&network
のような扱いです。単項*
演算子を使うと、networkが指しているメモリの値を取り出すことができます。ソースコード上では*network
はC文字列へのポインタですが、AcidではCの型情報が失われているので、ただの整数です。
acid: *network 0x00006850
Acidでは、Cの型とは別に変数のフォーマットが存在します。現在変数が持っているフォーマットはwhatisで調べたり、fmtで変更したりできます。そして*
演算子がメモリから取り出す値のサイズはAcidの変数が持つフォーマットに依存します。例えばs(文字列)なら\0
まで読み込みますし、D(4バイト整数)なら4バイト読みます。
それではnetwork変数が指している文字列を表示してみましょう。
acid: whatis network integer variable format X acid: x = fmt(*network, 's') // *network=xが指すアドレスには文字列が格納されている(x自体はただの整数) acid: x = *network\s // fmtのショートハンド(*の方が結合強い) acid: whatis x integer variable format s acid: s = *x string variable acid: s tcp acid: *(*network\s) // これでもいい tcp
フォーマットを変更すれば、整数や浮動小数なども読み取れます。*
と似たような@
演算子もあるようです。利用可能なすべてのフォーマットはAcid Manualを参照してください。
構造体などのメンバ変数
構造体などの場合、Acidは型情報を持っていないのでメンバ変数を名前で参照できません。もちろん変数の先頭アドレスからオフセット分だけ加算すればアクセス可能ですが、アラインメントなどを考慮すると非常に面倒です。8c -a
オプションを使うと、Acidで利用できる型を生成してくれるので、これを使うといいでしょう。他のコンパイラではDWARFとして埋め込まれているような情報も、Plan 9では人が読み書きできるようなテキストファイルとして分けて扱います。
# -nオプションを省略するとstdoutへ出力 # -aaオプションの場合は.hファイルの内容を出力に含まない % 8c -an main.c
-a
オプションが生成したファイルには、以下のようにCの構造体や共用体と同じ名前の型や関数が含まれます。
sizeofAddr = 8; aggr Addr { 'X' 0 host; 'D' 4 port; }; defn Addr(addr) { complex Addr addr; print(" host ", addr.host\X, "\n"); print(" port ", addr.port, "\n"); };
これらのファイルは、acid -l
オプションやinclude(string)
などで読み込んで使います。
% acid -l ./main.acid 8.out // キャストする場合 acid: x = (Addr)main:a acid: x host 0x00006854 port 9000 acid: *(x.host) localhost // 関数を使う場合 acid: Addr(main:a) host 0x00006854 port 9000
ブレークポイント設定
ブレークポイントの設定はbpset(address)
やbpdel(address)
関数で行います。acidはシンボルテーブルを参照するので、address引数には関数名をそのまま渡してもいいし、filepc(where)
関数でファイル名と行番号から明示的に与えることもできます。また、現在設定中のブレークポイントが知りたければbptab()
関数が使えます。
acid: bpset(strbuf_vinsertf) acid: bpset(filepc("strbuf.c:262")) acid: bpset(filepc("strbuf.c:274")) acid: bptab() 0x00236588 strbuf_vinsertf SUBL $0x20,SP 0x002365ad strbuf_vinsertf+0x25 MOVL 0x8(BP),CX 0x002366b4 strbuf_vinsertf+0x12c MOVL 0x8(SI),CX acid: cont()
ブレーク中の操作
ブレークポイントで停止すると、プロセスはStopped*3状態に遷移して、acidが入力を受け付けるようになります。
ソースコードやレジスタの表示
停止している場所を調べるためにソースコードの参照ができます。
acid: src(addr) // addrを中心に10行Cのソースを表示 acid: line(addr) // addrの行だけCのソースを表示 acid: asm(addr) // addrから30行アセンブリのソースを表示
PCレジスタに現在のプログラムカウンタが設定されているので、これを使うのが便利でしょう。
acid: src(*PC) /tmp/x/git/strbuf.c:274 269 strbuf_grow(sb, len); 270 memmove(sb->buf + pos + len, sb->buf + pos, sb->len - pos); 271 /* vsnprintf() will append a NUL, overwriting one of our characters */ 272 save = sb->buf[pos + len]; 273 len2 = vsnprintf(sb->buf + pos, len + 1, fmt, ap); >274 sb->buf[pos + len] = save; 275 if (len2 != len) 276 BUG("your vsnprintf is broken (returns inconsistent lengths)"); 277 strbuf_setlen(sb, sb->len + len); 278 } 279
演算もできるので現在位置から少し前も見られます。
acid: asm(*PC-10) strbuf_vinsertf+0x122 0x002366aa MOVL sb+0x0(FP),SI strbuf_vinsertf+0x126 0x002366ae MOVL len+0x1c(SP),BP strbuf_vinsertf+0x12a 0x002366b2 MOVL AX,BX strbuf_vinsertf+0x12c 0x002366b4 MOVL 0x8(SI),CX strbuf_vinsertf+0x12d 0x002366b5 DECL SI strbuf_vinsertf+0x12e 0x002366b6 ORB CL,0xa048dea(CX) strbuf_vinsertf+0x134 0x002366bc ADDL pos+0x4(FP),AX strbuf_vinsertf+0x138 0x002366c0 MOVBSX save+0x17(SP),CX strbuf_vinsertf+0x13d 0x002366c5 MOVB CL,0x0(AX) ...
レジスタに保存されている値はregs()
などで表示できます。
acid: regs() PC 0x002366b4 strbuf_vinsertf+0x12c /tmp/x/git/strbuf.c:274 SP 0xdfffe884 ECODE 0xf01006d6 EFLAG 0x00000692 CS 0x00000023 DS 0x0000001b SS 0x0000001b GS 0x0000001b FS 0x0000001b ES 0x0000001b TRAP 0x00000003 breakpoint AX 0x0000000b BX 0x0000000b CX 0x0074571b DX 0x004ebd7c DI 0xffffffff SI 0xdfffeabc BP 0x00000016
他に、汎用レジスタや浮動小数点レジスタだけ表示する関数もありますが、通常はregs()
があれば十分でしょう。
スタックトレースの取得
stk()
またはlstk()
関数でスタックトレースを取得できます。lstk()
はstk()
と似ていますが、ローカル変数の値も含めて出力されます。
ステップ
cont()
またはstep()
関数で処理を再開できます。
acid: step() // 1命令を実行 acid: cont() // なんらかで停止するまで実行
マルチプロセス
デバッグ中のプログラムがfork(2)した場合、新しいプロセスはStopped状態になって停止します。そのプロセスをデバッグしたい場合は
% acid <新しいプロセスのPID>
でアタッチしてcont()
すればデバッグ可能ですし、処理を進めたいだけなら
% echo start >/proc/<新しいプロセスのPID>/ctl
で再開できます。プロセス操作の詳細はproc(3)を読んで下さい。
または、acid:プロンプトで待ち受けている状態であれば、以下の関数が使えるかもしれません。
acid: procs() // アクティブなプロセスリスト取得 acid: setproc(pid) // pidをカレントプロセスに切り替え acid: start(pid) // 停止しているpidを再開
ライブラリ
acidに-l library
オプションを与えると/sys/lib/acid以下*4のライブラリを読み込みます。特にtrussは、実際に発行されたシステムコールを確認できるので便利です。
% acid -l truss /bin/git acid: progargs = "xx" acid: new() acid: truss()