2005年11月12日

普通のやつらの下を行け: objcopy で実行ファイルにデータを埋め込む

プログラムの実行に不可欠なデータをファイルから読み込んで利用することがあります。この方法を用いると、データの更新が手軽にできるという利点がある一方で、単体の実行ファイルで実行できない、データファイルが紛失してしまう、といった問題もあります。

普通のやつらの下を行けの第5回として、今回は objcopy を用いて実行ファイルにデータを埋め込む方法を取り上げたいと思います。

 

データの埋め込み

小さなデータをソースコードに埋め込むのは簡単です。ソースコード中に埋め込まれている "hello, world" などのメッセージはソースコードに埋め込まれたデータといえます。

一方、画像や辞書などの巨大なデータをソースコードに埋め込むのはそう簡単ではありません。まず、データを文字列などに変換する必要がある上に、変換後の巨大なソースコードはコンパイラが処理できるサイズを超えてしまう可能性があります。

objcopy

そこで登場するのが GNU binutils に付属する objcopy コマンドです。objcopy を使うと任意のファイルをリンク可能なオブジェクトファイルに変換できます。

たとえば、 foo.jpg を i386 用の ELF32 形式のオブジェクトファイル foo.o に変換するには次のように実行します。

% objcopy -I binary -O elf32-i386 -B i386 foo.jpg foo.o

foo.o をリンクした C のプログラムからは foo.jpg のデータは以下の変数名を用いて参照できます。

extern char _binary_foo_jpg_start[];
extern char _binary_foo_jpg_end[];
extern char _binary_foo_jpg_size[];

ポインタではなく配列なところがポイントです。これらの変数はたとえば次のように使います。

const char *start = _binary_foo_jpg_start;  // データの先頭のアドレスを取得
const char *end = _binary_foo_jpg_end;  // データの末尾のアドレス + 1 を取得
int size = (int)_binary_foo_jpg_size;  // データのサイズを取得

最後の _binary_foo_jpg_size は &_binary_foo_jpg_size[0] がアドレスではなく値 (データのサイズ) となっているので要注意です。

郵便番号検索ツール

それでは、いよいよ巨大なデータを埋め込んだプログラムを objcopy を使って作ってみましょう。ここでは郵便番号データを使って郵便番号検索ツール zipgrep を作ることにします。

まずは CSV 形式の郵便番号データを取得してオブジェクトファイルに変換します。nkf を使って半角かなを全角に変換し、iconv を使って Shift_JIS を UTF-8 に変換しています。 新しい nkf なら、nkf だけで UTF-8 に変換することも可能です。

% wget http://www.post.japanpost.jp/zipcode/dl/kogaki/lzh/ken_all.lzh
% lha e ken_all.lzh
% nkf -Ss-Z ken_all.csv | iconv -f sjis -t utf8 > zipcode.csv
% objcopy --readonly-text -I binary -O elf32-i386 -B i386 zipcode.csv zipcode.o

変換した zipcode.csv は 17MB になりました。

次に、郵便番号データを grep するプログラムを作成します。ここでは PCRE を使って正規表現の処理を行うことにします。

#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <pcre.h>

extern char _binary_zipcode_csv_start[];
extern char _binary_zipcode_csv_end[];

int
main (int argc, char **argv)
{
    assert(argc == 2);
    const char *error_message;
    int error_offset;
    pcre *re = pcre_compile(argv[1], 0, &error_message, &error_offset, NULL);
    assert(re != NULL);

    const char *bod = _binary_zipcode_csv_start;  // beginning of data
    const char *eod = _binary_zipcode_csv_end;  // end of data
    const char *bol = bod;  // beginning of line
    while (bol < eod) {
        const char *eol = strchr(bol, '\n');  // end of line
        if (eol == NULL) {
            eol = eod;
        }

        int ovector[3];
        int status = pcre_exec(re, NULL, bol, eol - bol, 0, 0, ovector, 3);
        if (status > 0) {
            printf("%.*s\n",  eol - bol, bol);
        }
        bol = eol + 1;
    }
    return 0;
}

後はこのファイルをコンパイルして zipcode.o とリンクすれば完成です。

% gcc -c zipgrep.c
% gcc -o zipgrep zipgrep.o zipcode.o -lpcre

zipgrep の実行結果は次のようになります。

% ./zipgrep 'ハック'
02402,"03925","0392544","アオモリケン","カミキタグンシチノヘマチ","ハックリタイ","青森県","上北郡七戸町","八栗平",0,0,0,0,0,0

たまたまハックで検索したら「ハックリタイ」といういい感じの地名が見つかりました。

まとめ

今回は objcopy を用いて実行ファイルにデータを埋め込む方法を取り上げました。今回のような普通のデータだけでなく、自分自身のソースコードや別のプログラムのバイナリといった変なものを埋め込んで遊ぶのもおもしろいと思います。

ところで、手元の環境 (Xeon 2.8 GHz + Debian GNU/Linux sarge + GCC 3.3.5) で上と同じ郵便番号データを C の文字列に変換してコンパイルところ、34秒かかりました。時間はかかったものの音をあげずにコンパイルできて感心しました。