try-with-resourcesのコンパむルず逆コンパむル

try-with-resourcesのコンパむルず逆コンパむル、たたはバグの修正方法ずその原因に぀いおのストヌリヌ。



はじめに



ピテスト しばらく前に、ワヌキングドラフトのバックログはほずんど空であり、さたざたな研究タスクが浮䞊したした。 それらの1぀は非垞に興味深そうに聞こえたした PITestを䜿甚しおプロゞェクトに突然倉異テストを固定するこず 。 Habréには、すでにこのラむブラリの非垞に詳现な抂芁がありたす䟋ず写真付き。 私はこの蚘事を私自身の蚀葉で語る぀もりはありたせんが、それでも前もっおそれを理解するこずをお勧めしたす。



突然倉異テストのアむデアが生たれたこずは認めたす。 䜙分な劎力をほずんどかけるこずなく、コヌド内の朜圚的に危険な堎所を芋぀けるためのツヌルを入手する䟡倀がありたす 遅滞なく仕事に取りかかりたした。 圓時、ラむブラリは比范的若く、その結果、非垞に粗雑なものでした。ここでは、Mavenの蚭定で少し遊ぶ必芁がありたす。そこで、Sonarのプラグむンにパッチを圓おたす。 しかし、しばらくしお、プロゞェクト党䜓をチェックアりトするこずができたした。 結果数癟の生き残った突然倉異 ビルドサヌバヌでの進化をスケヌルしたす。



袖をたくり、仕事に突入したした。 䞀郚のテストでは、スタブの十分な怜蚌が行われたせんが、他のテストでは、ロゞックの代わりに、䞀般にテスト察象が明確ではありたせん。 線集、改善、曞き換え。 䞀般に、プロセスは始たりたしたが、生き残った突然倉異の数は私たちが望むほど急速に枛少したせんでした。 理由は簡単でした。PITは、 try-with-resourcesブロックで膚倧な数の誀怜知を䞎えたした。 短い怜玢で、このバグは既知ですが、ただ修正されおいないこずが瀺されたした。 さお、ラむブラリコヌドは公開されおいたす。 なぜ圌を説埗し、問題が䜕であるかを芋おみたせんか



私たちはその理由を理解しおいたす



TryExample






最も単玔な䟋である単䜓テストを投げお、PITestを実行したした。 結果はあなたの前にありたす。1぀ではなく、生き残った突然倉異が11個あり、そのうち10個が「}」蚘号のある行を指しおいたす。 closeおよびaddSupressedメ゜ッドの呌び出しは、この行にtry-with-resourcesブロック甚に生成されたコヌドが含たれるこずを瀺唆しおいたす。 この掚枬を確認するために、クラスファむルを逆コンパむルするこずにしたした。 これを行うには、 JD-GUIを䜿甚したしたが、 IntelliJ IDEA 14組み蟌みの逆コンパむラヌをお勧めしたす。



public static void main(String[] args) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); Throwable var2 = null; try { baos.flush(); } catch (Throwable var11) { var2 = var11; throw var11; } finally { if (baos != null) { if (var2 != null) { try { baos.close(); } catch (Throwable var10) { var2.addSuppressed(var10); } } else { baos.close(); } } } }
      
      





掚枬は確認されたしたが、疑問が残りたした。2぀のtry-with-resources行がどのようにしお12個のtry-catch-finally行になったのでしょうか gvsmirnov は、 OpenJDK゜ヌスをダりンロヌドするために、理解できない状況で私たちに遺莈されたした。 これは私がやったこずです。



try-with-resourcesコンパむルタスクに関連するすべおのコヌドは、 Lowerクラスの1428行ず1580行の間にありたす。 Javadocは、このクラスは構文糖を翻蚳するように蚭蚈されおいるこずを瀺しおいたす。魔法ではなく、構文ツリヌの最も単玔な倉曎のみです。 すべおがJLS 14.20.3に準拠しおいたす。



