jarファイルのバイナリの読み方
「夏休みバイナリ入門 2012 第2日目」に参加してきました。
第1日目に引き続き、主催の七誌さん、スタッフの皆さん、どうもありがとうございました!
- (第1日目)Javaのクラスファイルの読み方 http://d.hatena.ne.jp/torazuka/20120820/cafebabe
やったこと
学んだことをメモします。
- 2つのJavaファイルFoo.javaとBar.javaを作る
- FooはBarに依存するように作る
- Foo.javaとBar.javaを1つのjarにする
- バイナリエディタで見る
- アスキーダンプを眺めても、圧縮されているせいで、何が何やらほとんど分からない
- Foo.javaとBar.javaを無圧縮でjarにする
- jarコマンドを実行する時、オプションに0(ゼロ)をつけると、無圧縮にできる
- バイナリエディタで見る
- 読める、読めるぞ!
というわけで、圧縮版と無圧縮版のjarのバイナリを読みました(両者を比較するところまでは、自分は時間切れでできませんでした)。
その後、ハフマン符号化について七誌さんの発表を聴き、LZ77、LZSS、固定ハフマン符号について学びました。また、用意していただいた問題を解きました。
- (資料)Deflate http://www.slideshare.net/7shi/deflate
以下では、jar(zip)ファイルの読み方についてまとめます。なお、本文で引用した構造体は、七誌さんが作成された資料からの引用です。
jarファイルの読み方
jarを読むには、zipの仕様を使います。
# 今回は、主催の七誌さんが事前にZIPの仕様を読み解いて、Cの構造体風に整理してくださっていました。その神プリントを参照しながら、参加者はバイナリを読みました。
zipコンテナの中身は、大きく3つに分かれています。
- ZIP_HEADER
- ZIP_CENTRAL_HEADER
- ZIP_END_HEADER
まず、一番下のZIP_END_HEADERから読み始めます。なぜなら、それよりも上の構造の開始位置やサイズが、ZIP_END_HEADERの中に書いてあるからです。
ZIP_END_HEADERの開始位置を特定するには、ZIP_END_HEADERのシグネチャを探します。それは、PK56、つまり、「50 4B 05 06」というバイト列です。
次に、ZIP_END_HEADERの開始位置を見つけたら、その仕様にしたがって、バイナリを読んでいきます。
typedef struct { uint8_t signature[4]; uint16_t number_of_disks; uint16_t disk_numer_start; uint16_t number_of_disk_entries; uint16_t number_of_entries; uint32_t central_dir_size; uint32_t central_dir_offset; uint16_t file_comment_length; } ZIP_END_HEADER:
特に大事な要素は、次の3つです。
- number_of_entries
- そのzipに入っているファイルの個数
- central_dir_size
- ZIP_CENTRAL_HEADERのサイズ
- central_dir_offset
- ZIP_CENTRAL_HEADERの開始アドレス
PK34、PK12、PK56というのは、それぞれ、ZIP_HEADER、ZIP_CENTRAL_HEADER、ZIP_END_HEADERの先頭にあるシグネチャです。なお、PK34とPK12で始まるブロックは、zipに入っているファイル(とディレクトリ)の個数分だけ、それぞれのHEADERの中に存在します。PK56で始まるブロックは、1つだけです。
ZIP_END_HEADER、つまり、PK56で始まるブロックのcentral_dir_offsetに、「9B 03 00 00」という値が入っている場合、見るべきZIP_CENTRAL_HEADERは「00 00 03 9B」から始まっていることになります。ここでは、リトルエンディアンなので、値をバイト単位で逆から読みます。
central_dir_offsetの値でZIP_CENTRAL_HEADERの開始位置を特定したら、今度はその仕様にしたがって、バイナリを読んでいきます。
typedef struct { uint8_t signature[4]; uint16_t version_made; uint16_t version; uint16_t flags; uint16_t compression; uint16_t dos_time; uint16_t dos_date; uint32_t crc32; uint32_t compressed_size; uint32_t uncompressed_size; uint16_t file_name_length; uint16_t extra_field_length; uint16_t file_comment_length; uint16_t disk_number_start; uint16_t internal_file_attributes; uint32_t external_file_attributes; uint32_t position; } ZIP_CENTRAL_HEADER:
ここでも、大事な要素は3つです。
- compressed_size
- 圧縮したサイズ
- uncompressed_size
- 圧縮していないサイズ
- position
- ZIP_HEADERでの(そのファイルに関するブロックの)開始アドレス
PK12で始まるブロックのpositionに、「2B 00 00 00」という値が入っている場合、対応するZIP_HEADERは「00 00 2B」から始まっていることになります。
positionの値でZIP_HEADERの開始位置を特定したら、今度はその仕様にしたがって、バイナリを読んでいきます。
typedef struct { uint8_t signature[4]; uint16_t version; uint16_t flags; uint16_t compression; uint16_t dos_time; uint16_t dos_date; uint32_t crc32; uint32_t compressed_size; uint32_t uncompressed_size; uint16_t file_name_length; uint16_t extra_field_length; } ZIP_HEADER:
以下は、読んでいる最中に教えてもらったコツやポイントです。
読み始めのコツ
zipに入れる前のclassファイル(Foo.class、Bar.class)を、それぞれバイナリエディタで開き、眺めます。各classファイルを示す特徴的なバイト列を、jarファイルの中に確認することで、2つのclassファイルがたしかにjarに含まれているのを見ることができます。
jarで圧縮されたサイズの確認
ZIP_CENTRAL_HEADERの中に、compressed_sizeとuncompressed_sizeという要素があります。
無圧縮のjarでは、この2つの値が同一ですが、圧縮されたjarでは、compressed_sizeの方が小さくなっています。
ZIP_CENTRAL_HEADERの意義
zipに詰め込まれたファイル(つまり、jarに詰め込まれた2つのclassファイル)のデータが入っているのは、コンテナの一番上にあるZIP_HEADERです。ここに、例のCAFEBABEから始まるバイト列が、そのまま入っています。
では、ZIP_CENTRAL_HEADERは何のためにあるのかというと、ZIP_HEADERのインデックスの役割を持っているのだそうです。
ファイルの圧縮/解凍ツールによっては、解凍前に圧縮ファイルの中身を一覧する機能を持っています。そのような機能は、このインデックスを活用するそうです。
気になっている箇所
終了後、気になったのは、このあたりです。
- jarとzipのバイナリの違い
- 圧縮無しでzipにしたものと、jarのバイナリを比べる。
- MANIFESTファイルがないことくらいしか、まだ確認していない。
- jarの圧縮でDeflateが使われている様子
- まだバイナリで確認していない。
- 見て分かるのかしらん。
また追々……宿題ということで。
今回の勉強会では本当に色々と教えていただき、Takeばかりで申し訳ないので、今後学んだことを使って何かやったら、日記にメモしていきたいと思います(せめて)。スタッフの皆さん、ありがとうございました。