読者です 読者をやめる 読者になる 読者になる

by shigemk2

当面は技術的なことしか書かない

Javaクラスファイル入門 #jclass_abc

勉強会

バイナリエディタなどの説明

そもそもクラスファイルはバイナリの集合

16進数

  • 0-9とA-Fで16を数える
  • バイナリは16進数で数える
  • 10は16進数だと16ですし、11は16進数だと17
  • 暗算で10進数と16進数を変換できる必要はないけど、数の大小は判別できるようにするといいし、9の次はAで、Fの次は10といった感じで次の数が何かは意識する
  • よく使う数はそのうち覚える(100→256)

2進数

  • 1と0で数字を数える
  • およそ進数というのは数字に-1した数までしか使わない(10進数なら0-9 2進数なら0-1)
  • 1を足すと繰り上がる 0 10 11 100 101 110 111

二進指数え法 - Wikipedia

計算

  • 2進数については、1が立っている桁の部分だけで足し算をしてみる。
  • 2進数から16進数の変換は4桁ずつで区切ってから計算することが可能だが、10進数と2進数の変換は面倒(逆に2進数と16進数は楽)

Pythonでの計算

>>> hex(9)
'0x9'
>>> bin(15)                                                                                                                                 '0b1111'
>>> hex(0b01101111111000001101111101011000)
'0x6fe0df58'
>>> bin(0xcafebabe)
'0b11001010111111101011101010111110'

なお、cafebabeはクラスファイルのシグネチャー

ファイルとバイナリの関係

  • コンピュータのデータはすべて16進数で表現できるけど、それを見るためのエディタがバイナリエディタ
class Hello {
    public static void main(String[] args) {
        System.out.println("hello");
    }
}
$ javac hello.java
$ hexedit Hello.class

1バイト=8ビット(00=0000 0000)

(viやEmacsだと最後に勝手に改行してくるので、0aが入って6バイトになっている)

6865 6c6c 6f0a

バイナリエディタの右側にアスキーコードが表示されていて、1文字が1バイトで表示されている

アドレス

  • ファイル先頭からのバイト数のこと。
  • バイナリエディタの左側に16進数で表示されている
  • 区切りがいいので、16バイトごとに区切られている

クラスファイルのヘッダの説明

概要

  • JVM仕様とはなにか
  • javapを使う
  • classファイルフォーマットの読み方概要

Javaのコンパイルと実行

.java→コンパイル→中間コード(.class)

  • JVM プラットフォーム依存部分を吸収して、どのプラットフォームでも動かすようにするやつ

ハードウェア→OS→JVM

JVM仕様

クラスファイルのデータ構造と出力である振る舞いのルールを定義するもので、このルールを満たす実装をJVMと呼ぶ(特定のJVMの仕様は定めない)

クラスファイルにさえなれば、元のコードの言語がGroovyだろうがScalaだろうがどっちでもいい

JVMの実装の詳細には踏み込まない

classファイルフォーマットとは

バイナリの並びを定義したもの

ツール

  • javap バイナリデータをJavaのクラスファイルとして解釈し、JVM仕様で定義されたデータ構造とマッピングを表示する
  • バイナリエディタ バイナリデータを見る。どんなバイナリでもいい

javap

  • Javaの標準ツール
  • OpenJDKベースのJDKに入っている
  • Javaクラスファイルを逆アセンブルするのに使う
  • コマンドライン上で実行

どんなときにjavapを使うか

  • 基本は使わない
  • 複数のコンパイラを使う状況で、コンパイラごとに出力される中間コードの差分を見たい
  • 処理系を作るときに期待する入力と実際の中間コードの差分を見たい

javapを使う前提条件

  • Javaがインストール
  • OpenJDKがインストールされている
  • JREだけではなくJDKが必要

javapを試す

Hello.javaを書いてコンパイル

class Hello {
    public static void main(String[] args) {
        System.out.println("hello");
    }
}
$ javac hello.java
$ javap Hello    
Compiled from "hello.java"
class Hello {
  Hello();
  public static void main(java.lang.String[]);
}

