java.lang.Classをインスタンス化します


コンストラクタjava.lang.Classは、Java言語で最も保護されているエンティティの1つです。 仕様では、JVM自体のみがClass型のオブジェクトを作成でき、ここでは何の関係もないと明確に述べていますが、本当にそうですか?


Reflection APIの深さ(だけでなく)を掘り下げて、そこにすべてを配置する方法と、既存の制限を回避することがどれほど難しいかを調べることを提案します。


デフォルト設定の64ビットJDK 1.8.0_151で実験を行っています。 Java 9については記事の最後になります。


レベル1.シンプル


最も素朴な試みから始めて、増やしていきましょう。 最初に敵に向かいましょう:


 private Class(ClassLoader loader) { classLoader = loader; } 

このコンストラクタは特別なものではありません。 コンパイラーは例外を作成せず、コンストラクターもバイトコードに存在します。 したがって、他のクラスと同じことをしようとします。


 Constructor<Class> constructor = Class.class.getDeclaredConstructor(ClassLoader.class); constructor.setAccessible(true); Class<?> clazz = constructor.newInstance(ClassLoader.getSystemClassLoader()); 

このコードは機能せず、次のエラーが発生することが予想されます。


 Exception in thread "main" java.lang.SecurityException: Cannot make a java.lang.Class constructor accessible at java.lang.reflect.AccessibleObject.setAccessible0(...) at java.lang.reflect.AccessibleObject.setAccessible(...) at Sample.main(...) 

最初の試行から、 setAccessible0メソッドから最初の警告をsetAccessible0java.lang.Classクラスのコンストラクタ専用にハードコーディングされていjava.lang.Class


 private static void setAccessible0(AccessibleObject obj, boolean flag) throws SecurityException { if (obj instanceof Constructor && flag == true) { Constructor<?> c = (Constructor<?>) obj; if (c.getDeclaringClass() == Class.class) { throw new SecurityException("Cannot make a java.lang.Class" + " constructor accessible"); } } obj.override = flag; } 

このメソッドのキー行は最後のものであるため、問題ではありません- overrideフィールドをtrue設定しoverride 。 これはブルートフォースを使用して簡単に実行できます。


 Field overrideConstructorField = AccessibleObject.class.getDeclaredField("override"); overrideConstructorField.setAccessible(true); overrideConstructorField.set(constructor, true); 

レベル2.より難しい


当然、 overrideフラグの設定が唯一の制限ではありませんが、 newInstanceメソッドの動作を少なくとももう少し進めることができます。 さらなる行動を計画するのに十分なほど。 今回は、エラーは次のようになります。


 Exception in thread "main" java.lang.InstantiationException: Can not instantiate java.lang.Class at sun.reflect.InstantiationExceptionConstructorAccessorImpl.newInstance(...) at java.lang.reflect.Constructor.newInstance(...) at Sample.main(...) 

