2005年11月 6日

普通のやつらの下を行け: BFDでデバッグ情報の取得

gcc に -g オプションを与えるとデバッグ情報をバイナリに埋め込むことができます。この情報は通常 gdb などのデバッガによって利用されますが、普通のプログラムでも利用できれば何かおもしろいことができるかもしれません。

普通のやつらの下を行けの第4回として、今回は BFD (libbfd) を用いてデバッグ情報を取得する方法を取り上げたいと思います。

 

BFD とは

BFD (Binary File Descriptor library) は各種バイナリフォーマットに対して低レベルな操作を行うためのライブラリです。 GNU binutils に含まれています。Debian GNU/Linux なら次のコマンドでインストールできます。

% sudo apt-get install binutils-dev

デバッグ情報の取得

ここでは BFD の bfd_find_nearest_line を用いてファイル名と行番号を取得するプログラムを作ってみます。環境は Debian GNU/Linux sarge です。

#include <assert.h>
#include <bfd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libgen.h>

static bfd *abfd;
static asymbol **symbols;
static int nsymbols;

void
show_debug_info (void *address)
{
    asection *section = bfd_get_section_by_name(abfd, ".debug_info");
    assert(section != NULL);

    const char *file_name;
    const char *function_name;
    int lineno;
    int found = bfd_find_nearest_line(abfd, section, symbols,
                                      (long)address,
                                      &file_name,
                                      &function_name,
                                      &lineno);
    if (found && file_name != NULL && function_name != NULL) {
        char tmp[strlen(file_name)];
        strcpy(tmp, file_name);
        printf("%s:%s:%d\n", basename(tmp), function_name, lineno);
    }
}

void whereami ()
{
    // 戻りアドレスのままだとデバッグ情報がひとつ先の行にいっ
    // てしまうので -1 している
    show_debug_info(__builtin_return_address(0) - 1);
}

void
__attribute__((constructor))
init_bfd_stuff ()
{
    abfd = bfd_openr("/proc/self/exe", NULL);
    assert(abfd != NULL);
    bfd_check_format(abfd, bfd_object);

    int size = bfd_get_symtab_upper_bound(abfd);
    assert(size > 0);
    symbols = malloc(size);
    assert(symbols != NULL);
    nsymbols = bfd_canonicalize_symtab(abfd, symbols);
}

int
main ()
{
    whereami();
    return 0;
}

このプログラムを gcc -g -lbfd whereami.c でコンパイルして実行すると、 whereami() を呼び出した行のファイル名と関数名、行番号が表示されます。デバッグ情報を利用するため、 -g オプションは必須です。

% gcc -lbfd whereami.c
% ./a.out
whereami.c:main:58

このプログラムの要点は次の2つです。

  • init_bfd_stuff() 関数でシンボルテーブルなどの情報を初期化している。 GCC 拡張の __attribute__((constructor)) を指定しているため、明示的に呼び出す必要がない。
  • show_debug_info() 関数では bfd_find_nearest_line() を呼び出してファイル名、関数名、行番号の情報を取得している。

いろいろ面倒くさいことをやっていますが、実はこれと同じ出力を得るだけなら、 __FILE__, __FUNCTION__, __LINE__ というマクロを用いるだけでもできます。

#define whereami() do { printf("%s:%s:%d\n", __FILE__, __FUNCTION__, __LINE__) } while (0)

これらのマクロはデバッグ用のトレース関数を作るときに使うと便利です。

バックトレースにファイル名と行番号を付加

先日の記事ではglibc の backtrace() 関数を用いてバックトレースを表示する方法を紹介しました。 上のコードを流用すれば、 gdb と同様にバックトレースにファイル名と行番号の情報を含めることができます。

#include <assert.h>
#include <bfd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libgen.h>
#include <execinfo.h>

static bfd *abfd;
static asymbol **symbols;
static int nsymbols;

void
show_debug_info (void *address)
{
    // 上と同じなので省略
}

void
__attribute__((constructor))
init_bfd_stuff ()
{
    // 上と同じなので省略
}

void
show_backtrace ()
{
    void *trace[128];
    int n = backtrace(trace, sizeof(trace) / sizeof(trace[0]));
    for (int i = 0; i < n; ++i) {  // C99 なら OK
        show_debug_info(trace[i] - 1);  // 上と同様の理由で -1
    }
}

void
bar ()
{
    show_backtrace();
}

void
foo ()
{
    bar();
}

int
main ()
{
    foo();
    return 0;
}

このプログラムをコンパイルして実行すると、次のような結果が出力されます。 C99 の文法を使っているので -std=c99 が必要です。

% gcc -g -std=c99 backtrace2.c -lbfd
% ./a.out
backtrace2.c:show_backtrace:38
backtrace2.c:bar:62
backtrace2.c:foo:68
backtrace2.c:main:74

まとめ

今回は BFD の bfd_find_nearest_line() を利用してデバッグ情報を取得する方法を取り上げました。 bfd.h を見ると他にもいろいろおもしろそうな関数があるので、折を見てまた遊んでみたいと思います。