Skip to content

Commit 397df00

Browse files
committed
Support dynamic class loading
1 parent c81f3ed commit 397df00

File tree

11 files changed

+647
-21
lines changed

11 files changed

+647
-21
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/*
2+
* Copyright (c) 2019, 2019, Oracle and/or its affiliates. All rights reserved.
3+
* Copyright (c) 2020 Alibaba Group Holding Limited. All Rights Reserved.
4+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
5+
*
6+
* This code is free software; you can redistribute it and/or modify it
7+
* under the terms of the GNU General Public License version 2 only, as
8+
* published by the Free Software Foundation. Oracle designates this
9+
* particular file as subject to the "Classpath" exception as provided
10+
* by Oracle in the LICENSE file that accompanied this code.
11+
*
12+
* This code is distributed in the hope that it will be useful, but WITHOUT
13+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15+
* version 2 for more details (a copy is included in the LICENSE file that
16+
* accompanied this code).
17+
*
18+
* You should have received a copy of the GNU General Public License version
19+
* 2 along with this work; if not, write to the Free Software Foundation,
20+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
21+
*
22+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
23+
* or visit www.oracle.com if you need additional information or have any
24+
* questions.
25+
*/
26+
package com.oracle.svm.agent;
27+
28+
import static com.oracle.svm.jvmtiagentbase.Support.jniFunctions;
29+
import static com.oracle.svm.jvmtiagentbase.Support.getMethodFullNameAtFrame;
30+
31+
import java.io.File;
32+
import java.io.FileOutputStream;
33+
import java.io.IOException;
34+
import java.nio.ByteBuffer;
35+
import java.nio.file.Files;
36+
import java.nio.file.Path;
37+
38+
import com.oracle.svm.jni.nativeapi.JNIMethodId;
39+
import org.graalvm.nativeimage.c.type.CCharPointer;
40+
import org.graalvm.nativeimage.c.type.CTypeConversion;
41+
import org.graalvm.nativeimage.c.type.VoidPointer;
42+
import org.graalvm.word.WordFactory;
43+
44+
import com.oracle.svm.jni.nativeapi.JNIEnvironment;
45+
import com.oracle.svm.jni.nativeapi.JNIObjectHandle;
46+
47+
public abstract class AbstractDynamicClassGenerationSupport {
48+
49+
protected JNIEnvironment jni;
50+
protected JNIObjectHandle callerClass;
51+
protected final String generatedClassName;
52+
protected TraceWriter traceWriter;
53+
protected NativeImageAgent agent;
54+
55+
private static String dynclassDumpDir = null;
56+
private static final String DEFAULT_DUMP = "dynClass";
57+
58+
static {
59+
if (dynclassDumpDir == null) {
60+
System.out.println("Warning: dynamic-class-dump-dir= was not set in -agentlib:native-image-agent=, using default location dynClass");
61+
dynclassDumpDir = DEFAULT_DUMP;
62+
}
63+
Path dumpDir = new File(dynclassDumpDir).toPath();
64+
try {
65+
if (!Files.exists(dumpDir)) {
66+
Files.createDirectory(dumpDir);
67+
}
68+
} catch (IOException e) {
69+
e.printStackTrace();
70+
}
71+
}
72+
73+
public static void setDynClassDumpDir(String dir) {
74+
dynclassDumpDir = dir;
75+
}
76+
77+
public static AbstractDynamicClassGenerationSupport getDynamicClassGenerationSupport(JNIEnvironment jni, JNIObjectHandle callerClass,
78+
String generatedClassName, TraceWriter traceWriter, NativeImageAgent agent) {
79+
return new DynamicDefineClassSupport(jni, callerClass, generatedClassName, traceWriter, agent);
80+
}
81+
82+
protected AbstractDynamicClassGenerationSupport(JNIEnvironment jni, JNIObjectHandle callerClass,
83+
String generatedClassName, TraceWriter traceWriter, NativeImageAgent agent) {
84+
this.jni = jni;
85+
this.callerClass = callerClass;
86+
// Make sure use qualified name for generatedClassName
87+
if (generatedClassName.indexOf('/') != -1) {
88+
this.generatedClassName = generatedClassName.replace('/', '.');
89+
} else {
90+
this.generatedClassName = generatedClassName;
91+
}
92+
this.traceWriter = traceWriter;
93+
this.agent = agent;
94+
}
95+
96+
public abstract boolean traceReflects();
97+
98+
/**
99+
* Get class definition from java.lang.ClassLoader.defineClass.
100+
*
101+
* @return JObject represents byte[] or DirectBuffer
102+
*/
103+
protected abstract JNIObjectHandle getClassDefinition();
104+
105+
protected abstract int getClassDefinitionBytesLength();
106+
107+
protected abstract byte[] getClassContents();
108+
109+
protected byte[] getClassContentsFromByteArray() {
110+
// bytes parameter of defineClass method
111+
JNIObjectHandle bytes = getClassDefinition();
112+
// len parameter of defineClass method
113+
int length = getClassDefinitionBytesLength();
114+
// Get generated class' byte array
115+
CCharPointer byteArray = jniFunctions().getGetByteArrayElements().invoke(jni, bytes, WordFactory.nullPointer());
116+
byte[] values = new byte[length];
117+
try {
118+
CTypeConversion.asByteBuffer(byteArray, length).get(values);
119+
} finally {
120+
jniFunctions().getReleaseByteArrayElements().invoke(jni, bytes, byteArray, 0);
121+
}
122+
return values;
123+
}
124+
125+
protected byte[] getClassContentsFromDirectBuffer() {
126+
// DirectBuffer parameter of defineClass
127+
JNIObjectHandle directbuffer = getClassDefinition();
128+
129+
// Get byte array from DirectBuffer
130+
VoidPointer baseAddr = jniFunctions().getGetDirectBufferAddress().invoke(jni, directbuffer);
131+
JNIMethodId limitMId = agent.handles().getMethodId(jni, agent.handles().javaNioByteBuffer, "limit", "()I", false);
132+
int limit = jniFunctions().getCallIntMethod().invoke(jni, directbuffer, limitMId);
133+
ByteBuffer classContentsAsByteBuffer = CTypeConversion.asByteBuffer(baseAddr, limit);
134+
byte[] dest = new byte[classContentsAsByteBuffer.limit()];
135+
classContentsAsByteBuffer.get(dest);
136+
classContentsAsByteBuffer.position(0);
137+
return dest;
138+
}
139+
140+
/**
141+
* Save dynamically defined class to file system.
142+
*
143+
* @return true if successfully dumped
144+
*/
145+
public boolean dumpDefinedClass() {
146+
byte[] values = getClassContents();
147+
// Get name for generated class
148+
String internalName = generatedClassName;
149+
if (internalName.indexOf('.') != -1) {
150+
internalName = internalName.replace('.', File.separatorChar);
151+
}
152+
String dumpFileName = dynclassDumpDir + File.pathSeparator + internalName + ".class";
153+
154+
// Get directory from package
155+
String dumpDirs = dumpFileName.substring(0, dumpFileName.lastIndexOf('/'));
156+
try {
157+
File dirs = new File(dumpDirs);
158+
if (!dirs.exists()) {
159+
dirs.mkdirs();
160+
}
161+
try (FileOutputStream stream = new FileOutputStream(dumpFileName);) {
162+
stream.write(values);
163+
}
164+
165+
// Dump stack trace for debug usage
166+
String dumpTraceFile = dynclassDumpDir + File.pathSeparator + internalName + ".txt";
167+
StringBuilder trace = new StringBuilder();
168+
int i = 0;
169+
while (true) {
170+
String methodName = getMethodFullNameAtFrame(jni, i++);
171+
if (methodName == null) {
172+
break;
173+
}
174+
trace.append(methodName).append("\n");
175+
}
176+
try (FileOutputStream traceStream = new FileOutputStream(dumpTraceFile);) {
177+
traceStream.write(trace.toString().getBytes());
178+
}
179+
} catch (IOException e) {
180+
e.printStackTrace();
181+
return false;
182+
}
183+
return true;
184+
}
185+
}

substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/BreakpointInterceptor.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import static com.oracle.svm.jvmtiagentbase.Support.jvmtiFunctions;
4545
import static com.oracle.svm.jvmtiagentbase.Support.testException;
4646
import static com.oracle.svm.jvmtiagentbase.Support.toCString;
47+
import static com.oracle.svm.jvmtiagentbase.Support.callObjectMethodL;
4748
import static com.oracle.svm.jvmtiagentbase.jvmti.JvmtiEvent.JVMTI_EVENT_BREAKPOINT;
4849
import static com.oracle.svm.jvmtiagentbase.jvmti.JvmtiEvent.JVMTI_EVENT_CLASS_PREPARE;
4950
import static com.oracle.svm.jvmtiagentbase.jvmti.JvmtiEvent.JVMTI_EVENT_NATIVE_METHOD_BIND;
@@ -61,7 +62,9 @@
6162
import java.util.concurrent.locks.ReentrantLock;
6263
import java.util.function.Supplier;
6364

65+
import com.oracle.svm.configure.trace.AccessAdvisor;
6466
import org.graalvm.compiler.core.common.NumUtil;
67+
import org.graalvm.compiler.phases.common.LazyValue;
6568
import org.graalvm.nativeimage.StackValue;
6669
import org.graalvm.nativeimage.UnmanagedMemory;
6770
import org.graalvm.nativeimage.c.function.CEntryPoint;
@@ -84,6 +87,7 @@
8487
import com.oracle.svm.configure.config.ConfigurationMethod;
8588
import com.oracle.svm.core.c.function.CEntryPointOptions;
8689
import com.oracle.svm.core.util.VMError;
90+
8791
import com.oracle.svm.jni.nativeapi.JNIEnvironment;
8892
import com.oracle.svm.jni.nativeapi.JNIMethodId;
8993
import com.oracle.svm.jni.nativeapi.JNINativeMethod;
@@ -131,6 +135,7 @@ final class BreakpointInterceptor {
131135
private static ResourceAccessVerifier resourceVerifier;
132136
private static NativeImageAgent agent;
133137

138+
private static AccessAdvisor accessAdvisor = new AccessAdvisor();
134139
private static Map<Long, Breakpoint> installedBreakpoints;
135140

136141
/**
@@ -175,6 +180,18 @@ private static void traceBreakpoint(JNIEnvironment env, JNIObjectHandle clazz, J
175180
}
176181
}
177182

183+
static void traceBreakpoint(JNIEnvironment env, JNIObjectHandle clazz, JNIObjectHandle declaringClass,
184+
JNIObjectHandle callerClass, String function, boolean allowWrite, boolean unsafeAccess, Object result,
185+
String fieldName) {
186+
if (traceWriter != null) {
187+
traceWriter.traceCall("reflect", function, getClassNameOr(env, clazz, null, TraceWriter.UNKNOWN_VALUE),
188+
getClassNameOr(env, declaringClass, null, TraceWriter.UNKNOWN_VALUE),
189+
getClassNameOr(env, callerClass, null, TraceWriter.UNKNOWN_VALUE), result, allowWrite, unsafeAccess,
190+
fieldName);
191+
guarantee(!testException(env));
192+
}
193+
}
194+
178195
private static boolean forName(JNIEnvironment jni, Breakpoint bp) {
179196
JNIObjectHandle callerClass = getDirectCallerClass();
180197
JNIObjectHandle name = getObjectArgument(0);
@@ -678,6 +695,64 @@ private static boolean handleGetSystemResources(JNIEnvironment jni, Breakpoint b
678695
return allowed;
679696
}
680697

698+
/**
699+
* java.lang.ClassLoader.postDefineClass is always called in java.lang.ClassLoader.defineClass,
700+
* so intercepting postDefineClass is equivalent to intercepting defineClass but with extra
701+
* benefit of being always able to get defined class' name even if defineClass' classname
702+
* parameter is null.
703+
*/
704+
@SuppressWarnings("unused")
705+
private static boolean postDefineClass(JNIEnvironment jni, Breakpoint bp) {
706+
boolean isDynamicallyGenerated = false;
707+
// Get class name from the argument "name" of
708+
// defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain)
709+
// The first argument is implicitly "this", so "name" is the 2nd parameter.
710+
String nameFromDefineClassParam = fromJniString(jni, getObjectArgument(1, 1));
711+
final String definedClassName;
712+
// 1. Don't have a name for class before defining.
713+
// The class is dynamically generated.
714+
if (nameFromDefineClassParam == null) {
715+
isDynamicallyGenerated = true;
716+
// Get name from parameter "c" of method postDefineClass(Class<?> c, ProtectionDomain
717+
// pd)
718+
definedClassName = getClassNameOrNull(jni, getObjectArgument(1));
719+
} else {
720+
definedClassName = nameFromDefineClassParam;
721+
// Filter out internal classes which are definitely not dynamically generated
722+
if (accessAdvisor.shouldIgnore(new LazyValue<>(() -> definedClassName), new LazyValue<>(() -> definedClassName))) {
723+
return false;
724+
}
725+
726+
// 2. Class with name starts with $ or contains $$ is usually dynamically generated
727+
String className = definedClassName.substring(definedClassName.lastIndexOf('.') + 1);
728+
if (className.startsWith("$") || className.contains("$$")) {
729+
isDynamicallyGenerated = true;
730+
} else {
731+
// 3. A dynamically defined class always return null
732+
// when call java.lang.ClassLoader.getResource(classname)
733+
// This is the accurate but slow way.
734+
JNIObjectHandle self = getObjectArgument(0);
735+
String asResourceName = definedClassName.replace('.', '/') + ".class";
736+
try (CCharPointerHolder resourceNameHolder = toCString(asResourceName);) {
737+
JNIObjectHandle resourceNameJString = jniFunctions().getNewStringUTF().invoke(jni, resourceNameHolder.get());
738+
JNIObjectHandle returnValue = callObjectMethodL(jni, self, agent.handles().javaLangClassLoaderGetResource, resourceNameJString);
739+
isDynamicallyGenerated = returnValue.equal(nullHandle());
740+
}
741+
}
742+
}
743+
if (isDynamicallyGenerated) {
744+
JNIObjectHandle callerClass = getDirectCallerClass();
745+
AbstractDynamicClassGenerationSupport dynamicSupport = AbstractDynamicClassGenerationSupport.getDynamicClassGenerationSupport(jni, callerClass,
746+
definedClassName, traceWriter, agent);
747+
if (!dynamicSupport.dumpDefinedClass()) {
748+
return false;
749+
}
750+
return dynamicSupport.traceReflects();
751+
} else {
752+
return true;
753+
}
754+
}
755+
681756
private static boolean newProxyInstance(JNIEnvironment jni, Breakpoint bp) {
682757
JNIObjectHandle callerClass = getDirectCallerClass();
683758
JNIObjectHandle classLoader = getObjectArgument(0);
@@ -1217,6 +1292,10 @@ private interface BreakpointHandler {
12171292
brk("java/lang/reflect/Proxy", "getProxyClass", "(Ljava/lang/ClassLoader;[Ljava/lang/Class;)Ljava/lang/Class;", BreakpointInterceptor::getProxyClass),
12181293
brk("java/lang/reflect/Proxy", "newProxyInstance",
12191294
"(Ljava/lang/ClassLoader;[Ljava/lang/Class;Ljava/lang/reflect/InvocationHandler;)Ljava/lang/Object;", BreakpointInterceptor::newProxyInstance),
1295+
/*
1296+
* For dumping dynamically generated classes
1297+
*/
1298+
brk("java/lang/ClassLoader", "postDefineClass", "(Ljava/lang/Class;Ljava/security/ProtectionDomain;)V", BreakpointInterceptor::postDefineClass),
12201299

12211300
optionalBrk("java/util/ResourceBundle",
12221301
"getBundleImpl",
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright (c) 2019, 2019, Oracle and/or its affiliates. All rights reserved.
3+
* Copyright (c) 2020 Alibaba Group Holding Limited. All Rights Reserved.
4+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
5+
*
6+
* This code is free software; you can redistribute it and/or modify it
7+
* under the terms of the GNU General Public License version 2 only, as
8+
* published by the Free Software Foundation. Oracle designates this
9+
* particular file as subject to the "Classpath" exception as provided
10+
* by Oracle in the LICENSE file that accompanied this code.
11+
*
12+
* This code is distributed in the hope that it will be useful, but WITHOUT
13+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15+
* version 2 for more details (a copy is included in the LICENSE file that
16+
* accompanied this code).
17+
*
18+
* You should have received a copy of the GNU General Public License version
19+
* 2 along with this work; if not, write to the Free Software Foundation,
20+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
21+
*
22+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
23+
* or visit www.oracle.com if you need additional information or have any
24+
* questions.
25+
*/
26+
package com.oracle.svm.agent;
27+
28+
import com.oracle.svm.jni.nativeapi.JNIEnvironment;
29+
import static com.oracle.svm.jvmtiagentbase.Support.getIntArgument;
30+
import static com.oracle.svm.jvmtiagentbase.Support.getObjectArgument;
31+
import static com.oracle.svm.jvmtiagentbase.Support.getMethodFullNameAtFrame;
32+
33+
import com.oracle.svm.jni.nativeapi.JNIObjectHandle;
34+
35+
public class DynamicDefineClassSupport extends AbstractDynamicClassGenerationSupport {
36+
37+
private String caller;
38+
private static final String DEFINE_CLASS_BYTEARRAY_SIG = "java.lang.ClassLoader.defineClass(Ljava/lang/String;[BIILjava/security/ProtectionDomain;)Ljava/lang/Class;";
39+
private static final String DEFINE_CLASS_DIRBUFFER_SIG = "java.lang.ClassLoader.defineClass(Ljava/lang/String;Ljava/nio/ByteBuffer;Ljava/security/ProtectionDomain;)Ljava/lang/Class;";
40+
41+
protected DynamicDefineClassSupport(JNIEnvironment jni, JNIObjectHandle callerClass, String generatedClassName, TraceWriter traceWriter, NativeImageAgent agent) {
42+
super(jni, callerClass, generatedClassName, traceWriter, agent);
43+
caller = getMethodFullNameAtFrame(jni, 1);
44+
}
45+
46+
@Override
47+
public boolean traceReflects() {
48+
traceWriter.traceCall("reflect", "getDeclaredMethods", generatedClassName);
49+
return true;
50+
}
51+
52+
@Override
53+
protected byte[] getClassContents() {
54+
assert DEFINE_CLASS_BYTEARRAY_SIG.equals(caller) || DEFINE_CLASS_DIRBUFFER_SIG.equals(caller);
55+
if (DEFINE_CLASS_BYTEARRAY_SIG.equals(caller)) {
56+
return getClassContentsFromByteArray();
57+
} else {
58+
return getClassContentsFromDirectBuffer();
59+
}
60+
}
61+
62+
/**
63+
* Get value of argument "b" from java.lang.ClassLoader.defineClass(String name, byte[] b, int
64+
* off, int len, ProtectionDomain protectionDomain) "b" is the 3rd argument. because the 1st
65+
* argument of instance method is always "this"
66+
*/
67+
@Override
68+
protected JNIObjectHandle getClassDefinition() {
69+
assert DEFINE_CLASS_BYTEARRAY_SIG.equals(caller) || DEFINE_CLASS_DIRBUFFER_SIG.equals(caller);
70+
return getObjectArgument(1, 2);
71+
}
72+
73+
/**
74+
* Get value of argument "len" from java.lang.ClassLoader. defineClass(String name, byte[] b,
75+
* int off, int len, ProtectionDomain protectionDomain) "len" is the 5th argument. because the
76+
* 1st argument of instance method is always "this"
77+
*/
78+
@Override
79+
protected int getClassDefinitionBytesLength() {
80+
assert DEFINE_CLASS_BYTEARRAY_SIG.equals(caller);
81+
return getIntArgument(1, 4);
82+
}
83+
}

0 commit comments

Comments
 (0)