vオプションをつけてjavapを試す

javap -v hello

Classfile /home/shigemk2/junk/2014/12/Hello.class
  Last modified 2014/12/20; size 409 bytes
  MD5 checksum ddcaca2219a4a4a964e368d745b4fa33
  Compiled from "hello.java"
class Hello
  SourceFile: "hello.java"
  minor version: 0 // バージョン
  major version: 51
  flags: ACC_SUPER // アクセス許可やクラスの属性
Constant pool: // 配列みたいなもの
   #1 = Methodref          #6.#15         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            //  hello
   #4 = Methodref          #19.#20        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            //  Hello
   #6 = Class              #22            //  java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               hello.java
  #15 = NameAndType        #7:#8          //  "<init>":()V
  #16 = Class              #23            //  java/lang/System
  #17 = NameAndType        #24:#25        //  out:Ljava/io/PrintStream;
  #18 = Utf8               hello
  #19 = Class              #26            //  java/io/PrintStream
  #20 = NameAndType        #27:#28        //  println:(Ljava/lang/String;)V
  #21 = Utf8               Hello
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
{
  Hello(); // 自動生成されたデフォルトコンストラクタ
    flags: 
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return        
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return        
      LineNumberTable:
        line 3: 0
        line 4: 8
}

参考

JVM仕様の目次をニーモニックでページ検索してみる(アセンブラのニーモニックとは肌色が少し違う)

javapの便利オプション

  • -private アクセス制御子のコードを表示
  • -c 逆アセンブルされたコードを表示
  • -l 行とローカル変数テーブルを表示する
  • -classpath カレントディレクトリからクラスパスへの相対 絶対パスを指定

classファイルフォーマットの読み方

P63

classファイルとはコンパイルによって生成される中間ファイル(8ビットのバイトストリームから成り立っている)

JVM仕様書のChapter4を見よ

全体は擬似構造体で、u4 u2は符号なしバイト、magicは項目 constant_poolは可変長table interfaces_countは配列の要素数 interfacesは固定長配列。クラスやインターフェースと1体1で存在する

型(u1 u2 u4)

1バイト=8ビット

magic(P64)

Java classファイルを識別するための定数値

minor_version major_version(P64)

classファイルフォーマットのバージョン

constant_pool_count(P64)

constant_poolの数(1以上必須)

constant_pool(P65)

  • 可変長のtable
  • 中身を読まなければ全体が何バイトあるか分からない
  • 個々の要素はjavap

cp_info(P70)

コンスタントプールの構造をあらわしている

access_flags

  • アクセス許可やクラスの属性をあらわす
  • javapにも出力されていた項目

this_class super_class

constant_poolテーブルへのインデックス インデックス先はconstant_class_info構造体

interfaces_count interfaces

  • interface_count クラス、インターフェースの直接のsuper interfaceの数

fileds_count fields

あとは対称性のある情報になっている なお、methods_countは自動生成されたメソッドも含まれる

attributes_count attributes

構造体と、その数をあらわす。新たなattributeを定義できる。

まとめ

  • JVM仕様とは、入力のクラスファイルのデータ構造と出力の振る舞いのルールを定義する
  • javapはclassファイルのバイナリをJVM仕様とマッピング
  • classファイルフォーマットの表記ルールと読み進め方

クラスファイルを読む練習

constant_poolは配列でconstant_pool_countは配列の要素数を表すけども、実際の要素数は1少ない。countが00 1Dとすると、実際の数は28個。

構造体は擬似言語。

cp_info {
  u1 tag;
  u1 info[];
}

tagは種類を、infoは具体的な中身を指す

