DWARF の行情報を読む

なんか行情報読むとかそいう話題が twitter にあったので、適当に作ってあったのでなんか書いてみる。

http://github.com/shinh/test/blob/master/addr2line.cc

DWARF 情報つけると (今の GCC だと -g でつくと思う) アドレスからコードの名前を行番号をひけるようになるんだけど、まぁその情報をプログラムから読むという話。

まずどこに格納されてるかっていうと、 .debug_line っていうセクションに入っているので、それを探す必要がある。上のプログラムで言うと main の中。先頭にある ELF ヘッダを読んでセクションヘッダの開始位置とセクション名の格納されてる .shstrtab の位置を教えてもらって、 .shstrtab の情報を参考にして .debug_info の位置を特定する。

.debug_line の中身はコンパイルユニット(要は一つの .cpp とか .c)ごとにヘッダと中身が入ってる感じで、ヘッダには全長やらファイル名やらなんやらが入っています。ファイル名は同じディレクトリ名複数回出た時に短くなるように、まずディレクトリ名を列挙して、その情報を使いつつ格納されてる。

中身の方はちょっとした VM みたいなものになっていて、ファイル名を示すレジスタ、行番号を示すレジスタ、アドレスを示すレジスタ、それともろもろがある感じ。オプコードの方はレジスタ群に値をセットするような命令群と、あとは行番号やアドレスに加算減算をする命令。ループとかは無い。

なんでこうなってるかというと、行情報ってのは連続した行番号やらアドレスが多いことが予想されて、基本的に差分は小さい値になるので、移動距離が短い時はなるべく短くエンコードできるように工夫してあるから。そのへんの数値は基本的に不定長で 7bit ずつエンコードするアレになってるので、差分で保持することによって、短い距離の移動は短くなる。

たぶん一番面白いのは special opcode と呼ばれてるもので、予約されてない opcode が出てくると全部この special opcode として扱われる。で、 opcode の数値自体を使って、

 a = op - opcode_base;
 uint addr_incr = (a / line_range) * minimum_instruction_length;
 int line_incr = line_base + (a % line_range);

という式で行番号とアドレス両方の移動量を計算する。 line_range と line_base と minimum_instruction_length はヘッダで指定できて、たぶん GCC は固定で line_range=14, line_base=-5 としてる気がする。 minimum_instruction_length は x86 だと 1 バイト命令があるのでほぼ確実に 1 。 opcode_base はここから先は special opcode でそれまでの opcode は予約されてるヤツですよーという値。今の DWARF3 では 13 。

a は opcode_base が 13 なので 0 から 243 (=256-13) までの値を取る。 addr_incr は 0 から 17 (=243/14) で line_incr は -5 から 8 (=-5+13)。この程度の値が値域にあるなら、まぁコメントが大量にあるとか無ければ、多くのケースで一つの行ごとに 1byte の領域で行情報を格納できて、おおむね効率的、という。

で、ということはコメントがあるか無いかでバイナリサイズって変わるということですね。下記のコードは手元ではコメントを消すと gcc -g 時のサイズが縮みました。

int main() {
    /*
     *
     *
     *
     *
     *
     *
     */
    puts("hello");
    /*
     *
     *
     *
     *
     *
     *
     */
    puts("world");
}

しかし、自分で書くとかはどうでも良くて、もっとまともなヤツが社内にあるのでそれ出してこのバグ閉じないといけないのでそれをやるべき…

http://code.google.com/p/google-glog/issues/detail?id=16

なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h