Javaクラス解析機を作る
ACC_ABSTRACTとか調べているうちに、Javaクラスの解析機を作りたくなってしまいました。
- 検索したらいっぱい見つかりそう。
- つーか、javapでいいじゃん!
とか思いつつ、でもそんなのかんけーねぇ!
ということで、すみやかに着手。The JavaTM Virtual Machine Specification Second Editionとshira - Byte Code (ByteCode) Formatを参考にしつつ、クラスファイルの先頭から以下のデータを読むところまで作ってみました。
名称 | サイズ | 説明 |
---|---|---|
magic | 4byte | クラスファイルを識別するためのマジックナンバー。0xCAFEBABEが入る。 |
minor_version | 2byte | クラスのマイナーバージョン |
major_version | 2byte | クラスのメジャーバージョン |
constant_pool_count | 2byte | constant_pool (クラス名メソッド名などの定数が格納されるテーブル)の数 |
constant_pool | 可変 | クラス名メソッド名などの定数が格納されるテーブル。constant_pool_count-1個のconstant_poolが並ぶ。「文字列」や「Class情報」など種類がいくつかあって、それぞれデータ形式/サイズが異なる。 |
access_flags | 2byte | クラス/インターフェイスのアクセス権限情報 |
this_class | 2byte | このクラスファイルで定義されているクラスを示すconstant_poolのid。 |
ここまでのコード
ここまでの解析コードは以下。
module Java # Java クラス class JavaClass CONSTANT_Class = 7 CONSTANT_Fieldref = 9 CONSTANT_Methodref = 10 CONSTANT_InterfaceMethodref = 11 CONSTANT_String = 8 CONSTANT_Integer = 3 CONSTANT_Float = 4 CONSTANT_Long = 5 CONSTANT_Double = 6 CONSTANT_NameAndType = 12 CONSTANT_Utf8 = 1 ACC_PUBLIC = 0x0001 ACC_FINAL = 0x0010 ACC_SUPER = 0x0020 ACC_INTERFACE = 0x0200 ACC_ABSTRACT = 0x0400 def initialize( class_data_io ) @io = class_data_io @constant_pool = [] read_magic read_version read_constant_pool read_access_flags read_class end # クラス名を取得 def class_name cp = @constant_pool[@this_class_id] raise "illegal class data." if cp == nil || cp[:tag] != CONSTANT_Class get_string_from_constants( cp[:name_index] ) end # クラスバージョンを文字列で取得する def version @major_version.to_s << "." << @minor_version.to_s end # クラスの文字列表現を得る def to_s <<STR // version #{version} class #{class_name} {} STR end private # magicを読む def read_magic magic = read( 4 ) raise "illegal class data." if magic != 0xCAFEBABE end # バージョンを読む def read_version @minor_version = read( 2 ) @major_version = read( 2 ) end # constant_pool を読む def read_constant_pool constant_pool_count = read(2) 1.upto(constant_pool_count-1) {|i| @constant_pool[i] = read_cp } end def read_cp tag = read( 1 ) cp = {:tag=>tag} case tag when CONSTANT_Class cp[:name_index] = read(2) when CONSTANT_Fieldref, CONSTANT_Methodref, CONSTANT_InterfaceMethodref cp[:class_index] = read(2) cp[:name_and_type_index] = read(2) when CONSTANT_String cp[:string_index] = read(2) when CONSTANT_Integer cp[:bytes] = read(4) when CONSTANT_Float cp[:bytes] = read(4) # TODO float化 when CONSTANT_Long cp[:high_bytes] = read(4) cp[:low_bytes] = read(4) when CONSTANT_Double cp[:high_bytes] = read(4) cp[:low_bytes] = read(4) # TODO double化 when CONSTANT_NameAndType cp[:name_index] = read(2) cp[:descriptor_index] = read(2) when CONSTANT_Utf8 length = read(2) cp[:bytes] = @io.read(length) else raise "unkown constant_pool_tag. tag =" << tag.to_s end return cp end # access_flagsを読む def read_access_flags @access_flags = read( 2 ) end # クラス情報を読む def read_class @this_class_id = read( 2 ) @super_class_id = read( 2 ) interfaces_count = read( 2 ) @interface_ids = [] interfaces_count.times{|i| @interface_ids << read( 2 ) } end # constant_poolからidに対応する文字列を取得する。 def get_string_from_constants( id ) cp = @constant_pool[id] raise "illegal constant pool id. id=" + id.to_s \ if cp == nil || cp[:tag] != CONSTANT_Utf8 return cp[:bytes] end # 指定したバイト数だけデータを読み込んで返す def read( size ) res = 0 size.times{|i| res = res << 8 | @io.getc } return res end end attr_reader :major_version, :minor_version end
動かしてみる
HelloWorldクラスを用意して、
public class HelloWorld { public static void main ( String[] args ) { System.out.println( "Hello World!" ); } }
解析!
open( "./HelloWorld.class", "r+b" ) {|io| jc = Java::JavaClass.new io puts jc.to_s }
実行結果です。
// version 49.0 class HelloWorld {}