私たちはパッケージsun.reflectクラスに直接連れて行かれ、そこで主な魔法が起こるはずであることを知っています。 newInstanceクラスの実装を調べて、そこにどのようにnewInstanceしたかを調べます。


 public T newInstance(Object ... initargs) throws ... { ... ConstructorAccessor ca = constructorAccessor; // read volatile if (ca == null) { ca = acquireConstructorAccessor(); } @SuppressWarnings("unchecked") T inst = (T) ca.newInstance(initargs); return inst; } 

実装から、 Constructorがインスタンス化のすべての作業をConstructorAccessor型の別のオブジェクトに委任することが明らかになります。 これは遅延方式で初期化され、それ以上変更されません。 acquireConstructorAccessorメソッドの内部については説明しません。結果として、 newConstructorAccessorクラスsun.reflect.ReflectionFactory newConstructorAccessorメソッドの呼び出しにつながるとしか言えません。 また、このメソッドがjava.lang.Classクラスのコンストラクター(および抽象クラス)のためにInstantiationExceptionConstructorAccessorImplオブジェクトを返します。 彼は何かをインスタンス化する方法を知りませんが、彼へのあらゆる訴えに対して例外を投げます。 これはすべて、たった1つのことを意味します。正しいConstructorAccessorを自分でインスタンス化する必要があります。


レベル3.ネイティブ


(上記のオブジェクトに加えて) ConstructorAccessorオブジェクトのタイプを調べる時間:



したがって、最も一般的な状況で最も一般的なクラスの場合、オブジェクトを取得します
NativeConstructorAccessorImplNativeConstructorAccessorImpl 、手動で作成しようとします:


 Class<?> nativeCAClass = Class.forName("sun.reflect.NativeConstructorAccessorImpl"); Constructor<?> nativeCAConstructor = nativeCAClass.getDeclaredConstructor(Constructor.class); nativeCAConstructor.setAccessible(true); ConstructorAccessor constructorAccessor = (ConstructorAccessor) nativeCAConstructor.newInstance(constructor); 

トリックはありません。オブジェクトは不必要な制限なしで作成され、残りのすべてはjava.lang.Classをインスタンス化するためにそれを使用することです:


 Class<?> clazz = (Class<?>) constructorAccessor.newInstance( new Object[]{ClassLoader.getSystemClassLoader()}); 

しかし、ここで驚きが待っています:


 # # A fatal error has been detected by the Java Runtime Environment: # # SIGSEGV (0xb) at pc=0x00007f8698589ead, pid=20537, tid=0x00007f8699af3700 # # JRE version: Java(TM) SE Runtime Environment (8.0_151-b12) (build 1.8.0_151-b12) # ... 

JVMは、特にすべての警告の後、そのような非論理的なアクションをユーザーに期待していないようです。 それにもかかわらず、この結果はsun.miscことながら成果と見なすことができますsun.miscは失敗し、パッケージsun.miscクラスを使用することはありません!


レベル4.マジック


ネイティブ呼び出しは機能しないため、 GeneratedConstructorAccessorを処理する必要があります。


実際、これは単なるクラスではなく、クラスのファミリー全体です。 ランタイムのコンストラクタごとに、独自の実装が生成されます。 そのため、ネイティブ実装が主に使用されます。バイトコードを生成し、そこからクラスを作成するのは高価です。 クラス自体を生成するプロセスは、 sun.reflect.MethodAccessorGeneratorクラスのgenerateConstructorメソッドに隠されています。 手動で呼び出すのは簡単です。


 Class<?> methodAccessorGeneratorClass = Class.forName("sun.reflect.MethodAccessorGenerator"); Constructor<?> methodAccessorGeneratorConstructor = methodAccessorGeneratorClass.getDeclaredConstructor(); methodAccessorGeneratorConstructor.setAccessible(true); Object methodAccessorGenerator = methodAccessorGeneratorConstructor.newInstance(); Method generateConstructor = methodAccessorGeneratorClass .getDeclaredMethod("generateConstructor", Class.class, Class[].class, Class[].class, int.class); generateConstructor.setAccessible(true); ConstructorAccessor constructorAccessor = (ConstructorAccessor) generateConstructor.invoke(methodAccessorGenerator, constructor.getDeclaringClass(), constructor.getParameterTypes(), constructor.getExceptionTypes(), constructor.getModifiers()); 

NativeConstructorAccessorImplの場合のNativeConstructorAccessorImpl 、落とし穴はありません-このコードは機能し、期待どおりに動作します。 しかし、少し考えてみましょう。さて、ある種のクラスを生成しました。プライベートコンストラクターを呼び出す権利はどこから来るのでしょうか。 これはすべきではないので、生成されたクラスをダンプし、そのコードを調べるだけです。 これを行うのは難しくありません-デバッガーをgenerateConstructorメソッドに入れ、適切なタイミングで必要なバイト配列をファイルにダンプします。 逆コンパイルされたバージョンは次のとおりです(変数の名前を変更した後)。


 public class GeneratedConstructorAccessor1 extends ConstructorAccessorImpl { public Object newInstance(Object[] args) throws InvocationTargetException { Class clazz; ClassLoader classLoader; try { clazz = new Class; if (args.length != 1) { throw new IllegalArgumentException(); } classLoader = (ClassLoader) args[0]; } catch (NullPointerException | ClassCastException e) { throw new IllegalArgumentException(e.toString()); } try { clazz.<init>(classLoader); return clazz; } catch (Throwable e) { throw new InvocationTargetException(e); } } } 

もちろん、このようなコードをコンパイルして戻すことはできません。これには2つの理由があります。



これらの命令は、異なるtryブロックに配置するために間隔が空いています。 なぜこれが行われるのか、私にはわかりません。 これはおそらく、例外が言語仕様に完全に準拠するように例外を処理する唯一の方法でした。


非定型の例外処理に目を向けると、このクラスの他のすべてはまったく正常ですが、彼が突然プライベートコンストラクターを呼び出す権利を得た場所はまだ明確ではありません。 すべてがスーパークラスにあることがわかります。


 abstract class ConstructorAccessorImpl extends MagicAccessorImpl implements ConstructorAccessor { public abstract Object newInstance(Object[] args) throws InstantiationException, IllegalArgumentException, InvocationTargetException; } 

JVMにはsun.reflect.MagicAccessorImplと呼ばれる有名な松葉杖があります。 彼の相続人はそれぞれ、あらゆるクラスの個人データに無制限にアクセスできます。 これはまさにあなたが必要とするものです! クラスが魔法の場合、 java.lang.Classインスタンスを取得するのに役立ちます。 私たちはチェックします:


 Class<?> clazz = (Class<?>) constructorAccessor.newInstance( new Object[]{ClassLoader.getSystemClassLoader()}); 

そして再び例外が発生します:


 Exception in thread "main" java.lang.IllegalAccessError: java.lang.Class at sun.reflect.GeneratedConstructorAccessor1.newInstance(...) at Sample.main(...) 

これはすでに非常に興味深いものです。 どうやら、約束の魔法は起こらなかったようです。 または私は間違っていますか?


エラーをより慎重に検討し、 newInstanceメソッドの動作と比較する価値があります。 問題がclazz.<init>(classLoader)場合、 InvocationTargetExceptionます。 実際、 IllegalAccessErrorがあります。つまり、コンストラクターへの呼び出しにはIllegalAccessErrorませんでした。 NEW命令はエラーとともに機能し、 java.lang.Classオブジェクトにメモリを割り当てることができませんでした。 ここに私たちの力はすべてあります。


レベル5.モダン


リフレクションは問題の解決に役立ちませんでした。 たぶん、Reflectionは古くて弱いので、代わりに若くて強いMethodHandlesを使用する必要がありますか? そう思う。 少なくとも試してみる価値はあります。


そして、Reflectionが不要であると判断するとすぐに、すぐに役立ちました。 MethodHandlesはもちろん優れていますが、これを使用すると、アクセスできるデータのみを取得するのが一般的です。 また、プライベートコンストラクターが必要な場合は、昔ながらの方法を使用する必要があります。


そのため、 java.lang.Classクラスへのプライベートアクセスを持つMethodHandles.Lookupが必要MethodHandles.Lookup 。 この場合に非常に適したコンストラクターがあります。


 private Lookup(Class<?> lookupClass, int allowedModes) { ... } 

それを呼ぶ:


 Constructor<MethodHandles.Lookup> lookupConstructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class); lookupConstructor.setAccessible(true); MethodHandles.Lookup lookup = lookupConstructor .newInstance(Class.class, MethodHandles.Lookup.PRIVATE); 

