ppbinについての覚え書き

昨年末にppbinというツールを作ったので、メモを残しておく。

GitHub - eel3/ppbin: Pretty-printer for binary file

ppbinとは何か?

ppbinは、指定した形式でバイナリファイルの16進数ダンプを吐き出すコマンドライン・アプリだ。

$ # test.binは44 byteのファイル。
$ # -n 20 --> 1行に20ワード出力する。
$ # -w 1  --> 1ワードの大きさは1バイト(1オクテット)。
$ ppbin -n 20 -w 1 test.bin
52 49 46 46 24 00 00 00 57 41 56 45 66 6D 74 20 10 00 00 00
01 00 02 00 44 AC 00 00 10 B1 02 00 04 00 10 00 64 61 74 61
00 00 00 00
$ _

――が、まあこの程度の機能だったらod(1)やhexdump(1)などを使えば済む話だ。

そもそも実現したかったのは「バイナリファイルの16進数ダンプを、C言語C++の配列の初期化構文としてソースファイルに埋め込み可能な形式で出力する」ことだ。ppbin自体は多少の汎用性を持たせたツールとして実装されていて、適切なオプションを付与することで、当初の目的を達成できるようになっている。

$ # -a 0x       --> 各ワードにプレフィックス「0x」を付与する。
$ # -b <header> --> ヘッダ行を付与する。
$ # -d ', '     --> ワード間の区切り文字として「, 」を使用する。
$ # -e <footer> --> フッタ行を付与する。
$ # -i 1        --> 各行を1文字インデントする。
$ # -l          --> 各ワードをリトルエンディアンとして解釈する。
$ # -n 4        --> 1行に4ワード出力する。
$ # -t          --> インデント文字としてスペースではなくタブを使用する。
$ # -w 4        --> 1ワードの大きさは4バイト(4オクテット)。
$ ppbin -a 0x \
>       -b 'static const uint32_t test_bin[] = {' \
>       -d ', ' \
>       -e '};' \
>       -i 1 \
>       -l \
>       -n 4 \
>       -t \
>       -w 4 \
>       test.bin
static const uint32_t test_bin[] = {
        0x46464952, 0x00000024, 0x45564157, 0x20746D66,
        0x00000010, 0x00020001, 0x0000AC44, 0x0002B110,
        0x00100004, 0x61746164, 0x00000000,
};
$ _

オプション次第では、こんな出力も可能だ。

$ # -a \\x --> 各ワードにプレフィックス「\x」を付与する。
$ # -d ''  --> ワード間の区切り文字として空文字を使用する(=各ワードをくっつけて表示)。
$ # -l     --> 各ワードをリトルエンディアンとして解釈する。
$ # -n 1   --> 1行に1ワード出力する。
$ # -p 1   --> 1ワードを1バイト(1オクテット)ずつ出力する。
$ # -w 4   --> 1ワードの大きさは4バイト(4オクテット)。
$ ppbin -a \\x -d '' -l -n 1 -p 1 -w 4 test.bin
\x46\x46\x49\x52
\x00\x00\x00\x24
\x45\x56\x41\x57
\x20\x74\x6D\x66
\x00\x00\x00\x10
\x00\x02\x00\x01
\x00\x00\xAC\x44
\x00\x02\xB1\x10
\x00\x10\x00\x04
\x61\x74\x61\x64
\x00\x00\x00\x00
$ _

つまりppbinはメカニズムの提供に徹したエンジンだ。お目当ての出力形式(ポリシー)ごとにシェルスクリプト・バッチファイルなどでラッピングして使うことを想定している。

なぜ自作したのか

先に書いたように、本来の目的は、バイナリファイルの中身をC言語C++の配列初期化構文に変換することだった。変換ツールを見繕うにあたり、以下の制約をクリアする必要があった。

  1. Windows上でも使える。
    • スクリプト言語で実装されている場合は、当該言語の処理系をWindowsに導入することが容易であること。
  2. 2バイトや4バイトごとの出力にも対応している。
  3. トルエンディアンにも対応している。
  4. 「4バイトのリトルエンディアンとして解釈したうえで、MSB側から2バイトずつ出力する」みたいな、ちょっと特殊な要求にも対応できる。
  5. 一度に複数のファイルを処理できる。
  6. 自動化のために、非対話式に使用することができる。

探し方が悪かったからか、上記の制約全てを満たすツールが見つからなかったため、自作に踏み切った。ppbinは、そのためのエンジンとして実装した。

Unix環境で動けば十分だったなら、od(1)やhexdump(1)でダンプしてsed(1)などで出力を加工するシェルスクリプトを書いたかもしれない。

今回はppbinをPythonで実装し、出力形式ごとにppbinを適切なオプション付きで呼び出すバッチファイルを実装した。想定ユーザは全員開発者なので、Pythonの処理系をWindowsに入れることぐらいは簡単にできるはずだ。

ちなみに、なぜPythonを使ったかというと、ppbinの前に作成・公開していた別のツールをPythonで実装していたからだ*1。つまり、想定ユーザの大半は、すでにPythonの処理系をインストール済みだったのだ。

