5. Backtrace系ライブラリ

RubyとかPythonみたいな高級言語だと、言語がうまくやってくれるのでBacktraceを簡単に見ることができるけれども、Cみたいな低級言語でそれを自分でやるのは難しい。なのでライブラリが存在している。Windowsは考えてない。

execinfo.h

わざわざインストールしなくても動きそうなのが、execinfo.hにあるbacktrace系関数で、man 3 backtraceとかすると見れる。

#include <execinfo.h>
#include <unistd.h>

void show_backtrace() {
  void *traces[1024];
  int sz = backtrace(traces, 1024);
  backtrace_symbols_fd(traces, sz, STDERR_FILENO);
}

backtraceにポインタを格納するための配列を渡すと、そこに戻りアドレスのリストみたいなのを詰め込んでくれる。それを更にbacktrace_symbolsみたいな関数に渡すと、そのアドレスから推測されるバイナリの名前とか関数名とかが書かれた文字列のリストに変換してくれる。この関数名とかはExportされているやつが対象っぽい。

例えばCRubyのvm_dump.cとかで使われていて、rb_bugでよく見るやつはこれ系をつかって生成してたりする。OSXの実装はちょっと劣っているというか、シグナルハンドラのコンテクストをうまく判定できないらしくて、自分で他のライブラリとかつかって実装している。シグナルトランポリンとかよくわからないので教えてほしい。

libunwind

execinfo.hのライブラリを更に高機能にしたのがlibunwindで、backtrace関数と互換のあるunw_backtraceを提供している以外にも、スタックの巻き戻しとかもできるっぽい。よく知らないけど、他のプロセスのスタックも見れるんだろうか。ライブラリでの提供なのでいろんなアーキテクチャに頑張って対応している。

binutilsのaddr2line

binutilsにはaddr2lineというコマンドラインツールがあって、アドレスから関数名の変換をしてくれる。これはDWARFのデバッグ情報を元に、Exportされていない関数の名前まで使ってくれるので、先のexecinfo.hのbackrace_symbolsよりも情報が多くなる。

CRubyにもどっかから持ってきたのかaddr2line.cってやつがあって、さっきのexecinfo.hで得た情報をもとにして、更にDWARFのデバッグ情報をつかって情報を拾い集めている。

libbfd

binutilsのaddr2lineのバックにあるのがlibbfdってやつで、オブジェクトファイルの操作をするライブラリらしい。Wikipediaにも載ってる。addr2lineの実装は1ファイルで結構小さいので、このlibbfdをリンクしながらaddr2lineから適宜ソースをパクってくると、コマンドラインからaddr2lineを叩かなくても、自分で自分のデバッグ情報を参照して自分のBacktraceを出力できる偉いプログラムが書ける。

全然関係ないけど、ibertyっていうライブラリがC++のシンボルのDemangleを行うユーティリティを提供してるみたいなんだけど、この名前がちょうどリンクするときに-libertyっていう名前になってクール。

libbacktrace

これはよくわからないんだけど、検索してたら出てきたやつ。多分libbfdと同じ感じの動きをしそうなんだけど、よくわからない。GCCとかGDBに最近組み込まれたのかな?自分は使ってみてはない。

これで何やったか

CRuby使ってるとよくstring.cのAssertion Errorで落ちると思うんだけど、それがなんで起こるのかを調査するために、次のようなpost-conditionをチェックする関数を随所に散りばめた。このpost-conditionはまだ正しくなくて、とりあえずminirubyで落ちてるからもっとRefineしないといけない。

static VALUE stringcheck(VALUE str) {
  if (FL_ALL(str, STR_NOEMBED)) {
    if (FL_ALL(str, STR_SHARED)) {
      VALUE shared_buffer = RSTRING(str)->as.heap.aux.shared;
      char *cache_buffer = RSTRING(str)->as.heap.ptr;
      long len = RSTRING(str)->as.heap.len;

      // Shared strings should have a shared buffer.
      bt_assert(shared_buffer != 0);
      // Shared strings should have a cache ptr.
      bt_assert(cache_buffer != NULL);
      // The shared buffer should be frozen.
      bt_assert(OBJ_FROZEN(shared_buffer));
      // The shared buffer should be a valid string.
      stringcheck(shared_buffer);

      if (FL_ALL(shared_buffer, STR_NOEMBED)) {
        // The length should be equal to the shared buffer.
        bt_assert(len <= RSTRING(shared_buffer)->as.heap.len);
        // The cache ptr should be equal to the shared buffer.
        bt_assert(cache_buffer - (RSTRING(shared_buffer)->as.heap.len - len) == RSTRING(shared_buffer)->as.heap.ptr);
      } else {
        // The length should be less than or equal to the shared buffer.
        bt_assert(len == RSTRING_EMBED_LEN(shared_buffer));
        // The cache ptr should be equal to the shared buffer.
        bt_assert(cache_buffer == RSTRING(shared_buffer)->as.ary);
      }
    } else {
      // Normal strings should have a positive length.
      bt_assert(RSTRING(str)->as.heap.len >= 0);
      // Normal strings should have a positive capacity.
      bt_assert(RSTRING(str)->as.heap.aux.capa > 0);
      // The length should be less than or equal to the capacity.
      bt_assert(RSTRING(str)->as.heap.aux.capa >= RSTRING(str)->as.heap.len);
      // Normal strings should have a buffer ptr.
      bt_assert(RSTRING(str)->as.heap.ptr != NULL);
    }
  }
  return str;
}

ここでbt_assertっていうのがAssertion違反を起こしたらBacktraceを吐きつつ落ちるマクロなんだけど、ここで出すBacktraceを名前付きにしたいと思って調べたらこんな感じになった。

実際libbfdでえっちらほっちらやってみたところ、出るには出た。だけど場所が微妙に違ったりしてどうなってるのかよくわからない。自分はexecinfo.hのbacktrace_symbols_fdぐらいの出力で、あとは適当に手でaddr2lineを実行すればいいかなと思った。それかint 3を埋め込む。

追記

上のコードをビルドが通るところまでなおした。string_assertionsというブランチを作った。まだ必要なところ全部に入れられているわけではない。

Embedのときは、STR_SHAREDの部分が長さになっていることに気づいたので、Embedのときのテストを削除した。

気づいて修正。適当に長さの分だけ下げたら同じポインタというのをチェックしている。ちなみに落ちているのはもともとstring.cにあったassert(OBJ_FROZEN(shared));。

bt_assert(!FL_TEST(shared_buffer, STR_SHARED));の間違いだった。結局、Sharedの階層は必ず2階層のフラットな状態になるわけではなく、dupし続ければ多段になることがわかったので、このテストは外している。

rb_bugを使って落とすようにした。

その他わかったこと:

  • gcc -O3 と gcc -O0 で、if (FL_TEST(...)) の動きが違う。
  • 引数をvolatileにすると、同様にif (FL_TEST(...)) の動きが違う。
  • というかFL_TESTをifの条件部で使うと、何故かどのケースでも正しく判定されない ケースが発生する。

    • -O0 で volatile にしても動きがおかしい。

    • STR_SHAREDが立っていないのに、if (FL_TEST(str, STR_SHARED))が真になる場合がある。

    • as.heap.ptrがgdbで見るとnon-nullなのに、bt_assert(RSTRING(str)->as.heap.ptr != NULL); で落ちる。

  • if (FL_TEST(...)) の代わりにif (FL_ALL(...)) を使ったらうまく判定できてい る。

    • 何故かas.heap.ptrのやつも引っかからなくなる。