Constant pool:
   #1 = Methodref          #6.#15         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            //  hello
   #4 = Methodref          #19.#20        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            //  Hello
   #6 = Class              #22            //  java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               hello.java
  #15 = NameAndType        #7:#8          //  "<init>":()V
  #16 = Class              #23            //  java/lang/System
  #17 = NameAndType        #24:#25        //  out:Ljava/io/PrintStream;
  #18 = Utf8               hello
  #19 = Class              #26            //  java/io/PrintStream
  #20 = NameAndType        #27:#28        //  println:(Ljava/lang/String;)V
  #21 = Utf8               Hello
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V

このあたりがこすい

  #18 = Utf8               hello
  #19 = Class              #26            //  java/io/PrintStream
  #20 = NameAndType        #27:#28        //  println:(Ljava/lang/String;)V

定数構造

  • indexとつくやつは定数プール
  • classを呼ぶと自動的にObjectが生成されるんだねっていう推測
  • name_indexが指す先は文字列で実体は別に存在する
  • name_and_type_index 名前がinit型がV
Methodref:
  class_index
     Class

メソッド

  • code属性 P85
  • line_number_table P88

以上、ここまでが前置き 全体

バイトコード

本丸。このあと話す

補足

そういえばクラスファイルはビッグエンディアンでした

Javaバイトコードの説明

簡単なJavaバイトコードの読み方

内容

Javaバイトコードとは

Java仮想マシンが実際に実行する命令形式で、Java仮想マシン用アセンブリ言語のようなもの

バイトコードとコンパイラを学ぶことはJavaプログラマの助けになる

Javaバイトコード - Wikipedia

読みかた

$ javap Hello.class
$ javap -v Hello.class

実装していないけどコンストラクタ部分のクラスファイルもある Codeの下の部分がバイトコード

{
  Hello();
    flags: 
    Code:
      stack=1, locals=1, args_size=1
      // この下がバイトコード
         0: aload_0       
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      // ここまで
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
      // この下がバイトコード
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      // ここまで
      LineNumberTable:
        line 3: 0
        line 4: 8
}

main部分のバイトコード

chapter6に各命令の仕様がのっているから、それみるといいよ

getstatic     #2 // フィールド取得
ldc           #3 // 引数を渡す
invokevirtual #4 // println実行
return           // void返却

getstaticはb2で、なんか引数を与える

実行持データ領域とフレーム

  • 引数を渡すときの挙動
  • くわしいところは2.5 2.6をみてくれ

フレーム

メソッドごとに作成されて、そのメソッドのローカル変数やオペランドスタックをもつ

オペランドスタック

(機械語でいうところのパラメータ)

  • メソッドを呼び出すときに渡すパラメータの準備
  • 呼び出したメソッドから結果の受け取り

など

オペランドスタックに引数を渡しておいて、必要なときにニーモニックに引数を渡す

ローカル変数配列

メソッド起動時のパラメータ引き渡しに使われる配列。

読み方 2

  Succ();
    flags: 
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return        
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: iconst_1      
         1: istore_1      
         2: iconst_3      
         3: istore_2      
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: iload_1       
         8: iload_2       
         9: iadd          
        10: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        13: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        16: iload_1       
        17: invokestatic  #4                  // Method succ:(I)I
        20: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        23: return        
      LineNumberTable:
        line 3: 0
        line 4: 2
        line 5: 4
        line 6: 13
        line 7: 23

  private static int succ(int);
    flags: ACC_PRIVATE, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: iload_0       
         1: iconst_1      
         2: iadd          
         3: ireturn       
      LineNumberTable:
        line 10: 0
}

実際に眺めてみると

ローカル変数配列でいろいろごにょごにょして、オペランドスタックに積み込んで、オペランドスタックの中身を呼び出す仕組み。でもローカル変数配列の値を参照するメソッドもあったりする。

main関数

iconst_1
istore_1
iconst_3
istore_2
getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
iload_1
iload_2
iadd
invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
iload_1
invokestatic  #4                  // Method succ:(I)I
invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
return

succ関数

iload_0
iconst_1
iadd
ireturn

iconstは-1から使える

