hogetrace - 関数コールトレーサ

でかいソフトウェアの、大量のソースコードを短時間で読む必要が生じたので、その補助ツールとしてptrace(2)ベースのLinux用関数トレーサを自作しました。こういうツール上でまずソフトウェアを実行してみて、どのファイルのどの関数がどういう順で呼ばれるか把握おけば、いきなりソースコードの山と格闘を始めるより楽かなーと思いまして。せっかく作ったので公開します。

http://binary.nahi.to/hogetrace/

straceはシステムコールだけ、ltraceは共有ライブラリ(DSO)の関数呼び出しだけ*1をトレースしますが、このツールは、実行バイナリ中の自作関数の呼び出しもトレースします。例えば再帰で1から10まで足し算するソースコードを用意して

% cat recursion.c
#include <stdio.h>
int sum(int n) {
  return n == 0 ? 0 : n + sum(n - 1);
}
int main() {
  int s = sum(10);
  printf("sum(10) = %d\n", s);
  return s;
}

最適化なしで普通にコンパイルしてトレーサにかけてみるとこんな感じの出力になります。-g をつけると、ファイル名/行番号を出せるので、そうしてあります。

% gcc -g -o recursion recursion.c
% hogetrace --plt -lATv ./recursion
[pid 17700] +++ process 17700 attached (ppid 17699) +++
[pid 17700] === symbols loaded: './recursion' ===
[pid 17700] ==> _start() at 0x08048300
[pid 17700]    ==> __libc_start_main@plt() at 0x080482cc
[pid 17700]       ==> __libc_csu_init() at 0x08048460
[pid 17700]          ==> _init() at 0x08048294
[pid 17700]             ==> call_gmon_start() at 0x08048324
[pid 17700]             <== call_gmon_start() [eax = 0x0]
[pid 17700]             ==> frame_dummy() at 0x080483b0
[pid 17700]             <== frame_dummy() [eax = 0x0]
[pid 17700]             ==> __do_global_ctors_aux() at 0x080484d0
[pid 17700]             <== __do_global_ctors_aux() [eax = 0xffffffff]
[pid 17700]          <== _init() [eax = 0xffffffff]
[pid 17700]       <== __libc_csu_init() [eax = 0x8049534]
[pid 17700]       ==> main() at 0x08048404 [/home/sato/ht-trunk/sample/recursion.c:9] 
[pid 17700]          ==> sum(int n <10>) at 0x080483d4 [/home/sato/ht-trunk/sample/recursion.c:4] 
[pid 17700]             ==> sum(int n <9>) at 0x080483d4 [/home/sato/ht-trunk/sample/recursion.c:4] 
[pid 17700]                ==> sum(int n <8>) at 0x080483d4 [/home/sato/ht-trunk/sample/recursion.c:4] 
[pid 17700]                   ==> sum(int n <7>) at 0x080483d4 [/home/sato/ht-trunk/sample/recursion.c:4] 
[pid 17700]                      ==> sum(int n <6>) at 0x080483d4 [/home/sato/ht-trunk/sample/recursion.c:4] 
[pid 17700]                         ==> sum(int n <5>) at 0x080483d4 [/home/sato/ht-trunk/sample/recursion.c:4] 
[pid 17700]                            ==> sum(int n <4>) at 0x080483d4 [/home/sato/ht-trunk/sample/recursion.c:4] 
[pid 17700]                               ==> sum(int n <3>) at 0x080483d4 [/home/sato/ht-trunk/sample/recursion.c:4] 
[pid 17700]                                  ==> sum(int n <2>) at 0x080483d4 [/home/sato/ht-trunk/sample/recursion.c:4] 
[pid 17700]                                     ==> sum(int n <1>) at 0x080483d4 [/home/sato/ht-trunk/sample/recursion.c:4] 
[pid 17700]                                        ==> sum(int n <0>) at 0x080483d4 [/home/sato/ht-trunk/sample/recursion.c:4] 
[pid 17700]                                        <== sum() [eax = 0x0]
[pid 17700]                                     <== sum() [eax = 0x1]
[pid 17700]                                  <== sum() [eax = 0x3]
[pid 17700]                               <== sum() [eax = 0x6]
[pid 17700]                            <== sum() [eax = 0xa]
[pid 17700]                         <== sum() [eax = 0xf]
[pid 17700]                      <== sum() [eax = 0x15]
[pid 17700]                   <== sum() [eax = 0x1c]
[pid 17700]                <== sum() [eax = 0x24]
[pid 17700]             <== sum() [eax = 0x2d]
[pid 17700]          <== sum() [eax = 0x37]
[pid 17700]          ==> printf@plt() at 0x080482ec
sum(10) = 55
[pid 17700]          <== printf@plt() [eax = 0xd]
[pid 17700]       <== main() [eax = 0x37]
[pid 17700]       ==> _fini() at 0x080484f8
[pid 17700]          ==> __do_global_dtors_aux() at 0x08048350
[pid 17700]          <== __do_global_dtors_aux() [eax = 0x0]
[pid 17700]       <== _fini() [eax = 0x0]
[pid 17700] +++ process 17700 detached (ppid 17699) +++
hogetrace: done