lookupを受け取ったら、必要なコンストラクタに対応するMethodHandleオブジェクトを取得できます。


 MethodHandle handle = lookup.findConstructor(Class.class, MethodType.methodType(Class.class, ClassLoader.class)); 

このメソッドを開始した後、私は率直に驚いた- lookupは、コンストラクターがクラスに確実に存在するものの、まったく存在しないふりをしている!


 Exception in thread "main" java.lang.NoSuchMethodException: no such constructor: java.lang.Class.<init>(ClassLoader)Class/newInvokeSpecial at java.lang.invoke.MemberName.makeAccessException(...) at java.lang.invoke.MemberName$Factory.resolveOrFail(...) at java.lang.invoke.MethodHandles$Lookup.resolveOrFail(...) at java.lang.invoke.MethodHandles$Lookup.findConstructor(...) at Sample.main(Sample.java:59) Caused by: java.lang.NoSuchFieldError: method resolution failed at java.lang.invoke.MethodHandleNatives.resolve(...) at java.lang.invoke.MemberName$Factory.resolve(...) at java.lang.invoke.MemberName$Factory.resolveOrFail(...) ... 3 more 

奇妙なことは、例外の理由がNoSuchFieldErrorです。 不思議なことに...


今回は間違っていたのは私でしたが、私はすぐにこれを理解しませんでした。 findConstructor仕様では、 MethodType結果が説明したとおりであるにもかかわらず、戻り値の型がvoidであることが必要です(すべて、コンストラクターを担当する<init>メソッドは、歴史的な理由でvoidを返すため)
lookupはコンストラクタを取得するための2番目のメソッドがあり、 unreflectConstructorと呼ばれるため、 unreflectConstructor方法で混乱を避けることができます。


 MethodHandle handle = lookup.unreflectConstructor(constructor); 