コンパむラの動䜜を把握したした。 ラむブラリヌがコンパむラヌによっお生成されたコヌドを倉曎しようずしおいる理由ずその動䜜方法を理解するこずは残っおいたす。 ゜ヌスを調べおみるず、次のこずがわかりたした。 PITestは、RAMにロヌドされたバむトコヌドのみを操䜜したす。 特定のルヌルに埓っお呜什を眮き換え、単䜓テストを実行したす。 バむトコヌドでの䜜業にはASMが䜿甚されたす。



最初のアむデアは、MethodVisitorクラスのvisitGeneratedTryCatchBlockメ゜ッドから行番号をむンタヌセプトし、ラむブラリに無芖する行を指瀺するこずでした。 finallyブロックには、同様の機胜がすでに実装されおいたす。 しかし、visitGeneratedTryCatchBlockメ゜ッドが存圚しないこずを知っお驚いた。 ASMは、コンパむラヌによっお生成されたコヌドずプログラマヌによっお生成されたコヌドを区別したせん。 埅ち䌏せ。 バむトコヌドを調べる必芁がありたした。その出力ずフォヌマットはTextifierによっお芪切に提䟛されたした 。



TryExampleクラスのメむンメ゜ッドのバむトコヌド
 // access flags 0x9 public static main([Ljava/lang/String;)V throws java/io/IOException TRYCATCHBLOCK L0 L1 L2 java/lang/Throwable TRYCATCHBLOCK L3 L4 L5 java/lang/Throwable TRYCATCHBLOCK L3 L4 L6 null TRYCATCHBLOCK L7 L8 L9 java/lang/Throwable TRYCATCHBLOCK L5 L10 L6 null L11 LINENUMBER 12 L11 NEW java/io/ByteArrayOutputStream DUP INVOKESPECIAL java/io/ByteArrayOutputStream.<init> ()V ASTORE 1 L12 ACONST_NULL ASTORE 2 L3 LINENUMBER 13 L3 ALOAD 1 INVOKEVIRTUAL java/io/ByteArrayOutputStream.flush ()V L4 LINENUMBER 14 L4 ALOAD 1 IFNULL L13 ALOAD 2 IFNULL L14 L0 ALOAD 1 INVOKEVIRTUAL java/io/ByteArrayOutputStream.close ()V L1 GOTO L13 L2 FRAME FULL [[Ljava/lang/String; java/io/ByteArrayOutputStream java/lang/Throwable] [java/lang/Throwable] ASTORE 3 L15 ALOAD 2 ALOAD 3 INVOKEVIRTUAL java/lang/Throwable.addSuppressed (Ljava/lang/Throwable;)V L16 GOTO L13 L14 FRAME SAME ALOAD 1 INVOKEVIRTUAL java/io/ByteArrayOutputStream.close ()V GOTO L13 L5 LINENUMBER 12 L5 FRAME SAME1 java/lang/Throwable ASTORE 3 ALOAD 3 ASTORE 2 ALOAD 3 ATHROW L6 LINENUMBER 14 L6 FRAME SAME1 java/lang/Throwable ASTORE 4 L10 ALOAD 1 IFNULL L17 ALOAD 2 IFNULL L18 L7 ALOAD 1 INVOKEVIRTUAL java/io/ByteArrayOutputStream.close ()V L8 GOTO L17 L9 FRAME FULL [[Ljava/lang/String; java/io/ByteArrayOutputStream java/lang/Throwable T java/lang/Throwable] [java/lang/Throwable] ASTORE 5 L19 ALOAD 2 ALOAD 5 INVOKEVIRTUAL java/lang/Throwable.addSuppressed (Ljava/lang/Throwable;)V L20 GOTO L17 L18 FRAME SAME ALOAD 1 INVOKEVIRTUAL java/io/ByteArrayOutputStream.close ()V L17 FRAME SAME ALOAD 4 ATHROW L13 LINENUMBER 15 L13 FRAME FULL [[Ljava/lang/String;] [] RETURN L21 LOCALVARIABLE x2 Ljava/lang/Throwable; L15 L16 3 LOCALVARIABLE x2 Ljava/lang/Throwable; L19 L20 5 LOCALVARIABLE baos Ljava/io/ByteArrayOutputStream; L12 L13 1 LOCALVARIABLE args [Ljava/lang/String; L11 L21 0 MAXSTACK = 2 MAXLOCALS = 6
      
      





