Skip to content

Commit c75ffbb

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 261c38a commit c75ffbb

File tree

11 files changed

+161
-32
lines changed

11 files changed

+161
-32
lines changed

substratevm/mx.substratevm/mx_substratevm.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1817,10 +1817,15 @@ def cinterfacetutorial(args):
18171817
@mx.command(suite.name, 'javaagenttest', 'Runs tests for java agent with native image')
18181818
def java_agent_test(args):
18191819
def build_and_run(args, binary_path, native_image, agents, agents_arg):
1820-
test_cp = os.pathsep.join([classpath('com.oracle.svm.test')] + agents)
1820+
mx.log('Run agent with JVM as baseline')
1821+
test_cp = os.pathsep.join([classpath('com.oracle.svm.test')])
1822+
java_run_cp = os.pathsep.join([test_cp, mx.dependency('org.graalvm.nativeimage').classpath_repr()])
1823+
mx.run_java( agents_arg + ['--add-exports=java.base/jdk.internal.org.objectweb.asm=ALL-UNNAMED',
1824+
'-cp', java_run_cp, 'com.oracle.svm.test.javaagent.AgentTest'])
1825+
test_cp = os.pathsep.join([test_cp] + agents)
18211826
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']
1822-
image_args = ['-cp', test_cp, '-J-ea', '-J-esa', '-H:+ReportExceptionStackTraces', '-H:Class=com.oracle.svm.test.javaagent.AgentTest']
1823-
native_image(image_args + svm_experimental_options(['-H:PremainClasses=' + agents_arg]) + ['-o', binary_path] + args)
1827+
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']
1828+
native_image(image_args + svm_experimental_options(agents_arg) + ['-o', binary_path] + args)
18241829
mx.run([binary_path] + native_agent_premain_options)
18251830

18261831
def build_and_test_java_agent_image(native_image, args):
@@ -1837,18 +1842,23 @@ def build_and_test_java_agent_image(native_image, args):
18371842
# Note: we are not using MX here to avoid polluting the suite.py and requiring extra build flags
18381843
mx.log("Building agent jars from " + test_classpath)
18391844
agents = []
1840-
for i in range(1, 2):
1845+
for i in range(1, 3):
18411846
agent = join(tmp_dir, "testagent%d.jar" % (i))
1842-
agent_test_classpath = join(test_classpath, 'com', 'oracle', 'svm', 'test', 'javaagent', 'agent' + str(i))
1843-
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")]
1844-
mx.run([mx.get_jdk().jar, 'cmf', join(test_classpath, 'resources', 'javaagent' + str(i), 'MANIFEST.MF'), agent] + class_list, cwd = tmp_dir)
1847+
current_dir = os.getcwd()
1848+
# Change to test classpath to create agent jar file
1849+
os.chdir(test_classpath)
1850+
agent_test_classpath = join('com', 'oracle', 'svm', 'test', 'javaagent', 'agent' + str(i))
1851+
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")]
1852+
class_list.append(join('com', 'oracle', 'svm', 'test', 'javaagent', 'AgentPremainHelper.class'))
1853+
mx.run([mx.get_jdk().jar, 'cmf', join(test_classpath, 'resources', 'javaagent' + str(i), 'MANIFEST.MF'), agent] + class_list, cwd = test_classpath)
18451854
agents.append(agent)
1855+
os.chdir(current_dir)
18461856

18471857
mx.log("Building images with different agent orders ")
1848-
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')
1858+
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'])
18491859

18501860
# Switch the premain sequence of agent1 and agent2
1851-
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')
1861+
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'])
18521862

18531863
native_image_context_run(build_and_test_java_agent_image, args)
18541864

substratevm/mx.substratevm/suite.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,7 @@
10421042
"java.base" : [
10431043
"jdk.internal.misc",
10441044
"sun.security.jca",
1045+
"jdk.internal.org.objectweb.asm"
10451046
],
10461047
},
10471048
"checkstyle": "com.oracle.svm.test",

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,10 @@ public boolean enableReachableInCurrentLayer() {
360360
return false;
361361
}
362362