このメソッドは確かに正しく動作し、必要なハンドルを返します。


真実の瞬間。 インスタンス化メソッドを実行します。


 Class<?> clazz = (Class<?>) handle. invoke(ClassLoader.getSystemClassLoader()); 

良いことは何も起こらないとすでに推測していると思いますが、少なくともエラーを見てみましょう。 これは新しいものです:


 Exception in thread "main" java.lang.IllegalAccessException: java.lang.Class at sun.misc.Unsafe.allocateInstance(...) at java.lang.invoke.DirectMethodHandle.allocateInstance(...) at java.lang.invoke.LambdaForm$DMH/925858445.newInvokeSpecial_L_L(...) at java.lang.invoke.LambdaForm$MH/523429237.invoke_MT(...) at Sample.main(...) 

デフォルトでは、スタックトレースは短く表示されるため、追加しました
-XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames起動パラメーターの-XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames 。 これにより、私たちがどのような奇妙な場所にいるかを理解しやすくなります。


MethodHandles生成するクラスMethodHandlesは詳しく説明しませんが、これは重要ではありません。 別のことが重要です-最終的にsun.misc.Unsafeを使用するsun.misc.Unsafeになり、彼でさえjava.lang.Classオブジェクトを作成できません。


allocaeInstanceメソッドallocaeInstance 、オブジェクトを作成したいが、そのコンストラクターを呼び出さない場所で使用されます。 これは、たとえばオブジェクトを逆シリアル化するときに役立ちます。 実際、これは同じNEW命令ですが、アクセス制御チェックの負担はかかりません。 今見たように、 ほとんど負担はありません。


Unsafeでもできなかったので、悲しい結論に達することができるだけです。新しいjava.lang.Classオブジェクトを割り当てることは不可能です。 興味深いことに、コンストラクタは禁止されていて、割り当ては禁止されていると思いました! これを回避してみましょう。


レベル6.安全でない


空のオブジェクトを作成し、それが何で構成されているかを確認することを提案します。 これを行うには、 Unsafeを使用して、新しいjava.lang.Objectを割り当てjava.lang.Object


 Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) theUnsafeField.get(null); Object object = unsafe.allocateInstance(Object.class); 

現在のJVMでは、結果は次のような12バイトのメモリ領域になります。



ここに表示されるのは「オブジェクトヘッダー」です。 概して、それは2つの部分で構成されています-関心のない8バイトのマークワードと、重要な4バイトのクラスワードです。


JVMはどのようにオブジェクトクラスを認識しますか? これは、クラスを記述する内部JVM構造へのポインターを格納するクラスワード領域を読み取ることでこれを行います。 したがって、この場所に別の値を書き込むと、オブジェクトのクラスが変更されます!


次のコードは非常に悪いので、絶対にしないでください。


 System.out.println(object.getClass()); unsafe.putInt(object, 8L, unsafe.getInt(Object.class, 8L)); System.out.println(object.getClass()); 

