ELM Home Page


2020.10.11

バイナリファイルの埋め込みかた


組み込みプログラミングでグラフィックデバイスを扱うようになると、フォントや画像などのファイルをそのままバイナリイメージでプログラムに埋め込みたいことが良くあります。バイナリデータの埋め込みには次に示すような手段があり、それぞれ特徴を持っているので最適な方法をとると良いでしょう。組み込みシステムでは必要性が高いわりに意外に知られていないようなので、32ビットへの誘いから抜き出して一つのページにまとめてみました。

  1. 定数テーブルを使う
  2. objcopyコマンドを使う
  3. .incbinディレクティブを使う
  4. Cソース中で.incbinを使う
  5. ROM書き込み時にマージする
  6. VC++プロジェクトでの埋め込み

定数テーブルを使う

誰もが最初に思いつくのは、バイナリファイルをCソースの定数配列形式に変換して埋め込む方法でしょう。

const uint8_t Image1[] = {
    0x20,0x6C,0x61,0x6E,0x67,0x3D,0x22,0x65,0x6E,0x22,0x3E,0x0A,0x3C,0x68,0x65,0x61,
    0x72,0x69,0x63,0x74,0x2E,0x64,0x74,0x64,0x22,0x3E,0x0A,0x3C,0x68,0x74,0x6D,0x6C,
    0x75,0x69,0x76,0x3D,0x22,0x43,0x6F,0x6E,0x74,0x65,0x6E,0x74,0x2D,0x54,0x79,0x70,

    /* データのサイズによっては数千行以上になることも!! */

    0x70,0x2D,0x65,0x71,0x75,0x69,0x76,0x3D,0x22,0x43,0x6F,0x6E,0x74,0x65,0x6E,0x74,
    0x74,0x22,0x20,0x74,0x69,0x74,0x6C,0x65,0x3D,0x22,0x53,0x69,0x74,0x65,0x20,0x54,
    0x2D,0x53,0x74,0x79,0x6C,0x65,0x2D,0x54,0x79,0x70,0x65,0x22,0x20,0x63,0x6F,0x6E
};

あまりスマートな方法とは言えませんが、数Kバイト程度までで変更のないデータなら最も手軽な方法と言えます。汎用性が高くC/C++に限らずどのような処理系でも同様に実現可能です。欠点はデータのサイズによってはソースファイルが膨大になってしまうこと、環境によって配列サイズに制限(intで表現可能なバイト数まで)があること、それと元のバイナリファイルに変更があったときは再変換の手間がかかることがあります。

objcopyコマンドを使う

このために用意されているものとしては、objcopyコマンド(実際の名前はツールチェーンによって異なる)でバイナリファイルをリンク可能なオブジェクトファイルに変換する方法があります。objcopyは多機能なファイルコンバータで、ビルドの過程でelfファイルからROMイメージ(hexファイルやベタバイナリ)を作成するのによく使われます。それ以外にも様々な変換が可能で、たとえばバイナリファイルfoo.binをオブジェクトファイルに変換する場合は、

objcopy -I binary -O elf32-little foo.bin foo.o

などとします。入力がベタのバイナリなので、それを-I binaryで明示する必要があります。デフォルトでは配置先セクションが.data(変数領域)になるので、実際には

objcopy -I binary -O elf32-little --rename-section .data=.rodata,alloc,load,readonly foo.bin foo.o

のようにして配置先セクションを.const.rodataなどのROM領域(リンカスクリプトに依存)に変更する必要があるでしょう。これらの処理はMakefileに記述してビルド時に必要に応じて行われるため、元ファイルに変更がある場合も再変換する手間が必要ありません。このため、不特定多数のバイナリファイルを扱うのに適しています。

組み込まれたデータの開始アドレス、終了アドレス+1、サイズは、objcopyがファイル名を元にしてそれぞれ_binary_foo_bin_start, _binary_foo_bin_end, _binary_foo_bin_sizeというシンボルを生成するので、Cコードからは次のようにして参照できます。

/* スタイル1 シンボルの宣言(型は任意) */
extern const char _binary_foo_bin_start[], _binary_foo_bin_size[];

const char *foo_ptr = _binary_foo_bin_start;     /* 開始アドレス */
size_t foo_size = (size_t)_binary_foo_bin_size;  /* サイズ */
/* スタイル2 シンボルの宣言(型は任意) */
extern const char _binary_foo_bin_start, _binary_foo_bin_size;

const char *foo_ptr = &_binary_foo_bin_start;    /* 開始アドレス */
size_t foo_size = (size_t)&_binary_foo_bin_size; /* サイズ */

.incbinディレクティブを使う

マニアックなものとしては、gasの.incbinディレクティブを使ってアセンブラソース中に埋め込むという方法があります。これは最も強力で、バイナリファイル中の一部を抜き出したり、それらを連結するなど自由自在な切り貼りが可能です。gas以外のアセンブラにも同様の機能を持つものがあるので、調べてみると良いでしょう。

.section ".rodata"                /* ROM(定数領域)に配置(AVRなら".progmem.data","a",@progbits) */

