ククログ

株式会社クリアコード > ククログ > ELFから公開されている関数名を抜き出す

ELFから公開されている関数名を抜き出す

先日、書きやすさとデバッグのしやすさを重視したC言語用テスティグフレームであるCutter 1.0.7がリリースされました。

Cutterでは、定義したテスト関数をフレームワークに登録する必要はありません。Cutterを用いたテストでは、共有ライブラリとしてテストを作成し、cutterコマンドでその共有ライブラリを読み込んで定義されているテスト関数を検出し実行します。

1.0.6までのCutterは、共有ライブラリから定義されているテスト関数を抽出するためにBFDライブラリを用いていました。しかし、共有ライブラリではなく静的ライブラリとしてBFDライブラリが提供されているプラットフォームがわりとあり、導入の障壁となる場合がありました。そこで、Cutter 1.0.7ではBFDライブラリに依存せず、共有ライブラリから定義されているテスト関数を抽出する機能を実装しました。

Cutter 1.0.7はExecutable and Linkable Format/Portable Executable/Mach-Oに対応しているため、Linux, *BSD, Solaris, Cygwin, Mac OS Xなどの環境でもBFDライブラリなしで動作するようになりました。

ELFのフォーマットを解説しているページや、readelfなどのELF関連ツールを紹介しているページはあるのですが、ELFからシンボル名を抜き出すプログラムを紹介しているページがなかったので、Cutterで行っている、ELFから公開されている関数名を抜き出す方法を紹介します。

下準備

簡略化のためファイルの内容をすべてメモリに読み込んでから処理します。コツコツ資源を利用したい場合は少しづつ読み込みながら処理することになります。

ファイルの内容を読み込むにはGLibのg_file_get_contents()が便利です。

gchar *content;
gsize length;

g_file_get_contents(filename, &content, &length, NULL);

これで、contentの中にファイルの内容が格納されました。これを使って公開されている関数名を抜き出します。

ELFのフォーマットに関する情報はelf.hで定義されています。ELF をパースするときはelf.hを使うと便利です。ここでも、elf.hを使います。

#include <elf.h>

ELFかどうかを判断

まず、ファイルがELFかどうかを判断します。

ELFは最初にヘッダが入っていて、それを見ることでELFかどうかを判断することができます。ここでは、64bit環境用のELFだけを対象とします。32bit環境用のELFを対象とする場合はコード中の「64」という箇所を「32」に変更します。どちらにも対応する場合はCutterのソースを参考にしてください。

Elf64_Ehdr *header = NULL;

header = (Elf64_Ehdr *)content;

if (memcmp(header->e_ident, ELFMAG, SELFMAG) == 0) {
    /* ELFファイル */
}

「MAG」は「マジック」の略だと思います。

共有ライブラリかどうかを判断

ELFであることが確認できたら、共有ライブラリかどうかを確認します。

if (header->e_type == ET_DYN) {
    /* 共有ライブラリ */
}

.dynsym/.dynstr/.textのセクションヘッダを探索

.dynsymセクションには動的に解決されるシンボルが入っています。.dynstrセクションにはそれらのシンボルの名前が入っています。これらを見ることで共有ライブラリの中にあるシンボル名の一覧を取得することができます。

.textには関数の本体などが入っています。.dynsymにあるシンボルが.textセクションに関連していると、共有ライブラリ内で定義されているシンボルだということがわかります。

.dynsym/.dynstr/.textのセクションヘッダを探し出すコードは以下のようになります。

Elf64_Shdr *dynstr = NULL;
Elf64_Shdr *dynsym = NULL;
uint16_t text_section_header_index = 0;

gsize section_offset;
uint16_t section_header_size;
uint16_t i, n_headers;
Elf64_Shdr *section_name_header;
gsize section_name_header_offset;
const gchar *section_names;

/* ファイルの先頭からセクションの先頭までのバイト数 */
section_offset = header->e_shoff;
/* 1つのセクションヘッダのバイト数 */
section_header_size = header->e_shentsize;
/* セクション数 */
n_headers = header->e_shnum;