Object.classのクラスワードを読み取り、それをobjectクラスワードに書き込みました。 作業の結果は次のとおりです。


 class java.lang.Object class java.lang.Class 

一気に、 java.lang.Classを割り当てたと仮定できjava.lang.Class 。 よくできました! 次に、コンストラクターを呼び出す必要があります。 笑うこともできますが、ASMを使用して、目的のコンストラクターを呼び出すことができるクラスを生成します。 当然、 MagicAccessorImplから継承する必要があります。


これがクラス作成の始まりです(定数は静的にインポートされるため、より短くなります)。


 ClassWriter cw = new ClassWriter(COMPUTE_FRAMES | COMPUTE_MAXS); cw.visit(V1_8, ACC_PUBLIC, "sun/reflect/MyConstructorInvocator", null, "sun/reflect/MagicAccessorImpl", null); 

そこで彼はコンストラクターを作成します:


 MethodVisitor init = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); init.visitCode(); init.visitVarInsn(ALOAD, 0); init.visitMethodInsn(INVOKESPECIAL, "sun/reflect/MagicAccessorImpl", "<init>", "()V", false); init.visitInsn(RETURN); init.visitMaxs(-1, -1); init.visitEnd(); 

そして、 void construct(Class<?>, ClassLoader)メソッドが作成されvoid construct(Class<?>, ClassLoader) 、内部でClass<?>オブジェクトのコンストラクタを呼び出します:


 MethodVisitor construct = cw.visitMethod(ACC_PUBLIC, "construct", "(Ljava/lang/Class;Ljava/lang/ClassLoader;)V", null, null); construct.visitCode(); construct.visitVarInsn(ALOAD, 1); construct.visitVarInsn(ALOAD, 2); construct.visitMethodInsn(INVOKESPECIAL, "java/lang/Class", "<init>", "(Ljava/lang/ClassLoader;)V", false); construct.visitInsn(RETURN); construct.visitMaxs(-1, -1); construct.visitEnd(); 

クラスの準備ができました。 目的のメソッドをダウンロードし、インスタンス化し、呼び出します。


 byte[] bytes = cw.toByteArray(); Class<?> myCustomInvocator = unsafe.defineClass(null, bytes, 0, bytes.length, ClassLoader.getSystemClassLoader(), null); Object ci = myCustomInvocator.newInstance(); Method constructMethod = myCustomInvocator.getDeclaredMethod("construct", Class.class, ClassLoader.class); Class<?> clazz = (Class<?>) object; constructMethod.invoke(ci, clazz, ClassLoader.getSystemClassLoader()); 

そしてそれは動作します! より正確には、それが機能することは幸運です。 次のコードを実行して確認できます。


 System.out.println(clazz.getClassLoader()); 

出力は次のようになります。


 sun.misc.Launcher$AppClassLoader@18b4aac2 

このClassLoader場所と、後で読み込まれた場所について、私は巧みに黙秘します。 そして、予想どおり、このオブジェクトで他のほとんどすべてのメソッドを呼び出すと、すぐにJVMがクラッシュします。 そして残り-目標は完了しました!


Java 9には何がありますか?


Java 9では、ほとんど同じです。 同じことができますが、いくつか注意点があります。



また、 sun.reflect移動し、 MyConstructorInvocatorクラスをjdk.internal.reflectと同じローダーでロードする必要があることも検討する価値があります。
ClassLoader.getSystemClassLoader()なくなり、アクセスできなくなります。


また、 NoSuchFieldError奇妙なバグも修正しました。現在はNoSuchMethodErrorが代わりにあり、そこにあるはずです。 些細なことですが、素晴らしい。


一般に、Java 9では、たとえそれが主な目標であったとしても、自分の足で撃つためにもっと一生懸命努力する必要があります。 これは最高だと思います。


結論:



あまりにも真剣に説明されたすべてをとるべきではありません。 java.lang.Classインスタンス化タスク自体は完全に無意味です。 ここでは、それを解決する過程で得られた知識が重要です。


ご清聴ありがとうございました!



Source: https://habr.com/ru/post/J341930/


All Articles