.balign 4                         /* 必要に応じてアライメント */
.global Foo123
Foo123:                           /* シンボルFoo123は埋め込まれたデータの先頭アドレスを示す */
.incbin "foo1.bin", <ofs>, <size> /* ofsはファイル先頭からのオフセット, sizeはバイト数 */
.incbin "foo2.bin", <ofs>         /* sizeを省略するとofs以降全部 */
.incbin "foo3.bin"                /* ofsも省略するとファイル全部 */

.global _sizeof_Foo123
.set _sizeof_Foo123, . - Foo123   /* 埋め込まれたサイズを定義 */

.text

埋め込まれたデータは、Cコードから次のようにして参照できます。

/* シンボルの宣言(型は任意) */
extern const char Foo123[], _sizeof_Foo123[];

const char *foo_ptr = Foo123;              /* 開始アドレス */
size_t foo_size = (size_t)_sizeof_Foo123;  /* サイズ */

Cソース中で.incbinを使う

これは、私が10年以上愛用している方法です。.incbinはアセンブラのディレクティブなのでCソースには記述できませんが、asmステートメントを使えば間接的にCソース中に記述することができます。この場合、次のようにマクロ定義しておくと手軽に利用できます。

/* Import a binary file */
#define IMPORT_BIN(sect, file, sym) asm (\
    ".section " #sect "\n"                  /* Change section */\
    ".balign 4\n"                           /* Word alignment */\
    ".global " #sym "\n"                    /* Export the object address */\
    #sym ":\n"                              /* Define the object label */\
    ".incbin \"" file "\"\n"                /* Import the file */\
    ".global _sizeof_" #sym "\n"            /* Export the object size */\
    ".set _sizeof_" #sym ", . - " #sym "\n" /* Define the object size */\
    ".balign 4\n"                           /* Word alignment */\
    ".section \".text\"\n")                 /* Restore section */

/* Import a part of binary file */
#define IMPORT_BIN_PART(sect, file, ofs, siz, sym) asm (\
    ".section " #sect "\n"\
    ".balign 4\n"\
    ".global " #sym "\n"\
    #sym ":\n"\
    ".incbin \"" file "\"," #ofs "," #siz "\n"\
    ".global _sizeof_" #sym "\n"\
    ".set _sizeof_" #sym ", . - " #sym "\n"\
    ".balign 4\n"\
    ".section \".text\"\n")

Cコード中ではこのマクロを次のように使います。gccではasmステートメントを関数の外にも置けるので、ソースファイル中の任意の場所に記述することができます。なお、ファイルパスの起点はそのCソースではなく、プロジェクトルートとなります。これは、asmステートメント内ではコンパイラの認識する(例えば#includeの)パスではなく、アセンブラの認識する(中間ファイル(*.s)中の)パスとなるからです。必要に応じてアセンブラに対してもインクルードディレクトリを指定してください。

/* foo.binを定数領域に配置しシンボルFooBinで参照できるようにする */
IMPORT_BIN(".rodata", "foo.bin", FooBin);

/* シンボルの宣言(型は任意) */
extern const char FooBin[], _sizeof_FooBin[];

    for (int i = 0; i < (int)_sizeof_FooBin; i++) {
        putchar(FooBin[i]);
    }

.incbinを使う方法では元ファイルの更新がビルド時に反映されないので、対象ソースを明示的に再コンパイルする必要があります。

ROM書き込み時にマージする

プログラムコードとの結合性の弱いモノリシックなデータブロック(たとえばファイルシステムイメージなど)は、個別のROMイメージとして管理されることがあります。この場合は、バイナリファイルをプログラムイメージ(HEXファイル)に直接連結するかROM書き込み時に同時指定するなどして最終的にROM上でマージされます。バイナリファイルのHEXファイルへの変換には、前出のobjcopyが使えます。データの配置先アドレスはプログラムコードと重ならないROM内のどこかに指定して管理する必要があります。

objcopy -I binary -O ihex --change-section-lma .data=0x20000 diskimage.bin diskimage.hex

連結されたデータは、Cコードから次のようにして参照できます。

/* データへのポインタ(型は任意) */
const uint8_t *diskimage = (const uint8_t*)0x20000;      /* 開始アドレス */

VC++プロジェクトでのデータ埋め込み

これは組み込みプロジェクトではなく、VC++プロジェクトにおけるデータの埋め込み方法です。あまり知られていないようなので、これもオマケとして入れておきました。VC++プロジェクトでは、ダイアログ、アイコン、カーソルなどのデータをリソースとして管理しているのはよく知られています。リソースには既定のクラスだけでなく、ユーザ定義の任意のデータも追加できます。

#include "resource.h"

    HRSRC hbin = FindResource(0, MAKEINTRESOURCE(IDR_BIN1), TEXT("BIN"));    // リソースデータ(IDR_BIN1)のハンドル
    const BYTE *bindata = (const BYTE*)LockResource(LoadResource(0, hbin));  // それをロード
    DWORD binsize = SizeofResource(0, hbin);                                 // それのサイズ

Sign