🛢️

C/C++の実行コードにバイナリを埋め込む

2024/05/04に公開

ニッチな記事を書くのが好きです

私が今いる Web 業界では、C/C++ 言語が使われることはほとんどなくなりました。ですが、むしろこの業界が特殊なだけで、C/C++ は様々なところで非常によく使われている……と思っています。

Zenn の読者は Web 業界の方が多いと勝手に思っているので、こんなネタが必要なのかは分からないんですが、いや、こういうのは自己満足です。書いてて楽しければいいんです! ということで書きました。そのついでで、いつか誰かの役に立つなら御の字というものです。

そういうところを踏まえたうえで「理由とか必要性とかどうでもいい、楽しそうだからやってみたい!」という同類の方はどうぞご覧ください😉

何のためにバイナリを埋め込むのか

プログラムが利用するファイルは、たいてい、必要なときに、必要なものを読み込みます。

ファイルを読み込むには、OSのシステムコールを呼び出します。C/C++ の場合、これは fopen()fread() などの関数を経由して行われます。
これらの呼び出しには(程度はさておき)時間がかかります。

しかしこれらのファイルをあらかじめ埋め込んでおけば、読み込まずに済みます。すると、そのための関数・システムコールを呼び出すことがなくなります。これが速度向上につながる、というわけです。

ただし! ただしですよ、これらの関数・OSのシステムコールの呼び出しにかかるコストは、現代では、大抵の場合ほとんど無視できる速度だと思います。(実際には計測してみないとわかりませんが)

どんなときに有効か

必ず読み込まれるファイルの場合

前節でも書きましたが、ファイルは、たいてい、必要なときに、必要なものを読み込みます。

しかし中には必ず読み込まれるものもあるはずです。そういうものを埋め込んでおくと無駄になりません。

また、起動時にはそういうものが結構あると思います。その場合、プログラムの初速を上げることができるかもしれません。

その他にも注意点がいくつかあります:

  • プログラムが置かれている媒体と、データ用の媒体が別になっていて、データ用の媒体の方が高速にアクセスできる場合、むしろ遅くなることがあります
  • あらかじめバイナリを埋め込んでおくと、プログラム自体が大きくなるので、起動時点のメモリ使用量が増えることになります
  • キャッシュがよく効く場合、ファイルを読み込むコストはさらに下がるでしょう
  • 埋め込む量が多くプログラムが大きくなると、プログラム自体がキャッシュに乗らなくなる可能性があります

などなど、いろいろな要素を考慮する必要があります。何かの状況でこの方法が有効かもしれない、と考えたときはきちんと計測してみることをお勧めします。

大量に小さなファイルを読み込む場合

繰り返しますが、関数・システムコールの呼び出しコストは、現代ではほとんど無視できるでしょう。

……が、それでもファイルが小さく、大量にある場合は別です。1万とか10万とかね[1]

読み込んだデータを格納するのに malloc() でヒープ領域を確保することが多いでしょうが、これにもコストがかかります。こちらはデータ量ではなく、データ数に比例するコストなので、小さなファイルを大量に読み込むときは、今でもそこそこコストがかかるはずです[2]。(たぶん)

塵も積もれば山となります。こういうところは節約したいものです[3]

原理(結論)

そんなに特別なことはしていません。埋め込みたいファイルを1バイトずつ読み込み、C/C++ の16進数表記(たとえば0x42など)に変えて、char 配列[4]を作ってヘッダファイル(.h)に書き込むというものです。

C/C++ のコードからこのヘッダファイルを include すれば、fopen(), fread() (場合によっては malloc())を使うことなく、char 配列にできる、というわけです。

コード

では、実際のコードをお見せして、説明したいと思います。

コード自体は、そんなに難しいことをしているわけではありません。fgetc() でファイルを読み込み、fprintf() で書き込むだけです。

https://github.com/keioni/blog-sample/blob/main/binary-to-array/binary-to-array.c

サクッと compile & link しましょう[5]
機種依存しないように作っているつもりですが、もし問題があれば教えてください。

あと、このプログラム自体は頻繁に使うものではないと思うので、これ自体の速度は気にしていません。PR お待ちしています😊

gcc -O3 binary-to-array.c -o binary-to-array

使い方

echo -n "Hello, World" | ./binary-to-array hello

hoge_data.h には以下のような内容が書き込まれます。なお、出力は見通しをよくするのと、1行あたりの文字数が多いとエディタに負荷がかかるので、10バイトごとに改行しています。

const unsigned char hello_array[] = {
	0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x57, 0x6f, 0x72,
	0x6c, 0x64,
};

const unsigned long hello_size = 12;

サンプル代わりの、ちょっとイカした使い方

指定したディレクトリにあるファイルをすべて char[] に変えて .h に書き込む、ということを一括で行うスクリプトを書いてみました。しかも、その 個々の .h ファイルをまとめた .h ファイルも作成します。これで1万でも10万でも楽勝ッス!

https://github.com/keioni/blog-sample/blob/main/binary-to-array/bulk-make.sh

余談

最初に書いたように、これはあくまで自己満足です。それでもまあ、実際、別に作っているプログラムで必要だったので、車輪の再発明だろうとは思いつつも手作りすることにしました。

やっぱり C/C++ のコードを書くのは楽しいものです。この、何かを無理矢理ゴリゴリいじっている感じがたまらんですね😊

最後に、本当に使いたい人向けの情報を:

  • Debian 系の Linux ディストリビューションを使っている方は hxtools というパッケージに bin2c というコマンドがあるようなのでこちらを検討してもいいのでは。(ただ私は使ったことがないので、よくわかりません)
  • あと、Adobe も作っていました。これもいいかもしれませんね。
    https://github.com/adobe/bin2c
脚注
  1. 普通はないですよね ↩︎

  2. メモリ確保のコストはOSの実装によって異なりますが、ヒープ管理用のデータ構造を操作する必要があるので、ファイル読み込みよりずっとコストがかかります。(それでも無視できる速度かもしれませんが……) ↩︎

  3. あくまで個人の感想です ↩︎

  4. char[] です。例えば [0x48, 0x22, 0x01] など ↩︎

  5. Makefile も用意できたらよかったんですが、あやふやな記憶しかないので書かないでおきます…… ↩︎

Discussion