Skip to content

Commit 1b2203d

Browse files
committed
Support dynamic class loading
1 parent c81f3ed commit 1b2203d

File tree

11 files changed

+638
-22
lines changed

11 files changed

+638
-22
lines changed
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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+
import java.nio.file.Paths;
38+
39+
import com.oracle.svm.jni.nativeapi.JNIMethodId;
40+
import org.graalvm.nativeimage.c.type.CCharPointer;
41+
import org.graalvm.nativeimage.c.type.CTypeConversion;
42+
import org.graalvm.nativeimage.c.type.VoidPointer;
43+
import org.graalvm.word.WordFactory;
44+
45+
import com.oracle.svm.jni.nativeapi.JNIEnvironment;
46+
import com.oracle.svm.jni.nativeapi.JNIObjectHandle;
47+
48+
public abstract class AbstractDynamicClassGenerationSupport {
49+
50+
protected JNIEnvironment jni;
51+
protected JNIObjectHandle callerClass;
52+
protected final String generatedClassName;
53+
protected TraceWriter traceWriter;
54+
protected NativeImageAgent agent;
55+
56+
private static Path dynclassDumpDir = null;
57+
private static final Path DEFAULT_DUMP = Paths.get("dynClass");
58+
59+
public static void setDynClassDumpDir(String dir) {
60+
dynclassDumpDir = Paths.get(dir);
61+
}
62+
63+
public static AbstractDynamicClassGenerationSupport getDynamicClassGenerationSupport(JNIEnvironment jni, JNIObjectHandle callerClass,
64+
String generatedClassName, TraceWriter traceWriter, NativeImageAgent agent) throws IOException{
65+
return new DynamicDefineClassSupport(jni, callerClass, generatedClassName, traceWriter, agent);
66+
}
67+
68+
protected AbstractDynamicClassGenerationSupport(JNIEnvironment jni, JNIObjectHandle callerClass,
69+
String generatedClassName, TraceWriter traceWriter, NativeImageAgent agent) throws IOException{
70+
if (dynclassDumpDir == null) {
71+
System.out.println("Warning: dynamic-class-dump-dir= was not set in -agentlib:native-image-agent=, using default location dynClass");
72+
dynclassDumpDir = DEFAULT_DUMP;
73+
}
74+
75+
if (!Files.exists(dynclassDumpDir)) {
76+
Files.createDirectories(dynclassDumpDir);
77+
} else if (!Files.isDirectory(dynclassDumpDir)) {
78+
throw new IOException("File " + dynclassDumpDir + " already exists! Cannot create the same name directory for dumping class file.");
79+
}
80+
81+
this.jni = jni;
82+
this.callerClass = callerClass;
83+
// Make sure use qualified name for generatedClassName
84+
this.generatedClassName = generatedClassName.replace('/', '.');
85+
this.traceWriter = traceWriter;
86+
this.agent = agent;
87+
}
88+
89+
public abstract boolean traceReflects();
90+
91+
/**
92+
* Get class definition from java.lang.ClassLoader.defineClass.
93+
*
94+
* @return JObject represents byte[] or DirectBuffer
95+
*/
96+
protected abstract JNIObjectHandle getClassDefinition();
97+
98+
protected abstract int getClassDefinitionBytesLength();
99+
100+
protected abstract byte[] getClassContents();
101+
102+
protected byte[] getClassContentsFromByteArray() {
103+
// bytes parameter of defineClass method
104+
JNIObjectHandle bytes = getClassDefinition();
105+
// len parameter of defineClass method
106+
int length = getClassDefinitionBytesLength();
107+
// Get generated class' byte array
108+
CCharPointer byteArray = jniFunctions().getGetByteArrayElements().invoke(jni, bytes, WordFactory.nullPointer());
109+
byte[] values = new byte[length];
110+
try {
111+
CTypeConversion.asByteBuffer(byteArray, length).get(values);
112+
} finally {
113+
jniFunctions().getReleaseByteArrayElements().invoke(jni, bytes, byteArray, 0);
114+
}
115+
return values;
116+
}
117+
118+
protected byte[] getClassContentsFromDirectBuffer() {
119+
// DirectBuffer parameter of defineClass
120+
JNIObjectHandle directbuffer = getClassDefinition();
121+
122+
// Get byte array from DirectBuffer
123+
VoidPointer baseAddr = jniFunctions().getGetDirectBufferAddress().invoke(jni, directbuffer);
124+
JNIMethodId limitMId = agent.handles().getMethodId(jni, agent.handles().javaNioByteBuffer, "limit", "()I", false);
125+
int limit = jniFunctions().getCallIntMethod().invoke(jni, directbuffer, limitMId);
126+
ByteBuffer classContentsAsByteBuffer = CTypeConversion.asByteBuffer(baseAddr, limit);
127+
byte[] dest = new byte[classContentsAsByteBuffer.limit()];
128+
classContentsAsByteBuffer.get(dest);
129+
classContentsAsByteBuffer.position(0);
130+
return dest;
131+
}
132+
133+
/**
134+
* Save dynamically defined class to file system.
135+
*
136+
* @return true if successfully dumped
137+
*/
138+
public boolean dumpDefinedClass() throws IOException{
139+
byte[] values = getClassContents();
140+
// Get name for generated class
141+
String internalName = generatedClassName.replace('.', File.separatorChar);
142+
Path dumpFile = dynclassDumpDir.resolve(internalName + ".class");
143+
144+
// Get directory from package
145+
Path dumpDirs = dumpFile.getParent();
146+
if (!Files.exists(dumpDirs)) {
147+
Files.createDirectories(dumpDirs);
148+
} else if (!Files.isDirectory(dumpDirs)) {
149+
throw new IOException("File " + dumpDirs + " already exists! Cannot create the same name directory for dumping class file.");
150+
}
151+
try (FileOutputStream stream = new FileOutputStream(dumpFile.toFile());) {
152+
stream.write(values);
153+
}
154+
155+
// Dump stack trace for debug usage
156+
Path dumpTraceFile = dynclassDumpDir.resolve(internalName + ".txt");
157+
StringBuilder trace = new StringBuilder();
158+
int i = 0;
159+
while (true) {
160+
String methodName = getMethodFullNameAtFrame(jni, i++);
161+
if (methodName == null) {
162+
break;
163+
}
164+
trace.append(methodName).append("\n");
165+
}
166+
try (FileOutputStream traceStream = new FileOutputStream(dumpTraceFile.toFile());) {
167+
traceStream.write(trace.toString().getBytes());
168+
}
169+
170+
return true;
171+
}
172+
}

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

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@
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;
5051
import static org.graalvm.word.WordFactory.nullPointer;
5152

