Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

Acidの使い方(基本)

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()

参考情報

*1:Unixで言うところのSEGV

*2:Acidの予約語と被った場合は名前の先頭に$を付ける

*3:suicideした場合はBroken

*4:p9pの場合は$PLAN9/acid