Upgrade to Pro — share decks privately, control downloads, hide ads and more …

javapを使ってクラスファイルを読んでみよう!

 javapを使ってクラスファイルを読んでみよう!

(JJUG CCC 2023 Spring 登壇資料)

みなさんは、Java のクラスファイルを読んだことがありますか。

そこには、Java を実行する上で必要なあらゆる情報が格納されています。
そう、Java について詳しく知ろうと思ったら、クラスファイルを読むことは避けて通れないのです。

「でも、クラスファイルってあくまで JVM が読むためのもので、人間には読めないんじゃないの?」
そんなことはありません!
Java のクラスファイルは、 javap というツールを使えば、わかりやすい形式で読むことができるのです。

今回は、そんな javap を使ってクラスファイルのどのような情報を知ることができるのか、どのように読んでいけばいいのかについて解説します。

このセッションで、あなたも JVM になれる!(かも)

YujiSoftware

June 04, 2023
Tweet

More Decks by YujiSoftware

Other Decks in Technology

Transcript

  1. 目次 • javap の概要 • javap の使い方 • 引数の指定方法 •

    オプションについて • –verbose オプションについて • 出力内容の解説 • まとめ
  2. javap って何? • JDK に付属しているツールの一つ • クラスファイルを逆アセンブル(人間に読みやすい形式で出力)するツール • できること •

    メソッドやフィールドの一覧の出力 • 定数プールの出力 • バイトコード(実行命令)の出力 • 属性の出力 etc… • できないこと • クラスファイルをもとのソースコードに戻す
  3. javap の位置づけ .java (ソースコード) .class (クラスファイル) JVM (Java仮想マシン) javac java

    javap コンパイル 実行 逆アセンブル結果 (テキストファイル)
  4. 補足:クラスファイルって何? • 0xCAFEBABE で始まるバイナリーファイル • バイトコード(実行命令)など、実行に必要な情報が含まれている • “クラス”ファイルという名前だが、クラス以外にも使う • インタフェース

    • モジュールの情報 etc… • バイナリなので… • コンピューターが扱いやすい • 人間には読みにくい • 読めなくはない https://bit.ly/3Ls1r70 • javap は、これを人間に読みやすいフォーマットに変換してくれる
  5. 使い方 • javap [options] classes... • classes の指定方法 • ファイル名

    • Foo.class • クラス名(完全限定名) • java.lang.Object • java/lang/Object • URL • jar:file:///path/log4j-api.jar!/org/apache/logging/log4j/simple/SimpleLogger.class • https://github.com/openjdk/jdk/raw/master/test/jdk/sun/misc/Hello.class • javac と同様に -cp (-classpath) の指定もできる • javap -cp log4j-api.jar org.apache.logging.log4j.simple.SimpleLogger
  6. 実行例 • シンプルな Hello world を用意 (com/example/Main.java) • これを javac

    でコンパイルして、javap で逆アセンブル package com.example; public class Main { public static void main(String[] args) { System.out.println("Hello world."); } } Compiled from "Main.java" public class com.example.Main { public com.example.Main(); public static void main(java.lang.String[]); } フィールドとメソッドの一覧が出力される
  7. よく使うオプション • -constants • static final 定数値を表示 • -private または

    -p • すべてのクラスとメンバーを表示 • デフォルトは、protected および public のみ • -verboseまたは-v • クラスの詳細情報を出力 • すべてのオプションは、ドキュメントまたはヘルプ(-h)を参照 • https://docs.oracle.com/javase/jp/20/docs/specs/man/javap.html • 記載のない隠しオプションがある • https://qiita.com/YujiSoftware/items/f734cf656e6c01195f92
  8. 実行例 (-verbose) • なんかいっぱい表示される! Classfile /home/yuji/sample/com/example/Main.class Last modified 2023/05/27; size

    426 bytes SHA-256 checksum 2887486397f85a098cd8908463d9499351497938089eafc72988a5cbaf4b55e0 Compiled from "Main.java" public class com.example.Main minor version: 0 major version: 64 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #21 // com/example/Main super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V … 全体:https://gist.github.com/YujiSoftware/9fef6bf6abd22e0cec6b32ea0d1151c8
  9. -verbose で表示される内容 • ファイルのプロパティ • パス、サイズ、日付、SHA-256ハッシュ • クラス定義 • バージョン、アクセスフラグ、クラス名、親クラス、etc...

    • 定数プール • フィールド情報 • メソッド情報 • 定義、バイトコード、変数テーブル、etc... • 属性 etc...
  10. -verbose で表示される内容 • ファイルのプロパティ • パス、サイズ、日付、SHA-256ハッシュ • クラス定義 • バージョン、アクセスフラグ、クラス名、親クラス、etc...

    • 定数プール • フィールド情報 • メソッド情報 • 定義、バイトコード、変数テーブル、etc... • 属性 etc...
  11. ファイルのプロパティ • クラスファイルのパス • 最終更新日 • SHA-256 ハッシュ • Java

    12 までは MD5 ハッシュ • Compiled from • コンパイルしたソースコードのファイル名(パスは含まない) • ファイルの中に書き込まれている属性の情報 Classfile /home/yuji/sample/com/example/Main.class Last modified 2023/05/27; size 426 bytes SHA-256 checksum 2887486397f85a098cd8908463d9499351497938089eafc72988a5cbaf4b55e0 Compiled from "Main.java" ディスク上の情報 (中身ではない)
  12. クラス定義 • アイテム : 値 という形式で出力される • // 以降は定数プールの値 •

    読みやすくなるように、javap がコメントとして出力してくれている public class Main minor version: 0 major version: 64 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #21 // com/example/Main super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1
  13. クラス定義:バージョン • 実行対象の Java のバージョン • コンパイル時に -target オプションで指定したバージョン •

    デフォルトは、コンパイルした Java のバージョン • 実行環境よりも新しいと、UnsupportedClassVersionError が発生 • Main has been compiled by a more recent version of the Java Runtime (class file version 64.0), this version of the Java Runtime only recognizes class file versions up to 61.0 • minor version • 基本的に "0" • コンパイル時に --enable-preview を指定した場合は "65535" • major version • Java 1.1 = "45" で、バージョンが上がるたびに1ずつ増える (Java 20 = "64") • 一覧 : https://javaalmanac.io/jdk/ minor version: 0 major version: 64
  14. クラス定義:アクセスフラグ • アクセス許可とプロパティの指定 flags: (0x0021) ACC_PUBLIC, ACC_SUPER 名称 値 解説

    ACC_PUBLIC 0x0001 公開(パッケージの外部からアクセスできる) ACC_FINAL 0x0010 継承不可 ACC_SUPER 0x0020 invokespecial 命令を実行した際に、スーパークラスメソッドを特別に扱う ※歴史的経緯によるもので、クラスには強制的に付与される ACC_INTERFACE 0x0200 インタフェース ACC_ABSTRACT 0x0400 インスタンス化できない ACC_SYNTHETIC 0x1000 ソースコードに存在しない(コンパイラが生成したクラス) ACC_ANNOTATION 0x2000 アノテーションインタフェース ACC_ENUM 0x4000 ENUMクラス ACC_MODULE 0x8000 モジュール
  15. クラス定義:クラス名 • クラス名 と 親クラス名 • 完全限定名の内部形式 • 歴史的な理由により、. (ドット)を

    / (スラッシュ)に置き換えている • #番号は定数プール(後述)の番号 • コメントがその定数値 this_class: #21 // com/example/Main super_class: #2 // java/lang/Object
  16. クラス定義:その他 • インタフェースの個数 • フィールドの数 • メソッドの個数 • 属性の個数 •

    属性? • ソースコードのアノテーション(@Deprecated など)ではない • (詳細は次のページ) interfaces: 0, fields: 0, methods: 2, attributes: 1
  17. クラスファイルの構造 • 定義 + 属性(0個以上)の入れ子になっている フィールド •定義 • アクセスフラグ •

    名前 • ディスクリプター(型) •属性(0個以上) • シグネチャ(ジェネリクス型情報) • 実行時アノテーション ... etc メソッド •定義 • アクセスフラグ • 名前 • ディスクリプター(型) •属性(0個以上) • コード • シグネチャ(ジェネリクス型情報)… etc クラス(=クラスファイル) • 定義 • バージョン • 定数プール • アクセスフラグ • … • フィールド(0個以上) • メソッド(0個以上) • 属性(0個以上) • ソースファイル名 • 内部クラス ...etc コード •定義 • 最大スタック • 最大ローカル • バイトコード • 例外テーブル • 属性(0個以上) • 行番号テーブル… etc
  18. 定数プールとは • クラスで使用する定数の集合 • 文字列 • 数値 • メソッド定義 •

    etc... • #1 から順に、番号が振られている • #0 は null を意味する Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> ...
  19. 定数プールのタグ • それぞれの定数には、内容を表す「タグ」がついている 分類 タグ 文字列 Utf8 数値定数 Integer, Float,

    Long, Double 文字列定数 String クラス Class 定義 Fieldref, Methodref, InterfaceMethodref 型 NameAndType 動的呼び出し MethodHandle, MethodType, InvokeDynamic, Dynamic その他 Module, Package
  20. • Invokevirtual(インスタンスメソッド呼び出し)のパラメータ • パラメータで指定されたメソッドを呼び出す 定数プールの使用例 #15 = Methodref #16.#17 #16

    = Class #18 #17 = NameAndType #19:#20 #18 = Utf8 java/io/PrintStream #19 = Utf8 println #20 = Utf8 (Ljava/lang/String;)V Code: stack=2, locals=1, args_size=1 (中略) 5: invokevirtual #15 8: return
  21. • Methodref(メソッド定義) • クラス定義(#16)、名前とディスクリプタ(#17)を参照 • Class(クラス定義) • クラス名(#18)を参照 • NameAndType(名前とディスクリプタ)

    • 名前(#19)、ディスクリプタ(#20)を参照 • つまり #15 は、 java.io.PrintStream クラスの void println(java.langString) メソッド 定数プールの使用例 #15 = Methodref #16.#17 #16 = Class #18 #17 = NameAndType #19:#20 #18 = Utf8 java/io/PrintStream #19 = Utf8 println #20 = Utf8 (Ljava/lang/String;)V
  22. -verbose で表示される内容(再掲) • ファイルのプロパティ • パス、サイズ、日付、SHA-256ハッシュ • クラス定義 • バージョン、アクセスフラグ、クラス名、親クラス、etc...

    • 定数プール • フィールド情報 • メソッド情報 • 定義、バイトコード、変数テーブル、etc... • 属性 etc... フィールド情報は解説をスキップ (メソッド情報とほぼ同じなので)
  23. メソッド情報 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC,

    ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #13 // String Hello world. 5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 3: 0 line 4: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String; コード属性 行番号テーブル属性 ローカル変数テーブル属性 メソッド定義
  24. メソッド定義 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC,

    ACC_STATIC • 定義 • 内部表現(ディスクリプタ、アクセスフラグ、など)を分かりやすく出力し たもの • とりあえず、ここだけ見ればOK • ディスクリプタ • フィールドやメソッドの型を表現した文字列 • メソッドの場合は、引数と戻り値の型を示している • アクセスフラグ • アクセス許可とメソッド属性を表現したフラグ
  25. メソッド情報:ディスクリプタ descriptor: ([Ljava/lang/String;)V • 引数と戻り値 • カッコ内が、引数 • 表記例 •

    void (String[] foo) • ([Ljava/lang/String;)V • byte (char c, long[][] map) • (C[[J)B FieldType 文字 型 意味 B byte 符号付きバイト C char 基本多言語面の Unicode 文字コード ポイント (UTF-16 エンコード) D double 倍精度浮動小数点数値 F float 単精度浮動小数点数地 I int 符号付き32​ビット整数 J long 符号付き64ビット整数 L ClassName; reference クラス<classname>のインスタンス S short 符号付き16ビット整数 V void (戻り値がない) Z boolean true または false [ reference 配列次元(1次元分)
  26. メソッド情報:アクセスフラグ flags: (0x0009) ACC_PUBLIC, ACC_STATIC フラグ名 値 意味 ACC_PUBLIC 0x0001

    public と宣言されている ACC_PRIVATE 0x0002 private と宣言されている ACC_PROTECTED 0x0004 protected と宣言されている ACC_STATIC 0x0008 static と宣言されている ACC_FINAL 0x0010 final と宣言されている(オーバーライドできない) ACC_SYNCHRONIZED 0x0020 synchronized と宣言されている(起動時はモニタ・ロックで囲まれる) ACC_BRIDGE 0x0040 ブリッジメソッド(コンパイラが生成した) ACC_VARARGS 0x0080 ... (可変長引数)と宣言されている ACC_NATIVE 0x0100 nativeと宣言されている(Java 以外の言語で実装されている) ACC_ABSTRACT 0x0400 abstract と宣言されている(実装が提供されていない) ACC_STRICT 0x0800 srictfp と宣言されている(浮動小数点が FP-strict である) ACC_SYNTHETIC 0x1000 合成メソッド(ソースコードに存在しない)
  27. メソッド情報:バイトコード Code: stack=2, locals=1, args_size=1 0: getstatic #7 // Field

    java/lang/System.out:Ljava/io/PrintStream; 3: ldc #13 // String Hello world. 5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return • stack : オペランド・スタックの最大の深さ • locals : 割り当てられるローカル変数の数 • ソースコード上のローカル変数の数とは必ずしも一致しない • インスタンスメソッドの場合、this で1つ消費 • long, double は2つ分を消費 • args_size : 引数の数
  28. メソッド情報:バイトコード Code: stack=2, locals=1, args_size=1 0: getstatic #7 // Field

    java/lang/System.out:Ljava/io/PrintStream; 3: ldc #13 // String Hello world. 5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return • ニーモニック(命令)とオペランドの組み合わせ • 先頭の数字はバイトコード内のオフセット • このコードの内容 • #7 の static フィールド(System.out)をスタックに積む • #13 の定数("Hello world.")をスタックに積む • #15 のメソッド( void PrintStream#println(String) )を呼び出す • 終了
  29. 補足:ニーモニック一覧 • 全113個 • わかりやすく読みやすい • Java 1.0 以降、ほとんど変化がない •

    Java 7 で invokedynamic が追加になっただけ • 詳しくは、JVM仕様を参照 • https://docs.oracle.com/javase/ specs/jvms/se20/html/jvms-6.html
  30. メソッド情報: 行番号テーブル、ローカル変数テーブル • LineNumberTable • バイトコードとソースコードの行数の対応表 • LocalVariableTable • ローカル変数のスコープと変数名の対応表

    • デバッグに必要な情報 • コンパイル時に -g:none を指定すると出力されない LineNumberTable: line 5: 0 line 6: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String;
  31. クラス属性 • SourceFile属性 • コンパイルしたソースコードのファイル名(パスは含まない) • プロパティの "Compiled From" はこの属性の情報と同じ

    • デバッグに必要な情報 • コンパイル時に -g:none を指定すると出力されない SourceFile: "Main.java"
  32. 主なクラスの属性 • InnerClasses • インナークラスの定義 • Signature • ジェネリクス型の定義 InnerClasses:

    private static #41= #22 of #26; // Inner=class Main$Inner of class Main Signature: #51 // Ljava/lang/Enum<Ljava/nio/file/AccessMode;>;
  33. 参考資料 • Java Virtual Machine Specification • Chapter 4. The

    class File Format • https://docs.oracle.com/javase/specs/jvms/se20/html/jvms-4.html • Java 仮想マシン仕様(第二版) • 上記の日本語訳 • すでに絶版 (´・ω・`) • 最新の仕様が載っていない • 2001年出版、Java 1.2 時代の仕様 • 基本的な構造は変わっていないので、 この本 + 上記のページを読むのがおすすめ
  34. 参考資料 • javap コマンドのドキュメント • https://docs.oracle.com/javase/jp/19/docs/specs/man/javap.html • javap のソースコード •

    https://github.com/openjdk/jdk/blob/master/src/jdk.jdeps/share/classes/co m/sun/tools/javap/Main.java • -verbose の出力については、ソースコードがドキュメント
  35. 補足 Q: javap –verbose の出力をパースして、ツールを作ると面白そう! A: やめたほうがいいです • javap の出力は構造化されていません

    • パースしづらい • バージョンによって変わる • 代わりに、クラスリーダーを使いましょう! • Object Web ASM の ClassReader • https://asm.ow2.io/javadoc/org/objectweb/asm/ClassReader.html • 標準APIも準備中 • JEP draft: Class-File API (Preview) https://openjdk.org/jeps/8280389
  36. 補足:便利なツール • Visual Studio Code に JVM Bytecode Viewer を

    入れておくと便利 • https://marketplace.visualstudio .com/items?itemName=mnxn.jv m-bytecode-viewer • クラスファイルを開いた際、自 動で javap してくれる • シンタックスハイライトしてく れる