結果の55が計算され、表示されるまでの関数の呼び出し状況がわかると思います。


当初は、KLabのftraceなど、gcc -finstrument-functions系のトレーサを使わせてもらおうと思っていたのですが、(私の解析したいソフトウェアの場合)ソフトウェアの再コンパイルMakefileの書き換えに大変手間がかかることがわかったので、「stripされていなければ再コンパイル不要」なツールを自作してみたという感じです*2。連休もあることだし、勉強がてら。仕組みはstrace/ltrace/gdb と同じくptrace(2)ベースで、fork/exec/clone対応、DSOの関数呼び出しのトレースも対応、です。なお関数の引数表示のところは、ftraceのソースコードをそのまま流用させていただきました。ありがとうございます。


以下仕組みのメモ + 遊んでいておもしろかったところ (あとでちゃんと書く):

  • libbfdを使って解析対象のプログラムの.symtabを読んで、全関数の先頭アドレスを得ている。ついでに.plt上の合成(synthetic)シンボル(printf@pltとか)も得ている
  • libopcodesで、さっき得た各関数の先頭アドレスから、バイナリを逆アセンブル、ret命令を探すことで関数からのリターンアドレスを静的に解析 /特定している。スタック見て動的にやる方法もありますが、今回は静的にやった。ltraceとは違う方法にしてみたかったというのもあるし、マルチスレッド環境で動的にセットしたBPを別スレッドが踏むとめんどくさいというのもあったり。
  • ret/retq命令探しの際、_start() はretじゃなくてhltだとか、末尾再帰関数で-O2だとretがないとか、throwで抜けてるとやっぱりretがないとか、構造体を値で戻すような関数だと0xC3ではなく0xC2だったりとか、__attribute__((noreturn))されてるとretしないとか、そういう関数を呼ぶ側も callではなくjmpにしちゃうとか、探すときに細かい注意点がある。
  • ひとつの関数内に複数のretが存在することが(もちろん?)ある。switch-case使ったときとか。
  • .debug_info 見て、関数の定義されているファイル名、行番号の情報を取得している。同じく.debug_info 見て、関数の引数の情報を取得している。
  • 解析対象をforkしてptrace(TRACEME);してsetenv(LD_BIND_NOW=1);してexec。setenvしておかないと、 PLT経由の関数呼びの初回(_dl_runtime_resolve時)がすぐにreturnするように表示されてしまう。子プロセスの起動時間を延ばしてしまう悪いhackかも。
  • 解析対象がメモリにロードされたら、関数の先頭アドレス、RET命令アドレスにブレークポイント設定。ptrace(POKE)でそのアドレスに0xCCを書くだけ
  • 解析対象が0xCCを踏むと、tracefにシグナルで通知がくる。ので、適当に情報を表示。0xCCで止まった解析対象の再開方法は、gdbとかと同じ(説明になっていない)。EIPを戻して元の命令書き戻してシングルステップして云々。詳しくは書籍「デバッガの理論と実装 (ASCII SOFTWARE SCIENCE Language)」あたりを。
  • PLT経由のジャンプのフックは、まずPLT先頭の jmp *0x80... のとこに0xCCを仕掛け、その次の命令 push 0x.. にも0xCCを仕掛けておく。jmp*を踏んで解析対象が止まったら、解析対象のスタック上のリターンアドレスをPEEKして、tracef側に記憶する。そのリターンアドレスをpush 0x..のアドレスにすりかえる(POKE)。push 0xにリターンしてくるとまた0xCCを踏んで止まるので、tracef側に記憶しておいた本物のリターンアドレスを、今度はEIPに書き込んで continue。以上。これはひどい
  • この方法だと、DSO内で例外がthrowされて、それが解析対象のバイナリまで到達すると、abortする。ごめんなさいごめんなさいごめんなさい。まぁ滅多にそんなことしないよね。
  • 最低限の対策として、__cxa_throw@plt の先頭でブレークした時は、このリターンアドレスのtweakを行わないようにしておいた。これで、exe内でthrowして、exe内でcatchする場合は問題なくなった。dso内でthrowして、exe内でcatchするプログラムをtraceする場合は、--pltか-Tのどちらかをはずしてもらわないとダメだ...。やはりltrace風の(よくあるデバッガの)、スタック見てリターンアドレスに地雷置くやりかたのほうがよかったか。
  • GOT/PLT回りは、arch毎にコードがいる。x86_64はx86とほとんど同じだけど、jmp* がPC相対なのでやっぱり#ifdefすることになった。
  • 例外のthrowで表示(コールツリーのインデント具合)が乱れないよう、各プロセス/スレッドの関数呼び出し状況を、tracef側の std::stack にも記憶しておく。
  • ptraceはぐぐってもあまり文献がなくて、結局頼りになるのはstraceとltraceのソースコードだけだった。straceは多OS対応で魔窟なので、ltraceを中心に見た。でもltraceはスレッドというかcloneというか PTRACE_O_TRACECLONE というかに対応していない。結局そこは試行錯誤。あと、ltraceの .dynほげセクションの処理部分は面白かったのだけど、今回は使わない...
  • straceをstraceしたり、ltraceをstraceしたりするといろいろなことがわかった。
  • プロセスのptraceを終了する(detach)と、そのプロセスが即死する現象に悩んだ。単に、プロセスの0xCCを元に戻さないままデタッチしていたからシグナルで死んでいるだけだった。ありがとうございました。forkすると0xCCなまま.textがコピーされることも忘れずに。当然ですが。
  • ptrace(GETREGS)に渡す構造体の初期化を省いたら、それが原因でvalgrindが死ぬほどの量の警告を出してくれた。ptraceの引数の初期化省略が原因だと気づくのにすこし時間がかかった。
  • ptrace(2)は、以前straceもどきを作ってみてそれでおわりにしていたけど、いろいろ試してみたら想像以上におもしろかった。早いとこ遊んでおけばよかった。

