コンストラクタ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
メソッドから最初の警告をsetAccessible0
。 java.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
オブジェクトのタイプを調べる時間:
-
BootstrapConstructorAccessorImpl
:
ConstructorAccessor
実装であるクラス自体をインスタンス化するために使用されます。 おそらく、無限再帰からいくつかのコードを保存します。 特に特化したものには触れません。 -
GeneratedConstructorAccessor
:
最も興味深い実装。これについては後で詳しく説明しますが、後で説明します。 -
NativeConstructorAccessorImpl
とDelegatingConstructorAccessorImpl
束:
デフォルトで返されるものであるため、そもそも私によって考慮されます。DelegatingConstructorAccessorImpl
は、フィールドに格納されている別のオブジェクトに作業をDelegatingConstructorAccessorImpl
だけです。 このアプローチの利点は、実装をその場で置き換えることができることです。 これは実際に行われることです。各コンストラクターのNativeConstructorAccessorImpl
は、システムプロパティsun.reflect.inflationThreshold
(デフォルトでは15)で指定された最大回数を満たし、その後GeneratedConstructorAccessor
置き換えられます。 公平を期すために、sun.reflect.noInflation
プロパティを"true"
設定すると、基本的にinflationThreshhold
NativeConstructorAccessorImpl
がゼロにリセットされ、NativeConstructorAccessorImpl
は原則として作成されなくなることを追加する必要があります。 デフォルトでは、このプロパティは"false"
です。
したがって、最も一般的な状況で最も一般的なクラスの場合、オブジェクトを取得します
NativeConstructorAccessorImpl
、 NativeConstructorAccessorImpl
、手動で作成しようとします:
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つの理由があります。
- 括弧なしで
new Class
を呼び出します。 オブジェクトにメモリを割り当てるNEW
命令に対応しますが、コンストラクターはそれを呼び出しません。 - call
clazz.<init>(classLoader)
-これはコンストラクターの呼び出しであり、Java言語のこのような明示的な形式では不可能です。
これらの命令は、異なる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では、ほとんど同じです。 同じことができますが、いくつか注意点があります。
-
--add-exports java.base/jdk.internal.reflect=sample
(sampleはモジュールの名前)をコンパイラオプションに追加します。 - スタートアップパラメータに追加します。
--add-opens java.base/jdk.internal.reflect=sample
--add-opens java.base/java.lang=sample
--add-opens java.base/java.lang.reflect=sample
--add-opens java.base/java.lang.invoke=sample
--add-opens java.base/jdk.internal.reflect=java.base
- モジュールに応じて、追加に
requires jdk.unsupported
。 - コンストラクター
java.lang.Class
署名java.lang.Class
変更したため、考慮に入れる必要があります。
また、 sun.reflect
移動し、 MyConstructorInvocator
クラスをjdk.internal.reflect
と同じローダーでロードする必要があることも検討する価値があります。
ClassLoader.getSystemClassLoader()
なくなり、アクセスできなくなります。
また、 NoSuchFieldError
奇妙なバグも修正しました。現在はNoSuchMethodError
が代わりにあり、そこにあるはずです。 些細なことですが、素晴らしい。
一般に、Java 9では、たとえそれが主な目標であったとしても、自分の足で撃つためにもっと一生懸命努力する必要があります。 これは最高だと思います。
結論:
- 必要に応じて、Javaで絶対にクレイジーなことを行うことができます、それは面白いです。
- Reflection APIはそれほど複雑ではありません。
-
MagicAccessorImpl
はすべてを行うとは限りません。 -
sun.misc.Unsafe
はすべてではないかもしれませんが、ほとんど; - Java 9はさらにあなたを保護しようとしています。
あまりにも真剣に説明されたすべてをとるべきではありません。 java.lang.Class
インスタンス化タスク自体は完全に無意味です。 ここでは、それを解決する過程で得られた知識が重要です。
ご清聴ありがとうございました!