Skip to content

Commit 8c427e6

Browse files
committed
Support agent by attaching it at build time
GraalVM now can apply the class transformation in the native image by attaching the java agent at build time. User can attach the java agent to native-image with -javaagent option.
1 parent 7d5509a commit 8c427e6

File tree

11 files changed

+158
-27
lines changed

11 files changed

+158
-27
lines changed

substratevm/mx.substratevm/mx_substratevm.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1766,10 +1766,15 @@ def cinterfacetutorial(args):
17661766
@mx.command(suite.name, 'javaagenttest', 'Runs tests for java agent with native image')
17671767
def java_agent_test(args):
17681768
def build_and_run(args, binary_path, native_image, agents, agents_arg):
1769-
test_cp = os.pathsep.join([classpath('com.oracle.svm.test')] + agents)
1769+
mx.log('Run agent with JVM as baseline')
1770+
test_cp = os.pathsep.join([classpath('com.oracle.svm.test')])
1771+
java_run_cp = os.pathsep.join([test_cp, mx.dependency('org.graalvm.nativeimage').classpath_repr()])
1772+
mx.run_java( agents_arg + ['--add-exports=java.base/jdk.internal.org.objectweb.asm=ALL-UNNAMED',
1773+
'-cp', java_run_cp, 'com.oracle.svm.test.javaagent.AgentTest'])
1774+
test_cp = os.pathsep.join([test_cp] + agents)
17701775
native_agent_premain_options = ['-XXpremain:com.oracle.svm.test.javaagent.agent1.TestJavaAgent1:test.agent1=true', '-XXpremain:com.oracle.svm.test.javaagent.agent2.TestJavaAgent2:test.agent2=true']
1771-
image_args = ['-cp', test_cp, '-J-ea', '-J-esa', '-H:+ReportExceptionStackTraces', '-H:Class=com.oracle.svm.test.javaagent.AgentTest']
1772-
native_image(image_args + svm_experimental_options(['-H:PremainClasses=' + agents_arg]) + ['-o', binary_path] + args)
1776+
image_args = ['-cp', test_cp, '-J-ea', '-J-esa', '-H:+ReportExceptionStackTraces', '-J--add-exports=java.base/jdk.internal.org.objectweb.asm=ALL-UNNAMED', '-H:Class=com.oracle.svm.test.javaagent.AgentTest']
1777+
native_image(image_args + svm_experimental_options(agents_arg) + ['-o', binary_path] + args)
17731778
mx.run([binary_path] + native_agent_premain_options)
17741779

17751780
def build_and_test_java_agent_image(native_image, args):
@@ -1786,18 +1791,23 @@ def build_and_test_java_agent_image(native_image, args):
17861791
# Note: we are not using MX here to avoid polluting the suite.py and requiring extra build flags
17871792
mx.log("Building agent jars from " + test_classpath)
17881793
agents = []
1789-
for i in range(1, 2):
1794+
for i in range(1, 3):
17901795
agent = join(tmp_dir, "testagent%d.jar" % (i))
1791-
agent_test_classpath = join(test_classpath, 'com', 'oracle', 'svm', 'test', 'javaagent', 'agent' + str(i))
1792-
class_list = [join(test_classpath, 'com', 'oracle', 'svm', 'test', 'javaagent', 'agent' + str(i), f) for f in os.listdir(agent_test_classpath) if os.path.isfile(os.path.join(agent_test_classpath, f)) and f.endswith(".class")]
1793-
mx.run([mx.get_jdk().jar, 'cmf', join(test_classpath, 'resources', 'javaagent' + str(i), 'MANIFEST.MF'), agent] + class_list, cwd = tmp_dir)
1796+
current_dir = os.getcwd()
1797+
# Change to test classpath to create agent jar file
1798+
os.chdir(test_classpath)
1799+
agent_test_classpath = join('com', 'oracle', 'svm', 'test', 'javaagent', 'agent' + str(i))
1800+
class_list = [join('com', 'oracle', 'svm', 'test', 'javaagent', 'agent' + str(i), f) for f in os.listdir(agent_test_classpath) if os.path.isfile(os.path.join(agent_test_classpath, f)) and f.endswith(".class")]
1801+
class_list.append(join('com', 'oracle', 'svm', 'test', 'javaagent', 'AgentPremainHelper.class'))
1802+
mx.run([mx.get_jdk().jar, 'cmf', join(test_classpath, 'resources', 'javaagent' + str(i), 'MANIFEST.MF'), agent] + class_list, cwd = test_classpath)
17941803
agents.append(agent)
1804+
os.chdir(current_dir)
17951805