ちょっと特殊な出力

ダンプする際、1バイトずつではなく2バイトないし4バイトずつ出力したいことがある。大抵のツールは、その要求に応えてくれる。ppbinも同様だ。

$ # -n 4 --> 1行に4ワード出力する。
$ # -w 4 --> 1ワードの大きさは4バイト(4オクテット)。
$ ppbin -n 4 -w 4 test.bin
52494646 24000000 57415645 666D7420
10000000 01000200 44AC0000 10B10200
04001000 64617461 00000000
$ _

ちなみにppbinは、出力が16進数形式に固定されている代償に、1ワードとして最大16バイト(128ビット)まで指定できる(といっても「1・2・4・8・16」の5種類しか選択できないのだが)。

複数バイトを1ワードとして指定できるようになると、今度は各ワードをリトルエンディアンとして解釈する機能が欲しくなる。ppbinは対応しているが、世の中の既存のツールは、この機能あたりから対応状況が若干怪しくなってくるようだ。

$ # -l   --> 各ワードをリトルエンディアンとして解釈する。
$ # -n 4 --> 1行に4ワード出力する。
$ # -w 4 --> 1ワードの大きさは4バイト(4オクテット)。
$ ppbin -l -n 4 -w 4 test.bin
46464952 00000024 45564157 20746D66
00000010 00020001 0000AC44 0002B110
00100004 61746164 00000000
$ _

では、例えば「4バイトのリトルエンディアンとして解釈したうえで、各ワードをMSB側から2バイトずつ出力する」みたいな、ちょっと特殊な要求はどうだろうか? ppbinには、この要求に応えるためのオプション-pが用意されている。

$ # -l   --> 各ワードをリトルエンディアンとして解釈する。
$ # -n 4 --> 1行に4ワード出力する。
$ # -p 2 --> 1ワードを2バイト(2オクテット)ずつ出力する。
$ # -w 4 --> 1ワードの大きさは4バイト(4オクテット)。
$ ppbin -l -n 4 -p 2 -w 4 test.bin
4646 4952 0000 0024 4556 4157 2074 6D66
0000 0010 0002 0001 0000 AC44 0002 B110
0010 0004 6174 6164 0000 0000
$ _

ファイル先頭の4バイトを順番に読むと「52 49 46 46」だ。これを4バイトのリトルエンディアンとして解釈すると「46 46 49 52」となり、MSB側から2バイトずつ分割すると「4646」と「4952」の2つとなる。

オプション-a-dは、実は-pによって分割された出力ごとに適用される。

$ # -a 0x   --> 各ワードにプレフィックス「0x」を付与する。
$ # -d ', ' --> ワード間の区切り文字として「, 」を使用する。
$ # -l      --> 各ワードをリトルエンディアンとして解釈する。
$ # -n 4    --> 1行に4ワード出力する。
$ # -p 2    --> 1ワードを2バイト(2オクテット)ずつ出力する。
$ # -w 4    --> 1ワードの大きさは4バイト(4オクテット)。
$ ppbin -a 0x -d ', ' -l -n 4 -p 2 -w 4 test.bin
0x4646, 0x4952, 0x0000, 0x0024, 0x4556, 0x4157, 0x2074, 0x6D66,
0x0000, 0x0010, 0x0002, 0x0001, 0x0000, 0xAC44, 0x0002, 0xB110,
0x0010, 0x0004, 0x6174, 0x6164, 0x0000, 0x0000,
$ _

そのため、安心してC言語C++の配列初期化構文への変換に使用することができる。

TODO

  • オプション-p-wに「1・2・4・8・16」の5種類以外の値も設定できるようにしたい。
  • 出力行の右端の空白文字を削除するオプション(rtrimやrstripのような機能)を追加したい。
    • 2022-12-08追記:commit:8390524にて、オプション-rとして機能追加した。
  • オプション-pによって分割された部分にオプション-a-dを適用するのを止めて、別途専用のプレフィックス/区切り文字を指定できるようにしたほうがよいかも?
    • 問題は、この機能にふさわしいオプション文字が思いつかないことだ……。
    • 2022-12-08追記:commit:2c6ec01の時点でオプション-A-Dが追加されている。オプション-a-dの仕様は変更していないため、オプション-pによって分割された各単位に適用される。その上で、オプション-A-Dはオプション-wで分割された単位に適用される。なお-Dの振る舞いは少し特殊で、-wで分割された単位の中の末尾の「-pで分割された単位」のデリミタを上書きするように作用する。
  • Python 3.xへの移行。
    • 2022-12-08追記:commit:5727486Python 3に移行した。なおcommit:a614907で型ヒントを導入したため、この追記を書いている時点ではPython 3.8以降のバージョンが必要となる。

*1:そのツールはRIFF WAVEファイルを取り扱うものだったので、標準ライブラリにWAVEファイル用のモジュールが用意されていたPythonで実装した。