53+
import java.io.IOException;
5254
import java.nio.ByteBuffer;
5355
import java.nio.ByteOrder;
5456
import java.util.ArrayList;
@@ -61,7 +63,9 @@
6163
import java.util.concurrent.locks.ReentrantLock;
6264
import java.util.function.Supplier;
6365

66+
import com.oracle.svm.configure.trace.AccessAdvisor;
6467
import org.graalvm.compiler.core.common.NumUtil;
68+
import org.graalvm.compiler.phases.common.LazyValue;
6569
import org.graalvm.nativeimage.StackValue;
6670
import org.graalvm.nativeimage.UnmanagedMemory;
6771
import org.graalvm.nativeimage.c.function.CEntryPoint;
@@ -84,6 +88,7 @@
8488
import com.oracle.svm.configure.config.ConfigurationMethod;
8589
import com.oracle.svm.core.c.function.CEntryPointOptions;
8690
import com.oracle.svm.core.util.VMError;
91+
8792
import com.oracle.svm.jni.nativeapi.JNIEnvironment;
8893
import com.oracle.svm.jni.nativeapi.JNIMethodId;
8994
import com.oracle.svm.jni.nativeapi.JNINativeMethod;
@@ -131,6 +136,7 @@ final class BreakpointInterceptor {
131136
private static ResourceAccessVerifier resourceVerifier;
132137
private static NativeImageAgent agent;
133138

139+
private static AccessAdvisor accessAdvisor = new AccessAdvisor();
134140
private static Map<Long, Breakpoint> installedBreakpoints;
135141

136142
/**
@@ -175,6 +181,18 @@ private static void traceBreakpoint(JNIEnvironment env, JNIObjectHandle clazz, J
175181
}
176182
}
177183

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

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

11791255
private interface BreakpointHandler {
1180-
boolean dispatch(JNIEnvironment jni, Breakpoint bp);
1256+
boolean dispatch(JNIEnvironment jni, Breakpoint bp) throws IOException;
11811257
}
11821258

11831259
private static final BreakpointSpecification[] BREAKPOINT_SPECIFICATIONS = {
@@ -1217,6 +1293,10 @@ private interface BreakpointHandler {
12171293
brk("java/lang/reflect/Proxy", "getProxyClass", "(Ljava/lang/ClassLoader;[Ljava/lang/Class;)Ljava/lang/Class;", BreakpointInterceptor::getProxyClass),
12181294
brk("java/lang/reflect/Proxy", "newProxyInstance",
12191295
"(Ljava/lang/ClassLoader;[Ljava/lang/Class;Ljava/lang/reflect/InvocationHandler;)Ljava/lang/Object;", BreakpointInterceptor::newProxyInstance),
1296+
/*
1297+
* For dumping dynamically generated classes
1298+
*/
1299+
brk("java/lang/ClassLoader", "postDefineClass", "(Ljava/lang/Class;Ljava/security/ProtectionDomain;)V", BreakpointInterceptor::postDefineClass),
12201300

12211301
optionalBrk("java/util/ResourceBundle",
12221302
"getBundleImpl",

0 commit comments

Comments
 (0)