torutkのブログ

ソフトウェア・エンジニアのブログ

GCCのコンパイルオプションで関数トレーサ

Binary Hacks ―ハッカー秘伝のテクニック100選
この本の「#77 関数へのenter/exitをフックする」で、GCCコンパイルオプション-finstrument-functionsを使い、関数が呼び出された時、関数から復帰するときにフックを入れる方法が紹介されています。フック関数のシグニチャは以下です。

void __cyg_profile_func_enter(void* func_addr, void* call_site);
void __cyg_profile_func_exit(void* func_addr, void* call_site);

このフック処理に渡されるアドレスから関数名を出して、関数の実行を追う簡易な関数トレーサを作成します。アドレスから関数に変換する方法で一番簡単そうなのは、同じ本の「#62 dlopenで実行時に動的リンクする」でglibcGNU拡張として紹介されているdladdr関数です。

#include <dlfcn.h>
int dladdr(void* addr, Dl_info* info);

簡単な関数Enter/Exitプリント処理

簡易関数トレーサVer.1
// simpletrace.cpp
#include <dlfcn.h>
#include <iostream>

extern "C" {
    void __cyg_profile_func_enter(void* func_address, void* call_site);
    void __cyg_profile_func_exit(void* func_address, void* call_site);
}

// dladdrを用いて関数アドレスから関数シンボルへ変換
const char* addr2name(void* address) {
    Dl_info dli;
    if (0 != dladdr(address, &dli)) {
        return dli.dli_sname;
    }
    return 0;
}

void __cyg_profile_func_enter(void* func_address, void* call_site) {
    const char* func_name = addr2name(func_address); 
    if (func_name) {
        std::cout << "simple tracer: enter - " << func_name << std::endl;
    }
}

void __cyg_profile_func_exit(void* func_address, void* call_site) {
    const char* func_name = addr2name(func_address); 
    if (func_name) {
        std::cout << "simple tracer: exit  - " << func_name << std::endl;
    }
}
  • C++でビルドするので、フック関数の宣言をextern "C"で囲みます。
  • アドレスからシンボルに変換する処理をユーティリティ関数addr2nameで定義しています。うまく変換できないときは0を返します。
    • なお、dladdrが成功(0以外をリターン)しても、Dl_info構造体のdli_snameに0が入っていることがあります。
  • フック処理関数内では単に標準出力にプリントしています。

上のソースファイルをコンパイルしライブラリを生成します。

$ g++ -fPIC -shared simpletrace.cpp -o libsimpletrace.so

簡易トレース対象プログラムを-finstrument-functionsオプション付きでビルドします。

$ g++ -fPIC -finstrument-functions main.cpp -o main

フック処理を記述したライブラリlibsimpletrace.soをLD_PRELOADで指定してトレース対象プログラムを実行します。

$ LD_PRELOAD=$HOME/libsmpletrace.so ./main
simple tracer: enter - main
simple tracer: enter - _ZN4calc13relativeSpeedEd
simple tracer: exit  - _ZN4calc13relativeSpeedEd
simple tracer: enter - _ZN5calc213relativeSpeedEd
simple tracer: exit  - _ZN5calc213relativeSpeedEd
simple tracer: exit  - main
$

C++の場合、関数シンボル名はマングルされた文字列となっています。一番単純なのは、この実行結果をc++filtコマンドにパイプしてしまう方法です。

$ LD_PRELOAD=$HOME/libsmpletrace.so ./main |c++filt
simple tracer: enter - main
simple tracer: enter - calc::relativeSpeed(double)
simple tracer: exit  - calc::relativeSpeed(double)
simple tracer: enter - calc2::relativeSpeed(double)
simple tracer: exit  - calc2::relativeSpeed(double)
simple tracer: exit  - main
$
応用
  1. シンボル名がC++マングルされた文字列の場合に、プログラム中でデマングルする
    • Binary Hackの「#68 C++のシンボルを実行時にデマングルする」で紹介されているのabi::__cxa_demangle()を使う
  2. 簡易トレース対象をライブラリ単位で指定する
    • dladdrの処理結果でアドレスに対応するバイナリ(ライブラリファイルまたは実行ファイル)の名前が取得できるので、トレース対象ライブラリ名一覧に合致するものがあるときのみ出力する等
  3. 引数・戻り値もトレースに自動表示する
    • ちょっと難易度が高い・・・
  4. トレース対象にしたくない
    • 関数の宣言に以下の例に示すようにattribute属性を追記する
__attribute__((no_instrument_function))
void func();