17961806
mx.log("Building images with different agent orders ")
1797-
build_and_run(args, join(tmp_dir, 'agenttest1'), native_image, agents,'com.oracle.svm.test.javaagent.agent1.TestJavaAgent1,com.oracle.svm.test.javaagent.agent2.TestJavaAgent2')
1807+
build_and_run(args, join(tmp_dir, 'agenttest1'), native_image, agents,[f'-javaagent:{agents[0]}=test.agent1=true', f'-javaagent:{agents[1]}=test.agent2=true'])
17981808

17991809
# Switch the premain sequence of agent1 and agent2
1800-
build_and_run(args, join(tmp_dir, 'agenttest2'), native_image, agents, 'com.oracle.svm.test.javaagent.agent2.TestJavaAgent2,com.oracle.svm.test.javaagent.agent1.TestJavaAgent1')
1810+
build_and_run(args, join(tmp_dir, 'agenttest2'), native_image, agents, [f'-javaagent:{agents[1]}=test.agent2=true', f'-javaagent:{agents[0]}=test.agent1=true'])
18011811

18021812
native_image_context_run(build_and_test_java_agent_image, args)
18031813

substratevm/mx.substratevm/suite.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -993,6 +993,7 @@
993993
"java.base" : [
994994
"jdk.internal.misc",
995995
"sun.security.jca",
996+
"jdk.internal.org.objectweb.asm"
996997
],
997998
},
998999
"checkstyle": "com.oracle.svm.test",

substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/api/HostVM.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,11 @@ public boolean isClosedTypeWorld() {
336336
return true;
337337
}
338338

339+
@SuppressWarnings("unused")
340+
public boolean isFromJavaAgent(Class<?> clazz) {
341+
return false;
342+
}
343+
339344
/**
340345
* Helpers to determine what analysis actions should be taken for a given Multi-Method version.
341346
*/

substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/infrastructure/WrappedConstantPool.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626

2727
import static jdk.vm.ci.common.JVMCIError.unimplemented;
2828

29+
import java.util.Arrays;
2930
import java.util.List;
3031
import java.util.stream.Collectors;
3132

3233
import com.oracle.graal.pointsto.constraints.UnresolvedElementException;
34+
import com.oracle.graal.pointsto.meta.AnalysisUniverse;
3335
import com.oracle.graal.pointsto.util.GraalAccess;
3436

3537
import jdk.graal.compiler.api.replacements.SnippetReflectionProvider;
@@ -41,6 +43,7 @@
4143
import jdk.vm.ci.meta.JavaType;
4244
import jdk.vm.ci.meta.ResolvedJavaMethod;
4345
import jdk.vm.ci.meta.ResolvedJavaType;
46+
import jdk.vm.ci.meta.UnresolvedJavaMethod;
4447