デバッガもどきを作るのは初めてだったので、とてーも楽しかったです。ソースコードやrpmはリンク先にあります。

*1:-x というオプションが一応ありますが略

*2:すでに類似ツールあるかなぁとか、gdbでできるんじゃないかなぁとか思いつつも。なにかありますかねぇ

関数テンプレートの特殊化について

関数テンプレートの特殊化いろいろ


なんか日記を書くのを忘れていました。よくない。というわけで、最近おぼえたtipsを書きます。クラステンプレート内のテンプレートの特殊化(もどき)についての自分なりのまとめ。


まず、

namespace A {
  template<int V>
  static void YYY() {
    std::printf("default %d\n", V);
  }
}
int main() {
  YYY<0>();
  YYY<128>();
}

のような関数テンプレートがあるとき、これを特殊化*1した関数は

namespace A {
  template<int V>
  static void YYY() {
    std::printf("default %d\n", V);
  }
  template<>
  static void YYY<128>() {
    std::printf("for 128\n");
  }
  template<>
  static void YYY<256>() {
    std::printf("for 256\n");
  }
}

のように書けます。同様に、メンバ関数テンプレート

struct B {
  template<int V>
  static void YYY() {
    std::printf("default %d\n", V);
  }
};

があった場合、この特殊化は、Bの中、クラススコープには規格上書いてはいけないことになっており、g++も実際にそういうコードを受け付けない*2ので、

template<>
inline void B::YYY<128>() {
  std::printf("for 128\n");
}
template<>
inline void B::YYY<256>() {
  std::printf("for 256\n");
}

のように名前空間スコープに書けばOKです。クラス外に出すとインライン化されにくくなるので、inlineをつけておきました。では、外側のクラスがクラステンプレートだった場合はどうかというと、

template <typename T>
struct C {
  template<int V>
  static void YYY() {
    std::printf("default %d\n", V);
  }
};

次のように、外側のCを明示的に特殊化した上で内側のYYY()を特殊化するような関数を名前空間スコープに書くことはできるけど、

// C<int> に対する YYY<128>()
template<> template<>
inline void C<int>::YYY<128>() {
  std::printf("for 128\n");
}

Cを特殊化しないでYYY()を特殊化するのはg++に "error: enclosing class templates are not explicitly specialized" だと言われてしまいコンパイルできません。C++の規格にも上のコードとそっくりの例まで挙げてダメだと書かれてます*3