/* ファイルの先頭からセクション名があるヘッダの先頭までのバイト数 */
section_name_header_offset =
    header->e_shoff +
    (header->e_shstrndx * header->e_shentsize);
/* セクション名があるヘッダ */
section_name_header =
    (Elf64_Shdr *)(content + section_name_header_offset);
/* セクション名が格納されている位置の先頭 */
section_names = content + section_name_header->sh_offset;

for (i = 0; i < n_headers; i++) {
    Elf64_Shdr *section_header = NULL;
    gsize offset;
    const gchar *section_name;

    /* ファイルの先頭からセクションヘッダの先頭までのバイト数 */
    offset = section_offset + (section_header_size * i);
    /* セクションヘッダ */
    section_header = (Elf64_Shdr *)(content + offset);
    /* セクション名 */
    section_name = section_names + section_header->sh_name;

    if (g_str_equal(section_name, ".dynstr")) {
        /* .dynstrセクション */
        dynstr = section_header;
    } else if (g_str_equal(section_name, ".dynsym")) {
        /* .dynsymセクション */
        dynsym = section_header;
    } else if (g_str_equal(section_name, ".text")) {
        /* .textセクションが先頭から何番目のセクションか */
        text_section_header_index = i;
    }
}

公開されているシンボル名一覧

.dynsym/.dynstr/.textのセクションヘッダが見つかったら、それらのセクションにアクセスして、共有ライブラリ内に定義されているシンボル一覧を取得できます。

公開されているシンボルが関数かどうかを判断する条件は、シンボルが.textセクションに関連付けられているかどうかです。

guint i, n_entries;
gsize symbol_section_offset;
gsize symbol_entry_size;
gsize name_section_offset;

/* ファイルの先頭からシンボルが定義されているセクションまでのバイト数 */
symbol_section_offset = dynsym->sh_offset;
/* シンボル定義領域のバイト数 */
symbol_entry_size = dynsym->sh_entsize;
/* ファイルの先頭からシンボル名が定義されているセクションまでのバイト数 */
name_section_offset = dynstr->sh_offset;
/* シンボル定義領域の数 */
if (symbol_entry_size > 0)
    n_entries = dynsym->sh_size / symbol_entry_size;
else
    n_entries = 0;

for (i = 0; i < n_entries; i++) {
    Elf64_Sym *symbol;
    uint64_t name_index;
    unsigned char info;
    uint16_t section_header_index;
    gsize offset;

    /* ファイルの先頭からシンボル定義領域までのバイト数 */
    offset = symbol_section_offset + (i * symbol_entry_size);
    /* シンボル定義 */
    symbol = (Elf64_Sym *)(content + offset);
    /* シンボル名は何番目に定義されているか */
    name_index = symbol->st_name;
    /* シンボルの情報 */
    info = symbol->st_info;
    /* シンボルに関連するセクションは何番目のセクションか */
    section_header_index = symbol->st_shndx;

        /* シンボルは関数に関連付けられている */
    if ((info & STT_FUNC) &&
        /* シンボルは公開されている */
        (ELF64_ST_BIND(info) & STB_GLOBAL) &&
        /* シンボルは.textセクションに関連付けられている */
        (section_header_index == text_section_header_index)) {
        const gchar *name;

        /* シンボル名 */
        name = content + name_section_offset + name_index;
        g_print("found: %s\n", name);
    }
}

参考

まとめ

elf.hを使って、BFDライブラリに依存せずに、ELFから公開されている関数名を抜き出す方法を紹介しました。ELFを読み書きするlibelfというライブラリもあるのですが、ELFから情報を取得するだけなら、elf.hで十分でしょう。

関数名が取得できたら、GModuleで関数本体を取得することができます。GLibは便利ですね。

いずれ、PEまたはMach-Oから公開されている関数名を抜き出す方法も紹介するかもしれません。