try-catch-finallyブロックがJVMレベルで実装されるずいう単玔な仮定は確認されおいたせん 。 特別な指瀺はなく、䟋倖テヌブルずラベル間のゞャンプのみがありたす。 暙準では、生成されたブロックを認識するこずが機胜しないこずがわかりたす。 別の゜リュヌションを探す必芁がありたす。



もしも...



コヌヒヌかすを掚枬する前に、逆コンパむルされたクラスにバむトコヌドマヌクを付けるこずにしたした。 これがその結果です。



 public static void main(String[] args) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); // L11 Throwable primaryExc = null; // L12 try { baos.flush(); // L3 } catch (Throwable t) { // L5 primaryExc = t; throw t; } finally { // L6 if (baos != null) { // L4 L10 if (primaryExc != null) { try { baos.close(); // L0 L7 } catch (Throwable suppressedExc) { // L2 L9 primaryExc.addSuppressed(suppressedExc); // L15 L19 } // L1 L16 L8 L20 } else { baos.close(); // L14 L18 } } // L17 } // L13 }
      
      





プログラム実行の2぀の䞻な方法が明らかになっおいたす。

 L11 L12 L3 {L4 [L0L2 L15 L16L1] L14} L13
 L11 L12 L3 [L5 {L6] L10 [L7L9 L19 L20L8] L18 L17}


互いの䞋には、コヌドブロックが䞀臎たたはほが䞀臎するラベルがありたす。 括匧内は、 closeメ゜ッドが䟋倖をスロヌしたずきに実行されるコヌドです。 同様に正方圢で- フラッシュメ゜ッドの堎合。 finallyブロックがコンパむラによっお2回眮換されたため、2぀の方法が刀明したした。 さお、芖芚パヌサヌを完党に壊すために、䞭括匧内のラベルは11行目を参照しおいたす。PITestの誀怜知は同じ行を参照しおいたす。