ローカル変数に入れるのがistore オペランドスタックにぶち込むのがiconst

バイトコードを読む練習

バイトコードの確認と逆コンパイル

バイトコードを真面目に読めば、Javaを復元できる

Classfile /home/shigemk2/junk/2014/12/jclass_abc/Test.class
  Last modified 2014/12/20; size 724 bytes
  MD5 checksum 8c8353723075b5cf6cd10b75545fd551
  Compiled from "Test.java"
class Test
  SourceFile: "Test.java"
  minor version: 0
  major version: 51
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #12.#24        //  java/lang/Object."<init>":()V
   #2 = Fieldref           #25.#26        //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = Class              #27            //  java/lang/StringBuilder
   #4 = Methodref          #3.#24         //  java/lang/StringBuilder."<init>":()V
   #5 = String             #28            //  
   #6 = Methodref          #3.#29         //  java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #7 = Methodref          #11.#30        //  Test.fib:(I)I
   #8 = Methodref          #3.#31         //  java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   #9 = Methodref          #3.#32         //  java/lang/StringBuilder.toString:()Ljava/lang/String;
  #10 = Methodref          #33.#34        //  java/io/PrintStream.println:(Ljava/lang/String;)V
  #11 = Class              #35            //  Test
  #12 = Class              #36            //  java/lang/Object
  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               main
  #18 = Utf8               ([Ljava/lang/String;)V
  #19 = Utf8               fib
  #20 = Utf8               (I)I
  #21 = Utf8               StackMapTable
  #22 = Utf8               SourceFile
  #23 = Utf8               Test.java
  #24 = NameAndType        #13:#14        //  "<init>":()V
  #25 = Class              #37            //  java/lang/System
  #26 = NameAndType        #38:#39        //  out:Ljava/io/PrintStream;
  #27 = Utf8               java/lang/StringBuilder
  #28 = Utf8               
  #29 = NameAndType        #40:#41        //  append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #30 = NameAndType        #19:#20        //  fib:(I)I
  #31 = NameAndType        #40:#42        //  append:(I)Ljava/lang/StringBuilder;
  #32 = NameAndType        #43:#44        //  toString:()Ljava/lang/String;
  #33 = Class              #45            //  java/io/PrintStream
  #34 = NameAndType        #46:#47        //  println:(Ljava/lang/String;)V
  #35 = Utf8               Test
  #36 = Utf8               java/lang/Object
  #37 = Utf8               java/lang/System
  #38 = Utf8               out
  #39 = Utf8               Ljava/io/PrintStream;
  #40 = Utf8               append
  #41 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #42 = Utf8               (I)Ljava/lang/StringBuilder;
  #43 = Utf8               toString
  #44 = Utf8               ()Ljava/lang/String;
  #45 = Utf8               java/io/PrintStream
  #46 = Utf8               println
  #47 = Utf8               (Ljava/lang/String;)V
{
  Test();
    flags: 
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return        
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #3                  // class java/lang/StringBuilder
         6: dup           
         7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        10: ldc           #5                  // String 
        12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: bipush        10
        17: invokestatic  #7                  // Method fib:(I)I
        20: invokevirtual #8                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        23: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        26: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        29: return        
      LineNumberTable:
        line 3: 0
        line 4: 29

  private static int fib(int);
    flags: ACC_PRIVATE, ACC_STATIC
    Code:
      stack=3, locals=1, args_size=1
         0: iload_0       
         1: iconst_1      
         2: if_icmpgt     7
         5: iload_0       
         6: ireturn       
         7: iload_0       
         8: iconst_2      
         9: isub          
        10: invokestatic  #7                  // Method fib:(I)I
        13: iload_0       
        14: iconst_1      
        15: isub          
        16: invokestatic  #7                  // Method fib:(I)I
        19: iadd          
        20: ireturn       
      LineNumberTable:
        line 7: 0
        line 8: 5
        line 10: 7
      StackMapTable: number_of_entries = 1
           frame_type = 7 /* same */

}