jarファイルのバイナリの読み方

「夏休みバイナリ入門 2012 第2日目」に参加してきました。

第1日目に引き続き、主催の七誌さん、スタッフの皆さん、どうもありがとうございました!

やったこと

学んだことをメモします。

  1. 2つのJavaファイルFoo.javaとBar.javaを作る
    • FooはBarに依存するように作る
  2. Foo.javaとBar.javaを1つのjarにする
  3. バイナリエディタで見る
    • アスキーダンプを眺めても、圧縮されているせいで、何が何やらほとんど分からない
  4. Foo.javaとBar.java無圧縮でjarにする
    • jarコマンドを実行する時、オプションに0(ゼロ)をつけると、無圧縮にできる
  5. バイナリエディタで見る
    • 読める、読めるぞ!

というわけで、圧縮版と無圧縮版のjarのバイナリを読みました(両者を比較するところまでは、自分は時間切れでできませんでした)。

その後、ハフマン符号化について七誌さんの発表を聴き、LZ77、LZSS、固定ハフマン符号について学びました。また、用意していただいた問題を解きました。

以下では、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ばかりで申し訳ないので、今後学んだことを使って何かやったら、日記にメモしていきたいと思います(せめて)。スタッフの皆さん、ありがとうございました。