// error: enclosing class templates are not explicitly specialized
template<typename T> template<>
inline void C<T>::YYY<128>() {
  std::printf("for 128\n");
}

そういうときどうするかというと、あまり詳しくないですがたぶん、古典ことMC++D, Modern C++ Designの2章に書いてある通り、

  • boost::type;
  • boost::mpl::bool_<B>
  • boost::mpl::int_
  • boost::mpl::integral_c

あたりのお好きなもの(LokiでいうType2Type, Int2Type)を使って、型とか個々の整数値、列挙値をそれ固有の軽い型のオブジェクトに変換して(説明になっていない)、オーバーロードで適切な関数を呼ぶようにすると思います。上の4つのMPL系クラステンプレートはそれぞれ3行ぐらいですので、自作するのも簡単です。

template <typename T>
class C { 
  void YYY_(boost::mpl::int_<128>) { /* specialized */ } 
  void YYY_(...) { /* default */ } 
public: 
  template<int V>
  static void YYY() {
    YYY_(boost::mpl::int_<V>());
  }
}; 

こうすると、YYY<0>(); と YYY<128>(); で別々の関数、それぞれ YYY_(...) と YYY_(boost::mpl::int_<128>) が呼ばれることになるので、特殊化が実現できたようなものです。これをここでは特殊化もどきと呼びます。


あるいは、次のように、クラススコープ、特に特殊化されていないクラステンプレートのスコープでも、クラステンプレートの部分特殊化なら可能であることを利用して、D::XXX()からD::D_::XXX()に処理を委譲してしまう手もあると思います。

template <typename T>
class D {
  template <int V, typename Dmy = void>
  struct D_ {
    static void XXX() {
      std::printf("default %d\n", V);
    }
  };
  template <int V> // partial specialization
  struct D_<V, typename boost::enable_if_c<V == 128>::type> {
    static void XXX() {
      std::printf("for %d\n", V);
    }
  };
  template <int V> // partial specialization
  struct D_<V, typename boost::enable_if_c<V == 256>::type> {
    static void XXX() {
      std::printf("for %d\n", V);
    }
  };
public:
  template<int V>
  static void XXX() {
    D_<V>::XXX();
  }
};

BoostのMLのこのメールでは、こちらの方法が推奨されていました。次の通り、D_ の部分特殊化版を誰でも後から追加できますからね。

// namespace scope に V==0の場合の特殊化を後から追加
template <typename T> template <int V>
struct D<T>::D_<V, typename boost::enable_if_c<V == 0>::type> {
  static void XXX() {
    std::printf("for %d\n", V); 
  }
};

のように。boost::enable_ifは、これまた数行で書けるだいたいこんな感じのクラステンプレートです。というわけで、「クラステンプレートC内のメンバ関数テンプレートを、Cを特殊化しないで特殊化(もどき)する」には、「クラステンプレートに委譲しとけば?」でファイナルアンサーのように思うのですが、練習(何の?)ため、他の関数に処理を委譲せずに特殊化もどきを実現できないかを考えてみます。

関数テンプレートの enable_ifオーバーロードによる特殊化もどき - 2分岐バージョン


もし、特殊化する関数が1つだけ、Pのときの関数と!Pのときの関数だけ用意すればことたりるなら、boost::enable_ifと関数のオーバーロードを使って、委譲なしで綺麗に特殊化もどきを実現できます。

  template<bool B>
  static void bar(typename boost::enable_if_c<B>::type* = 0) {
    std::printf("for true\n");
  }
  template<bool B>
  static void bar(typename boost::disable_if_c<B>::type* = 0) {
    std::printf("for false\n");
  }

としたり、

  template<int V>
  static void boo(typename boost::enable_if_c<V == 0>::type* = 0) {
    std::printf("for 0\n");
  }
  template<int V>
  static void boo(typename boost::enable_if_c<V != 0>::type* = 0) {
    std::printf("default\n");
  }

としたり、です。

(おまけ)関数テンプレートの enable_ifオーバーロードによる特殊化もどき - 3+分岐バージョン


ですが、合計3つ以上の関数を用意したい場合にはenable_ifはちょっと面倒です。

  template<int V>
  static void boo(typename boost::enable_if_c<V == 0>::type* = 0) {
    std::printf("for 0\n");
  }
  template<int V>
  static void boo(typename boost::enable_if_c<V == 1>::type* = 0) {
    std::printf("for 1\n");
  }
  static void boo(typename boost::enable_if_c<V == 2>::type* = 0) {
    std::printf("for 2\n");
  }
  static void boo(typename boost::enable_if_c<!(V == 0) && !(V == 1) && !(V == 2)>::type* = 0) {
    std::printf("default\n");
  }