363+
public boolean isFromJavaAgent(@SuppressWarnings("unused") Class<?> clazz) {
364+
return false;
365+
}
366+
363367
/**
364368
* Helpers to determine what analysis actions should be taken for a given Multi-Method version.
365369
*/

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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,13 @@ protected void onValueUpdate(EconomicMap<OptionKey<?>, Object> values, String ol
224224
public static OptionEnabledHandler<Boolean> imageLayerEnabledHandler;
225225
public static OptionEnabledHandler<Boolean> imageLayerCreateEnabledHandler;
226226

227+
@APIOption(name = "-javaagent", valueSeparator = ':')//
228+
@Option(help = "Enable the specified java agent in native image. Usage: -javaagent:<jarpath>[=<options>]. " +
229+
"The java agent will run at image build time to take its effects in the output native image. " +
230+
"Be noticed: The java agent's premain method will be re-executed at native image runtime. " +
231+
"The agent should isolate the executions according to runtime environment.", type = User, stability = OptionStability.EXPERIMENTAL)//
232+
public static final HostedOptionKey<AccumulatingLocatableMultiOptionValue.Strings> JavaAgent = new HostedOptionKey<>(AccumulatingLocatableMultiOptionValue.Strings.build());
233+
227234
@Fold
228235
public static boolean getSourceLevelDebug() {
229236
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
@@ -286,6 +286,8 @@ private static <T> String oR(OptionKey<T> option) {
286286
final String oHInspectServerContentPath = oH(PointstoOptions.InspectServerContentPath);
287287
final String oHDeadlockWatchdogInterval = oH(SubstrateOptions.DeadlockWatchdogInterval);
288288

289+
final String oHJavaAgent = oH(SubstrateOptions.JavaAgent);
290+
289291
final Map<String, String> imageBuilderEnvironment = new HashMap<>();
290292
private final ArrayList<String> imageBuilderArgs = new ArrayList<>();
291293
private final Set<String> imageBuilderUniqueLeftoverArgs = Collections.newSetFromMap(new IdentityHashMap<>());
@@ -1517,6 +1519,7 @@ private List<String> getAgentArguments() {
15171519
String agentOptions = "";
15181520
List<ArgumentEntry> traceClassInitializationOpts = getHostedOptionArgumentValues(imageBuilderArgs, oHTraceClassInitialization);
15191521
List<ArgumentEntry> traceObjectInstantiationOpts = getHostedOptionArgumentValues(imageBuilderArgs, oHTraceObjectInstantiation);
1522+
List<ArgumentEntry> javaAgentOpts = getHostedOptionArgumentValues(imageBuilderArgs, oHJavaAgent);
15201523
if (!traceClassInitializationOpts.isEmpty()) {
15211524
agentOptions = getAgentOptions(traceClassInitializationOpts, "c");
15221525
}
@@ -1535,6 +1538,12 @@ private List<String> getAgentArguments() {
15351538
args.add("-agentlib:native-image-diagnostics-agent=" + agentOptions);
15361539
}
15371540

1541+
if (!javaAgentOpts.isEmpty()) {
1542+
for (ArgumentEntry javaAgentOpt : javaAgentOpts) {
1543+
args.add("-javaagent:" + javaAgentOpt.value);
1544+
}
1545+
}
1546+
15381547
return args;
15391548
}
15401549

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

Lines changed: 33 additions & 7 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 should never happen because the image build process (HotSpot) already loaded the
108+
// agent during startup.
109+
throw 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) {
100-
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, ""));
126+
throw UserError.abort("Could not register agent premain method because class %s was not found. Please check your %s setting.", premainClass,
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
@@ -1151,4 +1151,17 @@ public boolean allowConstantFolding(AnalysisMethod method) {
11511151
public SimulateClassInitializerSupport createSimulateClassInitializerSupport(AnalysisMetaAccess aMetaAccess) {
11521152
return new SimulateClassInitializerSupport(aMetaAccess, this);
11531153
}
1154+
1155+
@Override
1156+
public boolean isFromJavaAgent(Class<?> clazz) {
1157+
if (SubstrateOptions.JavaAgent.hasBeenSet()) {
1158+
try {
1159+
String classLocation = clazz.getProtectionDomain().getCodeSource().getLocation().getFile();
1160+
return SubstrateOptions.JavaAgent.getValue().values().stream().map(s -> s.split("=")[0]).anyMatch(s -> s.equals(classLocation));
1161+
} catch (Exception e) {
1162+
return false;
1163+
}
1164+
}
1165+
return false;
1166+
}
11541167
}

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)