4548
public class WrappedConstantPool implements ConstantPool, ConstantPoolPatch {
4649

@@ -117,7 +120,26 @@ public JavaMethod lookupMethod(int cpi, int opcode) {
117120
@Override
118121
public JavaMethod lookupMethod(int cpi, int opcode, ResolvedJavaMethod caller) {
119122
try {
120-
return universe.lookupAllowUnresolved(wrapped.lookupMethod(cpi, opcode, OriginalMethodProvider.getOriginalMethod(caller)));
123+
JavaMethod ret = universe.lookupAllowUnresolved(wrapped.lookupMethod(cpi, opcode, OriginalMethodProvider.getOriginalMethod(caller)));
124+
/**
125+
* The java agent classes are loaded by appClassloader, but their dependencies could be
126+
* loaded by NativeImageClassloader. So if the required method could not be resolved, we
127+
* look further into classes loaded by nativeImageClassloader.
128+
*/
129+
if (ret instanceof UnresolvedJavaMethod && universe.hostVM().isFromJavaAgent(OriginalClassProvider.getJavaClass(caller.getDeclaringClass()))) {
130+
UnresolvedJavaMethod unresolvedResult = (UnresolvedJavaMethod) ret;
131+
String className = unresolvedResult.format("%H");
132+
String methodNameWithSignature = unresolvedResult.format("%n(%P)");
133+
try {
134+
Class<?> loadedClass = ((AnalysisUniverse) universe).getConcurrentAnalysisAccess().findClassByName(className);
135+
ResolvedJavaType resolvedType = ((AnalysisUniverse) universe).getOriginalMetaAccess().lookupJavaType(loadedClass);
136+
ResolvedJavaMethod resolvedMethod = Arrays.stream(resolvedType.getDeclaredMethods(false)).filter(m -> m.format("%n(%P)").equals(methodNameWithSignature)).findFirst().get();
137+
return universe.lookupAllowUnresolved(resolvedMethod);
138+
} catch (Exception e) {
139+
// Could not get the resolved method, get to the unresolved path
140+
}
141+
}
142+
return ret;
121143
} catch (Throwable ex) {
122144
Throwable cause = ex;
123145
if (ex instanceof ExceptionInInitializerError && ex.getCause() != null) {

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,10 @@ protected void onValueUpdate(EconomicMap<OptionKey<?>, Object> values, String ol
202202
public static OptionEnabledHandler<Boolean> imageLayerEnabledHandler;
203203
public static OptionEnabledHandler<Boolean> imageLayerCreateEnabledHandler;
204204

205+
@APIOption(name = "-javaagent", valueSeparator = ':')//
206+
@Option(help = "Enable the specified java agent in native image. Usage: -javaagent:<jarpath>[=<options>]. It's the same as using javaagent in JVM", type = User, stability = OptionStability.EXPERIMENTAL)//
207+
public static final HostedOptionKey<AccumulatingLocatableMultiOptionValue.Strings> JavaAgent = new HostedOptionKey<>(AccumulatingLocatableMultiOptionValue.Strings.build());
208+
205209
@Fold
206210
public static boolean getSourceLevelDebug() {
207211
return SourceLevelDebug.getValue();

substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/NativeImage.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,8 @@ private static <T> String oR(OptionKey<T> option) {
270270
final String oHInspectServerContentPath = oH(PointstoOptions.InspectServerContentPath);
271271
final String oHDeadlockWatchdogInterval = oH(SubstrateOptions.DeadlockWatchdogInterval);
272272

273+
final String oHJavaAgent = oH(SubstrateOptions.JavaAgent);
274+
273275
final Map<String, String> imageBuilderEnvironment = new HashMap<>();
274276
private final ArrayList<String> imageBuilderArgs = new ArrayList<>();
275277
private final Set<String> imageBuilderUniqueLeftoverArgs = Collections.newSetFromMap(new IdentityHashMap<>());
@@ -1476,6 +1478,7 @@ private List<String> getAgentArguments() {
14761478
String agentOptions = "";
14771479
List<ArgumentEntry> traceClassInitializationOpts = getHostedOptionArgumentValues(imageBuilderArgs, oHTraceClassInitialization);
14781480
List<ArgumentEntry> traceObjectInstantiationOpts = getHostedOptionArgumentValues(imageBuilderArgs, oHTraceObjectInstantiation);
1481+
List<ArgumentEntry> javaAgentOpts = getHostedOptionArgumentValues(imageBuilderArgs, oHJavaAgent);
14791482
if (!traceClassInitializationOpts.isEmpty()) {
14801483
agentOptions = getAgentOptions(traceClassInitializationOpts, "c");
14811484
}
@@ -1494,6 +1497,12 @@ private List<String> getAgentArguments() {
14941497
args.add("-agentlib:native-image-diagnostics-agent=" + agentOptions);
14951498
}
14961499

1500+
if (!javaAgentOpts.isEmpty()) {
1501+
for (ArgumentEntry javaAgentOpt : javaAgentOpts) {
1502+
args.add("-javaagent:" + javaAgentOpt.value);
1503+
}
1504+
}
1505+
14971506
return args;
14981507
}
14991508

substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/InstrumentFeature.java

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.graalvm.nativeimage.hosted.Feature;
3636

3737
import com.oracle.svm.core.PreMainSupport;
38+
import com.oracle.svm.core.SubstrateOptions;
3839
import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature;
3940
import com.oracle.svm.core.feature.InternalFeature;
4041
import com.oracle.svm.core.option.AccumulatingLocatableMultiOptionValue;
@@ -47,6 +48,9 @@
4748
import jdk.graal.compiler.options.Option;
4849
import jdk.graal.compiler.options.OptionStability;
4950

51+
import java.io.IOException;
52+
import java.util.jar.JarFile;
53+
5054
/**
5155
* This feature supports instrumentation in native image.
5256
*/
@@ -77,28 +81,50 @@ public void afterRegistration(AfterRegistrationAccess access) {
7781
PreMainSupport support = new PreMainSupport();
7882
ImageSingletons.add(PreMainSupport.class, support);
7983

80-
List<String> premainClasses = Options.PremainClasses.getValue().values();
81-
for (String clazz : premainClasses) {
82-
addPremainClass(support, cl, clazz);
84+
List<String> agentOptions = SubstrateOptions.JavaAgent.getValue().values();
85+
for (String agentOption : agentOptions) {
86+
addPremainClass(support, cl, agentOption);
8387
}
8488
}
8589

86-
private static void addPremainClass(PreMainSupport support, ClassLoader cl, String premainClass) {
90+
private static void addPremainClass(PreMainSupport support, ClassLoader cl, String javaagentOption) {
91+
int separatorIndex = javaagentOption.indexOf("=");
92+
String agent;
93+
String premainClass = null;
94+
String options = "";
95+
// Get the agent file
96+
if (separatorIndex == -1) {
97+
agent = javaagentOption;
98+
} else {
99+
agent = javaagentOption.substring(0, separatorIndex);
100+
options = javaagentOption.substring(separatorIndex + 1);
101+
}
102+
// Read MANIFEST in agent jar
103+
try {
104+
JarFile agentJarFile = new JarFile(agent);
105+
premainClass = agentJarFile.getManifest().getMainAttributes().getValue("Premain-Class");
106+
} catch (IOException e) {
107+
// This shall not happen, because at this moment GraalVM is running with -javaagent.
108+
// If the agent doesn't exist, the JVM shall fail to start.
109+
UserError.abort(e, "Can't read the agent jar %s. Please check option %s", agent,
110+
SubstrateOptionsParser.commandArgument(SubstrateOptions.JavaAgent, ""));
111+
}
112+
87113
try {
88114
Class<?> clazz = Class.forName(premainClass, false, cl);
89115
Method premain = findPremainMethod(premainClass, clazz);
90116

91117
List<Object> args = new ArrayList<>();
92118
/* The first argument contains the premain options, which will be set at runtime. */
93-
args.add("");
119+
args.add(options);
94120
if (premain.getParameterCount() == 2) {
95121
args.add(new PreMainSupport.NativeImageNoOpRuntimeInstrumentation());
96122
}
97123

98124
support.registerPremainMethod(premainClass, premain, args.toArray(new Object[0]));
99125
} catch (ClassNotFoundException e) {
100126
UserError.abort("Could not register agent premain method because class %s was not found. Please check your %s setting.", premainClass,
101-
SubstrateOptionsParser.commandArgument(Options.PremainClasses, ""));
127+
SubstrateOptionsParser.commandArgument(SubstrateOptions.JavaAgent, ""));
102128
}
103129
}
104130

substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/SVMHost.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,4 +1087,17 @@ public Set<AnalysisMethod> loadOpenTypeWorldDispatchTableMethods(AnalysisType ty
10871087
// return OpenTypeWorldFeature.loadDispatchTable(type);
10881088
return Set.of();
10891089
}
1090+
1091+
@Override
1092+
public boolean isFromJavaAgent(Class<?> clazz) {
1093+
if (SubstrateOptions.JavaAgent.hasBeenSet()) {
1094+
try {
1095+
String classLocation = clazz.getProtectionDomain().getCodeSource().getLocation().getFile();
1096+
return SubstrateOptions.JavaAgent.getValue().values().stream().map(s -> s.split("=")[0]).anyMatch(s -> s.equals(classLocation));
1097+
} catch (Exception e) {
1098+
return false;
1099+
}
1100+
}
1101+
return false;
1102+
}
10901103
}

substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/javaagent/AgentTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,20 @@ private static void testPremainSequence() {
5858
}
5959
}
6060

61+
private static void testInstrumentation() {
62+
// The return value of getCounter() should be changed by agent
63+
Assert.assertEquals(11, getCounter());
64+
}
65+
66+
private static int getCounter() {
67+
return 10;
68+
}
69+
6170
public static void main(String[] args) {
6271
testPremain();
6372
testAgentOptions();
6473
testPremainSequence();
74+
testInstrumentation();
6575
System.out.println("Finished running Agent test.");
6676
}
6777
}

substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/javaagent/agent1/TestJavaAgent1.java

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,23 @@
3434
import java.security.ProtectionDomain;
3535
import java.util.Collections;
3636
import java.util.Set;
37+
import jdk.internal.org.objectweb.asm.ClassReader;
38+
import jdk.internal.org.objectweb.asm.ClassWriter;
39+
import jdk.internal.org.objectweb.asm.ClassVisitor;
40+
import jdk.internal.org.objectweb.asm.MethodVisitor;
41+
import jdk.internal.org.objectweb.asm.Opcodes;
3742

3843
public class TestJavaAgent1 {
44+
3945
public static void premain(
4046
String agentArgs, Instrumentation inst) {
4147
AgentPremainHelper.parseOptions(agentArgs);
4248
System.setProperty("instrument.enable", "true");
49+
AgentPremainHelper.load(TestJavaAgent1.class);
4350
if (!ImageInfo.inImageRuntimeCode()) {
44-
DemoTransformer dt = new DemoTransformer("com.oracle.svm.test.javaagent.TestJavaAgent1");
51+
DemoTransformer dt = new DemoTransformer();
4552
inst.addTransformer(dt, true);
4653
} else {
47-
AgentPremainHelper.load(TestJavaAgent1.class);
4854
/**
4955
* Test {@code inst} is {@link NativeImageNoOpRuntimeInstrumentation} and behaves as
5056
* defined.
@@ -127,12 +133,15 @@ public static void premain(
127133
}
128134
}
129135

136+
/**
137+
* Change the return value of {@code AgentTest#getCounter()} from 10 to 11 in the agent.
138+
*/
130139
static class DemoTransformer implements ClassFileTransformer {
131140

132141
private String internalClassName;
133142

134-
DemoTransformer(String name) {
135-
internalClassName = name.replaceAll("\\.", "/");
143+
DemoTransformer() {
144+
internalClassName = "com/oracle/svm/test/javaagent/AgentTest";
136145
}
137146

138147
@Override
@@ -142,13 +151,36 @@ public byte[] transform(
142151
Class<?> classBeingRedefined,
143152
ProtectionDomain protectionDomain,
144153
byte[] classfileBuffer) {
145-
byte[] byteCode = classfileBuffer;
146-
147154
if (internalClassName.equals(className)) {
148-
System.out.println("Let's do transformation for " + className);
149-
// Do class transformation here
155+
ClassReader cr = new ClassReader(classfileBuffer);
156+
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
157+
158+
ClassVisitor cv = new ClassVisitor(Opcodes.ASM9, cw) {
159+
@Override
160+
public MethodVisitor visitMethod(int access, String name, String descriptor,
161+
String signature, String[] exceptions) {
162+
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
163+
if ("getCounter".equals(name) && "()I".equals(descriptor)) {
164+
return new MethodVisitor(api, mv) {
165+
@Override
166+
public void visitInsn(int opcode) {
167+
if (opcode == Opcodes.IRETURN) {
168+
super.visitLdcInsn(11);
169+
}
170+
super.visitInsn(opcode);
171+
}
172+
};
173+
}
174+
return mv;
175+
}
176+
};
177+
178+
cr.accept(cv, 0);
179+
180+
return cw.toByteArray();
150181
}
151-
return byteCode;
182+
183+
return null;
152184
}
153185
}
154186
}

0 commit comments

Comments
 (0)