のようにdefaultな関数の引数がえらいことになります。boo(...) などでdefault関数を書ければいいんですけどね。コンパイラに曖昧だと言われてしまってどうにも。どうにかなるでしょうか。


上の例は、特殊化(もどき)の条件(V==ほげ)が2回づつ登場してしまっているのが気に入りません。ですので、今回は、MPLを使って、頑張って特殊化もどきの条件を各1回だけ書けばいいようにしてみました。はじめてのMPLと。

#include <cstdio>
#include <boost/utility/enable_if.hpp>
#include <boost/type_traits/is_same.hpp>
#include <boost/mpl/int.hpp>
#include <boost/mpl/bool.hpp>
#include <boost/mpl/lambda.hpp>
#include <boost/mpl/list.hpp>
#include <boost/mpl/at.hpp>
#include <boost/mpl/or.hpp>
#include <boost/mpl/fold.hpp>

template <typename T>
struct X {

  struct detail {
    typedef boost::mpl::list<
      boost::mpl::lambda<
        boost::is_same<
          boost::mpl::_1,
          boost::mpl::int_<128>  // V==128で特殊化したい
        >
      >::type,
      boost::mpl::lambda<
        boost::is_same<
          boost::mpl::_1,
          boost::mpl::int_<256>  // V==256で特殊化したい
        >
      >::type
    > value_list;

    // boost::mpl::fold<> にあとで渡す
    template<int V>
    struct compar_meta_fn {
      template<typename Sum, typename Seq>
      struct apply {
        typedef boost::mpl::or_<
          Sum, /* true_ or false_ */
          typename Seq::type::template apply<boost::mpl::int_<V> >::type
        > type;
      };
    };
  };

  // ::template apply<... という奇妙な文法については、書籍 "C++ Templates - the complete guide" の9章に丁寧な解説があります
  template<int V>
  static void XXX(typename boost::enable_if<
                     typename boost::mpl::at_c<typename detail::value_list, 0>::type::template apply<boost::mpl::int_<V> >::type
                  >::type* = 0) {
    std::printf("for %d\n", V); // 128
  }

  template<int V>
  static void XXX(typename boost::enable_if<
                     typename boost::mpl::at_c<typename detail::value_list, 1>::type::template apply<boost::mpl::int_<V> >::type
                  >::type* = 0) {
    std::printf("for %d\n", V); // 256
  }

  template<int V>
  static void XXX(typename boost::disable_if<
                     typename boost::mpl::fold<
                       typename detail::value_list,
                       boost::mpl::false_,
                       typename boost::mpl::lambda<typename detail::template compar_meta_fn<V> >::type
                    >::type
                  >::type* = 0) {
    std::printf("default %d\n", V);
  }
};

int main() {
  X<int>::XXX<0>();
  X<long>::XXX<128>();
  X<float>::XXX<256>();
  X<double>::XXX<512>();
}

以上です。実行結果:

% ./a.out
default 0
for 128
for 256
default 512

整数値でなく型で特殊化もどきをしたい場合は、boost::mpl::int_ではなくboost::typeを使えばOKです。is_same<>も別のに変えた方がいいかもしれないけど。で、ちょっとこのままでは長すぎるので、適当にマクロで(BOOST.PPは見なかったことにします)

template <typename T> struct Z {
  LIST_BEGIN()
    LIST_ELEM(128),
    LIST_ELEM(256)
  LIST_END(lst);

  template<int V>
  static void XXX(ENABLE_NTH(lst,0)) {
    std::printf("for %d\n", V); // 128
  }
  template<int V>
  static void XXX(ENABLE_NTH(lst,1)) {
    std::printf("for %d\n", V); // 256
  }
  template<int V>
  static void XXX(OTHERWISE(lst)) {
    std::printf("default %d\n", V);
  }
};

くらいに縮めればなんとか使え...ないですかね。とりあえず、異なるVごとに別の XXX(OTHERWISE) 関数が生成されてしまうのは問題か。

結論


MPLを今更ながら使ってみたら面白かった。なお、MPLを使ってみるにあたっては、稲葉一浩さんのBoost本(第二版)に大変お世話になりました。

*1:関数テンプレートの部分的な特殊化はできないから、以下関数に付いて単に特殊化と言ったら明示的特殊化です

*2:VC++はなぜか書けるみたいだけど

*3:これまたVC++では通るみたいですが...。なんてプログラマに優しいんだ