Konturからパフォーマンスタスクが既にありましたが 、次はJBreak 2018 Javaカンファレンス(別名「Excel from siell」)のハードコアタスクを紹介しました。
タスクは元の定式化で与えられ、各タスクにはいくつかの正解があります。各タスクに対して、ネタバレの下で解決策が与えられます。
タスク1
同僚はJava言語仕様を読み、次のことを書きました。
void playWithRef() { Object obj = new Object(); WeakReference<Object> ref = new WeakReference<>(obj); System.out.println(ref.get() != null); System.gc(); System.out.println(ref.get() != null); }
そして、あなたにすくい上げるために:実行のどんな結果が可能ですか?
- A :false、false
- B :false、true
- C :true、false
- D :true、true
正解は、 A 、 C 、 Dです。
変数obj
のスコープはメソッド全体であり、寿命のスコープはWeakReference
コンストラクターを終了した後(実際には、コンストラクターの内部で-少し前でも)終了します。 そして、 GCがこのオブジェクトを破壊できるかどうかに影響を与えるのは人生の領域です。
ただし、便利な場合、 VMは変数の寿命を延ばすことができます。 たとえば、HotSpotインタープリターは、変数が表示されている限り生存していることをGCに伝えます(これは、デバッガーで確認できることです)。 つまり、HotSpot VMで追加オプションなしで(または明示的な-Xint
)サンプルを実行すると、オプションDが簡単に実現されます。
Cの結果は、多くのコンパイラ(たとえば、HotSpot C1 / C2、Excelsior JET JIT&AOTなど)で実現されます。 コンパイラーは、 obj
変数が使用されておらず、最初のget()
呼び出しによってGCがオブジェクトを破棄することを何も妨げないことを計算できるほどスマートです。 ただし、ほとんどの場合、GCにはSystem.gc()
明示的な呼び出しのみが付属します。この動作は、 -Xcomp
するHotSpot VMまたは任意のモードのExcelsior JETで現れます。
たとえば、 WeakReference
コンストラクターの実行の最後にGCが到着した場合、オプションAは理論的に達成可能です。
タスクはJDK 8コードのバグに基づいており、メソッドの引数は不正確にWeakReference
保存され、メソッドの実行中に死にました。 これについては、テクニカルブログに個別の詳細な投稿があります。
タスク2
邪悪なハッカーが元のJavaファイルを削除し、クラスファイルの断片をシャッフルしました。
A: 0700 0401 0001 4300 2000 0300 0100 0000 B: 0000 0000 00 C: 6a61 7661 2f6c 616e 672f 4f62 6a65 6374 D: cafe babe 0000 0031 0005 0700 0201 0010
検証可能なクラスファイルを取得できるように、それらを再配置します。
正解は、 D 、 C 、 A 、 Bです。
このタスクはより独創的ですが、それでも新しいことを教えます。
クラスファイルが4バイトのヘッダー0xCAFEBABE
で始まることは広く知られています。つまり、 Dが確実に最初になります。 常識は、短いピースBが最後に来ることを教えてくれます-それは尾です。
次に、クラスファイルにはConstantPoolがあり、2バイトの長さとUTF-8でエンコードされた実際の文字列で構成される文字列定数があります。 UTF-8のように見える唯一のピースはCピースです-これはjava/lang/Object
文字列(クラスのスーパークラスへの参照)を表すUTF-8です。 これは、バイト0x0010
(文字列の長さは16)の前にある必要があり、唯一の適切なオプションはDである 、つまりCが2番目であることを意味します。
あるいは、 Bの最後の行全体がゼロで構成されていることに気付くことができます。つまり、最後から2番目の行はゼロで終わるはずです。つまり、 Aです!
class C minor version: 0 major version: 49 flags: ACC_SUPER Constant pool: #1 = Class #2 // java/lang/Object #2 = Utf8 java/lang/Object #3 = Class #4 // C #4 = Utf8 C { }
タスク3
Graalについての別の話を聞いて、インスピレーションでJVMコンパイラインターフェイスを見て、独自のJavaコンパイラを書くことにしました! そして、メソッドのx86_64コードを生成することから始めることにしました。
static boolean invert(boolean x) { return !x; }
そのようなメソッドに対してどのような生成コードが正しいでしょうか?
凡例:Intel構文が使用され、呼び出し規約は、引数がrcx
にあり、 rax
がrax
です。
A: test ecx, ecx jnz True mov eax, 1 ret True: mov eax, 0 ret B: xor eax, eax test ecx, ecx jnz End add eax, 1 End: ret C: mov eax, 1 sub eax, ecx ret D: mov eax, ecx xor eax, 1 ret
正解はA 、 Bです。
アセンブラーのリストは、Java会議で頻繁に見ることができますが、Intel x86コマンドシステムに精通していない場合、同等のCコードを以下に示します。
A: res = (arg == 0) ? 1 : 0; B: res = 0; if (arg == 0) res += 1; C: res = 1; res -= arg; D: res = arg; res ^= 1;
実際、これらのすべての反転アルゴリズムは正しく機能しますが、入力引数は通常の論理値0
および1
取ります。
その後、楽しみが始まります。 検証者の観点から見ると、すべての短整数型( boolean
、 byte
、 char
、 short
)はint
と同等です。 さらに 、 boolean
固有のバイトコード命令はまったく存在しません。 たとえば、調査中のメソッドのバイトコード命令は次のとおりです。
public static boolean invert(boolean); 0: iload_0 1: ifne 8 4: iconst_1 5: goto 9 8: iconst_0 9: ireturn
したがって、 boolean
を受け入れるメソッドはint
で動作する準備ができている必要があり、ゼロ以外の値はすべてtrue
として扱われtrue
。 この場合、「最適化された」バリアントCおよびDは、誤ってC(2) = -1
およびD(2) = 3
として動作し始め、より単純なAおよびBは引き続き動作しますA(2) = B(2) = 0
。
これらの微妙さを説明するには、バイトコードを操作する必要があります。 GitHubの例では 、0、1、2、3、-1の数値がinvert
メソッドに渡されて結果が出力され、その後にprintln(boolean)
およびprintln(int)
呼び出されます。
奇妙な事実:JDK 8ではHotSpot C2コンパイラーがバージョンDを生成し、JDK 9では生成テンプレートがより正しいものに変更されました。
JDK 8では、テンプレートDが明確に表示され、誤った結果が表示されます。
$ jdk8/bin/java -Xcomp -Xbatch -XX:-TieredCompilation -XX:CompileCommand=print,Inverter.invert -XX:+UnlockDiagnosticVMOptions -XX:PrintAssemblyOptions=intel BooleanHell ... Compiled method (c2) 1216 533 Inverter::invert (10 bytes) ... # {method} {0x0000000012600d08} 'invert' '(Z)Z' in 'Inverter' # parm0: rdx = boolean # [sp+0x20] (sp of caller) 0x00000000057d7ac0: sub rsp,0x18 0x00000000057d7ac7: mov QWORD PTR [rsp+0x10],rbp ;*synchronization entry ; - Inverter::invert@-1 (line 3) 0x00000000057d7acc: mov eax,edx 0x00000000057d7ace: xor eax,0x1 ;*ireturn ; - Inverter::invert@9 (line 3) 0x00000000057d7ad1: add rsp,0x10 0x00000000057d7ad5: pop rbp 0x00000000057d7ad6: test DWORD PTR [rip+0xfffffffffdf58524],eax # 0x0000000003730000 ; {poll_return} 0x00000000057d7adc: ret ... false (0) -> true (1) true (1) -> false (0) true (2) -> true (3) true (3) -> true (2) true (-1) -> true (-2)
JDK 9はboolean
値の正規化を改善しました。入力引数が範囲{0、1}( test
およびsetne
)に削減され、結果が正しくなりました。
$ jdk9/bin/java -Xcomp -Xbatch -XX:-TieredCompilation -XX:CompileCommand=print,Inverter.invert -XX:+UnlockDiagnosticVMOptions -XX:PrintAssemblyOptions=intel BooleanHell ... Compiled method (c2) 4702 1496 Inverter::invert (10 bytes) ... # {method} {0x000001fa974d2dc0} 'invert' '(Z)Z' in 'Inverter' # {method} {0x000001fa974d2dc0} 'invert' '(Z)Z' in 'Inverter' # parm0: rdx = boolean # [sp+0x20] (sp of caller) 0x000001fafcb57720: sub rsp,0x18 0x000001fafcb57727: mov QWORD PTR [rsp+0x10],rbp ;*synchronization entry ; - Inverter::invert@-1 (line 3) 0x000001fafcb5772c: test edx,edx 0x000001fafcb5772e: setne al 0x000001fafcb57731: movzx eax,al 0x000001fafcb57734: xor eax,0x1 ;*ireturn {reexecute=0 rethrow=0 return_oop=0} ; - Inverter::invert@9 (line 3) 0x000001fafcb57737: add rsp,0x10 0x000001fafcb5773b: pop rbp 0x000001fafcb5773c: test DWORD PTR [rip+0xfffffffffdf688be],eax # 0x000001fafaac0000 ; {poll_return} 0x000001fafcb57742: ret ... false (0) -> true (1) true (1) -> false (0) true (2) -> false (0) true (3) -> false (0) true (-1) -> false (0)
タスク4
突然、このメソッドの呼び出しが次のことにつながることに非常に興味があることに気付きました。
void guessWhat(Iterable<?> x) { System.out.println(x.getClass()); }
- A :
class java.util.ArrayList
- B :
null
- C :
interface java.lang.Iterable
- D :
class java.lang.Integer
正解はA 、 Dです。
Object.getClass()
常にゼロ以外のクラスを返し、インターフェース型のインスタンスはないため、バリアントBとCは不可能です。 オプションAは簡単に実装できます: guessWhat(new ArrayList<Object>())
。
ただし、オプションDは実現可能ですIterable
はIterable
インターフェイスを実装しませんが、それでも、そのメソッドのインスタンスをこのメソッドにIterable
ことができます。 答えは、典型的なJava言語システムの厳密さが再びJVM検証型システムの弱点に陥ったということです。すべての参照型は 、すべてのインターフェースと割り当て互換です。 つまり、インターフェイスタイプが期待されるほとんどすべての場所(パラメーター、戻り値、フィールドを含む)、任意の参照値(つまり、任意のクラスと配列)を渡すことができます。
この効果は 、バイトコードを操作するか、クラスファイルを部分的に再コンパイルすることで実証できます。
タスク5
もう一度javacの不可in性を信じて、あなたは実験することにしました:
class C { private boolean getBoolean() { return false; } } interface I { default boolean getBoolean() { return true; } } class D extends C implements I {} public class Test { public static void main(String[] a) { foo(new D()); } public static void foo(I i) { System.out.println(i.getBoolean()); } }
Test
クラスをコンパイルして実行しようとするとどうなりますか?
- A :未コンパイル
- B :
java.lang.IllegalAccessError
スローする - C :
true
- D :
false
印刷されます。
正解はBです。
多くの人は、 IllegalAccessError
が部分的な再コンパイルまたは難読化でやり過ぎている人の運命だと信じています。 そのため、 ProGuardが難読化中に 2つの異なるメソッド(1つはプライベート、もう1つはデフォルト)に同じ名前を付け、結果のアプリケーションはIllegalAccessError
をスローし始めました。
ただし、2つのそのようなメソッドがソースコードですぐに同じ名前を持つ場合、 javac
警告なしでそれらをコンパイルし、 IllegalAccessError
も実行時にスローされることが判明しました。
このJVMの動作は、 invokeinterface
ステートメントのターゲットメソッドがどのようにinvokeinterface
されるかによって説明されます。 仕様によれば、クラスとすべてのスーパークラスのインスタンスメソッドが最初に調べられ、次に適切なデフォルトメソッドがスーパーインターフェース間で検索されますが、見つかったメソッドのプライバシーはプロセス全体の完了後にのみチェックされます。
したがって、検索は、スーパークラスC
のプライベートgetBoolean
メソッドで終了しますI
は、スーパーインターフェースI
からデフォルトのgetBoolean
メソッドを見つける方法をgetBoolean
ますI
その後、 IllegalAccessError
すでに論理的にスローされます。
興味深いことに、これは Java 11で変更され、検索プロセスでプライベートメソッドをスキップする予定です。
タスク6
突然、コンパイルされたJavaアプリケーションのネイティブコードをデバッグすることになります。 あなたはソースを持っていませんが、問題のあるメソッドをすでに見つけています、ここにあります:
1: lea rax, [rel _Test_foo] 2: push rax 3: mov eax, dword [rcx+0FH] 4: idiv dword [rdx+0FH] 5: mov rbx, qword [rel _Test_array] 6: mov ebx, dword [rbx+3BH] 7: add eax, ebx 8: ret 8
このメソッドを実行すると、さまざまなタイプのJava例外がスローされる可能性があると思われました。 どの指示が非難される可能性があるのかを理解する(番号を示す)のはまだですか?
-
StackOverflowError
:_________ -
NullPointerException
:_________ -
ArithmeticException
:_________ -
IndexOutOfBoundsException
:_________
正解は次のとおりです。
-
StackOverflowError
:2 -
NullPointerException
:NullPointerException
-
ArithmeticException
:4 -
IndexOutOfBoundsException
:なし
コンパイラは多くの方法で例外チェックを生成できます。 たとえば、オブジェクトのフィールドにアクセスする前に、 null
に対するオブジェクトの明示的なチェックを生成できます。失敗した場合は例外がスローされます。 ただし、このような明示的なチェックは、パフォーマンスとコードサイズに悪影響を及ぼします。 したがって、コンパイラーは暗黙的なチェックを実行しようとします:ポインター逆参照コードのみが生成されます。これは、nullポインターの場合、JVMが対応するJava例外としてキャッチ、認識、再スローするハードウェア例外を引き起こします。
この問題では、このような暗黙の例外を引き起こす可能性のある命令を見つけることが必要でした。
StackOverflowError
は、許可された範囲外の次のスタックスロットを読み書きしようとすると発生します。 これは、 push rax
命令で発生する可能性があります。
暗黙のArithmeticException
をidiv dword [rdx+0FH]
候補は1つだけです。整数除算命令idiv dword [rdx+0FH]
です。 参照解除された値がゼロの場合、ゼロによるハードウェア除算が行われ、その後にArithmeticException
続きます。
NullPointerException
をスローできる暗黙のチェックは、Javaコードで非常に一般的です。 それらを見つけるには、何かが間接参照されているすべての場所を考慮してください。 命令mov rbx, qword [rel _Test_array]
は相対アドレスの静的データを逆参照するため、エラーにつながることはありません。 しかし、命令mov eax, dword [rcx+0FH]
、 idiv dword [rdx+0FH]
、 mov ebx, dword [rbx+3BH]
は、メソッドパラメーターを逆参照し、静的データを読み取ります。つまり、 NullPointerException
をスローできます。
興味深いことに、 idiv dword [rdx+0FH]
は、一度に2つの暗黙的なチェックが含まれています。これにより、多くのJVM問題が発生することがあります 。
IndexOutOfBoundsException
暗黙的なチェックは、配列の要素をアドレス指定する命令で行う必要があります。 ヒントは、特定の_Test_array
をレジスターに読み取り、命令5
および6
それを逆参照することです。 ただし、配列要素へのアクセスを生成するこのパターンでは、範囲外のインデックスは単純に配列に隣接するヒープ内のメモリにアクセスするため、ハードウェア例外は発生しません。 したがって、ほとんどのプロセッサアーキテクチャでは、 IndexOutOfBoundsException
チェックIndexOutOfBoundsException
明示的に生成されます。 ただし、まれに、コンパイラはこのようなチェックがまったく必要ないことを証明できます。これがこの問題で発生します。 つまり、 IndexOutOfBoundsException
はここではまったくIndexOutOfBoundsException
できません。
タスク7
邪悪なハッカーがコンピューターを再びハッキングし、 Helper.class
hexエディターで編集して、 sayC
メソッドの終わりが検証不能になるようにしました。
public class Main { public static void main(String[] args) { System.out.print("A"); Helper.sayB(); Helper.sayC(); } } public class Helper { public static void sayB() { System.out.print("B"); } public static void sayC() { System.out.print("C"); // bad bytecode goes here } }
Main
クラスを開始するとどうなりますか?
- A :
VerifyError
投げる - B :「
A
」が出力され、VerifyError
スローされます - C :「
AB
」が出力され、VerifyError
スローされます - D :「
ABC
」が出力され、VerifyError
スローされます
正解はBです。
特定のクラスのバイトコードの検証は、このクラスのメソッドが実行される前に機能します。 Helper
クラスには、検証不可能なsayC
メソッドがあります。これは、クラス全体が完全に検証不可能であることを意味します。 したがって、オプションCとDは間違いです。実行はsayB
メソッドに到達しません。
次に、 VerifyError
スローされるポイントを理解する必要があります。 仕様により 、実行時にリンクが必要な場合、JVMに強力なリンク解決がある場合でも(クラスがロードされるとすべてのリンクがすぐに解決されます)、リンク解決エラーがスローされます。 このタスクでは、 Helper
への参照は「 A
」の出力後にのみ必要なので、正解はBです。
良い例は、説明されている動作を示しています。 手動操作によって取得された検証不可能なバイトコード。
Nikita Lipsky別名pjBoomsはJBreak 2018 (これまでのスライドのみ)およびJPoint 2017 ( ビデオがあります )でJavaバイトコード検証について詳しく説明しました。
おわりに
会議ではアセンブラーのタイプに怖がっていた人もいましたが、JVMの複雑さに没頭することを決めた人がかなりいました:私たちのスタンドにタスクを渡すすべての人に、バイトコードの複雑さ、検証者、暗黙の例外についてのエクスプレスコースを実施しました。 解決策を読んで、 新しいことを学んでほしい もしそうなら、私たちの目標は達成されました!