自作の関数に対して、簡易コールグラフを出力する。

はじめに

本記事は、自作の関数(システムコールや、ライブラリ関数ではない)を呼び出した際に、その関数名を出力できないか調べてみた結果を記載したものである。
今回作成したものは、こちらから。

結論

GCCにおけるオプションの一つに「-finstrument-functions」というものがあり、これと+αでやりたいことができる。

GCCの-finstrument-functionsオプションとは何か

各関数で、呼び出された直後とreturn前 に自作の関数(ここでは、関数名を出力する関数)が呼べるようになる。
(一つ一つの関数に追加するのは面倒だから便利ということ。)

使い方

まだ、使い始めということもあり、簡単なサンプルしか示せないが、載せる。

  • __cyg_profile_func_enter()、__cyg_profile_func_exit() 関数を作成する。

   この関数名は固定である。
   今回やりたいことは、関数名の出力だが、その出力関数をこの2つの関数に記述する。
   (下記にtrace.cというファイルを載せる。詳細はそこで。)

  • 共有ライブラリ化する

   上で作成したソース(trace.c)を共有ライブラリ化する。

  • LD_PRELOAD で実行する。

   関数名を出力したい実行ファイルは、もうすでに作成済みであるとして、LD_PRELOADを使って、実行する。
    実行例:
     $ LD_PRELOAD=./libtrace.so ./main(実行ファイル)

trace.c

__cyg_profile_func_enter()、__cyg_profile_func_exit()の部分を載せる。

andre@andre-VirtualBox:~/work/ftrace$ cat trace.c
#define _GNU_SOURCE
#include<dlfcn.h>
#include<stdio.h>
#include<string.h>
#define MAX_SIZE 256
const char* addr2name               (   void* address,
                                        char* funcname      );
void        __cyg_profile_func_enter(   void* func_address,
                                        void* call_site     );
void        __cyg_profile_func_exit (   void* func_address,
                                        void* call_site     );
void        common_func             (   void* func_address,
                                        const char *fmt     );

/* ### 関数の呼び出し直後に実行される ### */
void __cyg_profile_func_enter(   void* func_address,
                                 void* call_site     )
{
    common_func( func_address, "enter =>" );
}

/* ### 関数の return 直前に実行される ### */
void __cyg_profile_func_exit (  void* func_address,
                                void* call_site     )
{
    common_func( func_address, "<= exit" );
}

void common_func( void* func_address, const char *fmt )
{
    char        _funcname[MAX_SIZE];
    const char  *_ret = NULL;

    memset( _funcname, 0x00, sizeof( _funcname ) );

    _ret = addr2name( func_address, _funcname );
    if( NULL != _ret )
    {
        printf( "%s _funcname = %s.\n", fmt,  _funcname );
    }
}

/* ### 関数ポインタのアドレスを(人が読める)関数名へ変換する ### */
const char*
addr2name( void* address, char* funcname )
{
    Dl_info dli;
    memset( &dli, 0x00, sizeof( dli ) );

    /* ### この dladdr() で関数名へ変換している ### */
    if( 0 != dladdr( address, &dli ) )
    {
        strncpy( funcname, dli.dli_sname, MAX_SIZE );
        return dli.dli_sname;
    }
    return NULL;
}

実行ファイルのソース

hello!とだけ出力する実行ファイルである。

andre@andre-VirtualBox:~/work/ftrace$ cat main.c
#include<stdio.h>

/* Proto Type */
void hello_world( void );

void
hello_world(void)
{
    printf( "hello!\n" );
}

int
main(void)
{
    hello_world();
    return 0;
}

ビルド&実行

今マイブームであるSConsを使って、ビルドしてみる。
SConstructという名前のファイルに、実行ファイルと共有ライブラリの2つを作成する命令を記述し、[scons]とだけ入力し、コマンド実行する。
http://www25.atwiki.jp/ovlivion-air/ が簡潔で、わかりやすい。
SConstruct

andre@andre-VirtualBox:~/work/ftrace$ cat SConstruct
Program('main.c', CFLAGS='-fPIC -finstrument-functions')
SharedLibrary('trace', Split('trace.c'), LIBS='dl', CFLAGS='-fPIC -DMACRO=_GNU_SOURCE')

scons

andre@andre-VirtualBox:~/work/ftrace$ scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
gcc -o trace.os -c -fPIC -DMACRO=_GNU_SOURCE -fPIC trace.c
scons: done building targets.

実行。。。

andre@andre-VirtualBox:~/work/ftrace$ LD_PRELOAD=./libtrace.so ./main
enter => _funcname = main.
enter => _funcname = hello_world.
hello!
<= exit _funcname = hello_world.
<= exit _funcname = main.

その他

記事を書いている途中に発見した、同じことをやっているページを発見。こちらのほうがちょっと詳しく書いてある。
http://alohakun.blog7.fc2.com/blog-entry-758.html

参考サイト

http://d.hatena.ne.jp/torutk/20090126/p1
→やりたいことをC++でやっていたページ
http://www.nxmnpg.com/ja/3/dladdr
http://linuxjm.sourceforge.jp/html/LDP_man-pages/man3/dlsym.3.html
→dladdr()について
http://linuxjm.sourceforge.jp/html/LDP_man-pages/man7/feature_test_macros.7.html
→「#define _GNU_SOURCE」を一番上に書くべきであることをなんとなくわからせてくれたページ。