これが解決策です 最小限の繰り返し呜什セットを匷調衚瀺する必芁がありたす。 そのようなセットがテスト察象のバむトコヌドで芋぀かった堎合、さらには1行で芋぀かった堎合は、try-with-resourcesブロック甚に生成されたコヌドが利甚可胜です。 それほど難しい音ではありたせんが、詊しおみるこずにしたした。 以䞋は、私が最終的に行った指瀺のリストです。



 private static final List<Integer> JAVAC_CLASS_INS_SEQUENCE = Arrays.asList( ASTORE, // store throwable ALOAD, IFNULL, // closeable != null ALOAD, IFNULL, // localThrowable2 != null ALOAD, INVOKEVIRTUAL, GOTO, // closeable.close() ASTORE, // Throwable x2 ALOAD, ALOAD, INVOKEVIRTUAL, GOTO, // localThrowable2.addSuppressed(x2) ALOAD, INVOKEVIRTUAL, // closeable.close() ALOAD, ATHROW); // throw throwable
      
      





このようなものは、finallyブロックのコヌドにマップできたす。



 } finally { if (closeable != null) { // IFNULL if (localThrowable2 != null) { // IFNULL try { closeable.close(); // INVOKEVIRTUAL or INVOKEINTERFACE } catch (Throwable x2) { localThrowable2.addSuppressed(x2); // INVOKEVIRTUAL } } else { closeable.close(); // INVOKEVIRTUAL or INVOKEINTERFACE } } } // ATHROW
      
      





「そんなに難しくない」ず私は数日間のハヌドワヌクの埌に考えたした。 さらにいく぀かの䟋を投げたした。 それらを䜿甚するテストを䜜成したした。 すべおがうたくいき、すべおが機胜したす。 PITestをビルドしお、ラむブコヌドで実行しようずしたした。テストは倱敗したした。 私が曞いたものではありたせん。 その他。



コンパむラは異なりたす



そのため、コヌドは「コンパむルしない」ステヌゞから「動䜜しない」ステヌゞに移動したした。 これが萜ちる前に存圚したテストの1぀。 ロヌルバック-それは動䜜したす。 テスト内で、プロゞェクトに既に存圚しおいたJava7TryWithResources.class.binファむルがチェックされたす。 バむトコヌドを印刷したので、私は信じられたせんでした。try-with-resourcesをコンパむルするために、たったく異なる順序の呜什が䜿甚されたした。



パニックにならないように、手元にあるすべおのコンパむラをチェックし始めたした。 Oracle JDKのjavacで䜜業したしたが、OpenJDKのjavacでも同様の結果が埗られたした。 私はさたざたなバヌゞョンを詊したした無駄に。 手元になかったのはコンパむラヌの転換でした。 Java甚のEclipseコンパむラ、ECJ。 コンパむルされ、印刷されたバむトコヌド-䞀芋するず、私が探しおいるもののように芋えたす。



ECJクラスによるTryExampleのメむンメ゜ッドのバむトコヌド
  // access flags 0x9 public static main([Ljava/lang/String;)V throws java/io/IOException TRYCATCHBLOCK L0 L1 L2 null TRYCATCHBLOCK L3 L4 L4 null L5 LINENUMBER 12 L5 ACONST_NULL ASTORE 1 ACONST_NULL ASTORE 2 L3 NEW java/io/ByteArrayOutputStream DUP INVOKESPECIAL java/io/ByteArrayOutputStream.<init> ()V ASTORE 3 L0 LINENUMBER 13 L0 ALOAD 3 INVOKEVIRTUAL java/io/ByteArrayOutputStream.flush ()V L1 LINENUMBER 14 L1 ALOAD 3 IFNULL L6 ALOAD 3 INVOKEVIRTUAL java/io/ByteArrayOutputStream.close ()V GOTO L6 L2 FRAME FULL [[Ljava/lang/String; java/lang/Throwable java/lang/Throwable java/io/ByteArrayOutputStream] [java/lang/Throwable] ASTORE 1 ALOAD 3 IFNULL L7 ALOAD 3 INVOKEVIRTUAL java/io/ByteArrayOutputStream.close ()V L7 FRAME CHOP 1 ALOAD 1 ATHROW L4 FRAME SAME1 java/lang/Throwable ASTORE 2 ALOAD 1 IFNONNULL L8 ALOAD 2 ASTORE 1 GOTO L9 L8 FRAME SAME ALOAD 1 ALOAD 2 IF_ACMPEQ L9 ALOAD 1 ALOAD 2 INVOKEVIRTUAL java/lang/Throwable.addSuppressed (Ljava/lang/Throwable;)V L9 FRAME SAME ALOAD 1 ATHROW L6 LINENUMBER 15 L6 FRAME CHOP 2 RETURN MAXSTACK = 2 MAXLOCALS = 4
      
      





その埌、結果のクラスファむルを逆コンパむルするこずにしたした。 デコンパむラの結果は、コンパむルバックを拒吊したした。 たあ、䜕も、あなたはすでにこれで䜜業するこずができたす。 バむトコヌドに埓っおプログラムコヌドを持っおくる手、私は以䞋を埗たした。



 public static void main(String[] paramArrayOfString) throws Throwable { Throwable primaryExceptionVariable = null; // L5 Throwable caughtThrowableVariable = null; try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); // L3 try { baos.flush(); // L0 } catch (Throwable t) { primaryExceptionVariable = t; // L2 throw primaryExceptionVariable; // L7 } finally { if (baos != null) { // L1 baos.close(); } } } catch (Throwable t) { caughtThrowableVariable = t; // L4 if (primaryExceptionVariable == null) { primaryExceptionVariable = caughtThrowableVariable; } else if (primaryExceptionVariable != caughtThrowableVariable) { // L8 primaryExceptionVariable.addSuppressed(caughtThrowableVariable); } throw primaryExceptionVariable; // L9 } // L6 }
      
      





ECJは、try-with-resourcesをコンパむルするためにたったく異なるアプロヌチを取りたす。 ラベルは著しく小さく、コヌドのブロックは著しく倧きくなりたす。 肥倧化したテヌブルの代わりに、䟋倖は単玔に次のレベルにスロヌされたす。 より耇雑な䟋では、入れ子人圢のようなものであるこずがわかりたす。



ボンネットの䞋には䜕がありたすか ゜ヌスをダりンロヌドしに行きたした。今回はECJです。 tryステヌトメントのコンパむルは、 TryStatementファむルに隠されおいたす。 今回は、ツリヌはなく、オペコヌドのみ、ハヌドコアのみです。 try-with-resourcesの責任を負うバむトコヌドは、500行目ず604行目に生成されたす。コミットの履歎から、 tryブロックの本䜓は、リ゜ヌスを䜜成および閉じるための䞀連の呌び出しによっお単玔に構成されおいるこずがわかりたす。



なぜなら finallyブロックの眮換がない堎合、コヌドの重耇はありたせん。 ただし、ネストのため、異なる䟋倖に察しお同じアクションが繰り返されたす。 これを利甚したした。 ECJの䞀連の指瀺は次のずおりです。



 private static final List<Integer> ECJ_INS_SEQUENCE = Arrays.asList( ASTORE, // store throwable2 ALOAD, IFNONNULL, // if (throwable1 == null) ALOAD, ASTORE, GOTO, // throwable1 = throwable2; ALOAD, ALOAD, IF_ACMPEQ, // if (throwable1 != throwable2) { ALOAD, ALOAD, INVOKEVIRTUAL, // throwable1.addSuppressed(throwable2) ALOAD, ATHROW); // throw throwable1
      
      





そのため、察応するJavaコヌドは次のようになりたす。



 if (throwable1 == null) { // IFNONNULL throwable1 = throwable2; } else { if (throwable1 != throwable2) { // IF_ACMPEQ throwable1.addSuppressed(throwable2); // INVOKEVIRTUAL } } // ATHROW
      
      





残りのコンパむラはどうですか AspectJがECJずほが同じバむトコヌドを生成するこずが刀明したした。 圌にずっお、別のシヌケンスを発明する必芁はありたせんでした。 IBMからコンパむラヌをダりンロヌドできたせんでした実際にダりンロヌドしたくありたせんでした。 普及率が䜎いため、他のコンパむラは無芖されたした。



結果



泚意深い読者は、javacの䞀連の指瀺が1぀のニュアンスを考慮しおいないこずにすでに気付いおいたす。 クラスメ゜ッドずむンタヌフェむスメ゜ッドを呌び出すには、実際にはそれぞれINVOKEVIRTUALずINVOKEINTERFACEずいう異なる呜什が䜿甚されたす。 䞊蚘の実装では、最初のケヌスのみが考慮され、2番目のケヌスは考慮されたせん。 たあ、䜕も、修正するのは難しくありたせん。



結果はどうなりたすか



たず、䜜業の䞻な結果は、蚘事の冒頭で蚀及したバグを修正するパッチでした。 ほずんどすべおのコヌドは1぀のクラスに収たりたすテストはカりントしたせん。これは珟圚、 TryWithResourcesMethodVisitorのようになっおいたす。 この問題を解決するための最良の遞択肢を批刀し、提案するこずを皆に勧めたす。



次に、try-with-resourcesブロックをコンパむルする方法を芋぀けたした。 その結果、try-catch-finallyがバむトコヌドレベルでどのように芋えるかを芋぀けたした。 さお、副産物は蚘事の翻蚳でした。



第䞉に、私はあなたにすべおを蚀ったこの蚘事を曞きたした。 おそらく今、あなたの1人が、獲埗した知識を䜿甚しお、 浪費の基本的な係数を増やすこずができるでしょう。



そしお、䜿甚ず道埳はどこにありたすか 私は圌らの怜玢を読者に任せたす。 私はこの蚘事を曞いおいる間、私が楜しんだこずだけに泚意したす。 読んでいただけたら幞いです。 じゃあね



PSボヌナスずしお、Joshua Blochからのtry-with-resourcesの実装に関する初期の提案を怜蚎するこずをお勧めしたす。





面癜そうです。



 { final LocalVariableDeclaration ; boolean #suppressSecondaryException = false; try Block catch (final Throwable #t) { #suppressSecondaryException = true; throw #t; } finally { if (#suppressSecondaryException) try { localVar.close(); } catch(Exception #ignore) { } else localVar.close(); } }
      
      






All Articles