Skip to content

Commit 4855eee

Browse files
authored
Bytecode version upgrade for invokedynamic dispatch (#9239)
1 parent 4baa694 commit 4855eee

File tree

6 files changed

+494
-0
lines changed

6 files changed

+494
-0
lines changed

javaagent-tooling/build.gradle.kts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ dependencies {
3939
implementation("io.opentelemetry.contrib:opentelemetry-aws-xray-propagator")
4040

4141
api("net.bytebuddy:byte-buddy-dep")
42+
implementation("org.ow2.asm:asm-tree")
4243

4344
annotationProcessor("com.google.auto.service:auto-service")
4445
compileOnly("com.google.auto.service:auto-service-annotations")
@@ -77,6 +78,17 @@ testing {
7778
compileOnly("com.google.code.findbugs:annotations")
7879
}
7980
}
81+
82+
val testPatchBytecodeVersion by registering(JvmTestSuite::class) {
83+
dependencies {
84+
implementation(project(":javaagent-bootstrap"))
85+
implementation(project(":javaagent-tooling"))
86+
implementation("net.bytebuddy:byte-buddy-dep")
87+
88+
// Used by byte-buddy but not brought in as a transitive dependency.
89+
compileOnly("com.google.code.findbugs:annotations")
90+
}
91+
}
8092
}
8193
}
8294

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.tooling.instrumentation.indy;
7+
8+
import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES;
9+
10+
import java.security.ProtectionDomain;
11+
import net.bytebuddy.ClassFileVersion;
12+
import net.bytebuddy.agent.builder.AgentBuilder;
13+
import net.bytebuddy.asm.Advice;
14+
import net.bytebuddy.asm.AsmVisitorWrapper;
15+
import net.bytebuddy.description.field.FieldDescription;
16+
import net.bytebuddy.description.field.FieldList;
17+
import net.bytebuddy.description.method.MethodList;
18+
import net.bytebuddy.description.type.TypeDescription;
19+
import net.bytebuddy.dynamic.DynamicType;
20+
import net.bytebuddy.implementation.Implementation;
21+
import net.bytebuddy.pool.TypePool;
22+
import net.bytebuddy.utility.JavaModule;
23+
import org.objectweb.asm.ClassVisitor;
24+
import org.objectweb.asm.MethodVisitor;
25+
import org.objectweb.asm.Opcodes;
26+
import org.objectweb.asm.commons.JSRInlinerAdapter;
27+
28+
/**
29+
* Patches the class file version to 51 (Java 7) in order to support injecting {@code INVOKEDYNAMIC}
30+
* instructions via {@link Advice.WithCustomMapping#bootstrap} which is important for indy plugins.
31+
*/
32+
public class PatchByteCodeVersionTransformer implements AgentBuilder.Transformer {
33+
34+
private static boolean isAtLeastJava7(TypeDescription typeDescription) {
35+
ClassFileVersion classFileVersion = typeDescription.getClassFileVersion();
36+
return classFileVersion != null && classFileVersion.getJavaVersion() >= 7;
37+
}
38+
39+
@Override
40+
public DynamicType.Builder<?> transform(
41+
DynamicType.Builder<?> builder,
42+
TypeDescription typeDescription,
43+
ClassLoader classLoader,
44+
JavaModule javaModule,
45+
ProtectionDomain protectionDomain) {
46+
47+
if (isAtLeastJava7(typeDescription)) {
48+
// we can avoid the expensive stack frame re-computation if stack frames are already present
49+
// in the bytecode.
50+
return builder;
51+
}
52+
return builder.visit(
53+
new AsmVisitorWrapper.AbstractBase() {
54+
@Override
55+
public ClassVisitor wrap(
56+
TypeDescription typeDescription,
57+
ClassVisitor classVisitor,
58+
Implementation.Context context,
59+
TypePool typePool,
60+
FieldList<FieldDescription.InDefinedShape> fieldList,
61+
MethodList<?> methodList,
62+
int writerFlags,
63+
int readerFlags) {
64+
65+
return new ClassVisitor(Opcodes.ASM7, classVisitor) {
66+
67+
@Override
68+
public void visit(
69+
int version,
70+
int access,
71+
String name,
72+
String signature,
73+
String superName,
74+
String[] interfaces) {
75+
76+
super.visit(Opcodes.V1_7, access, name, signature, superName, interfaces);
77+
}
78+
79+
@Override
80+
public MethodVisitor visitMethod(
81+
int access,
82+
String name,
83+
String descriptor,
84+
String signature,
85+
String[] exceptions) {
86+
87+
MethodVisitor methodVisitor =
88+
super.visitMethod(access, name, descriptor, signature, exceptions);
89+
return new JSRInlinerAdapter(
90+
methodVisitor, access, name, descriptor, signature, exceptions);
91+
}
92+
};
93+
}
94+
95+
@Override
96+
public int mergeWriter(int flags) {
97+
// class files with version < Java 7 don't require a stack frame map
98+
// as we're patching the version to at least 7, we have to compute the frames
99+
return flags | COMPUTE_FRAMES;
100+
}
101+
});
102+
}
103+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.tooling.instrumentation.indy;
7+
8+
import net.bytebuddy.asm.AsmVisitorWrapper;
9+
import net.bytebuddy.description.field.FieldDescription;
10+
import net.bytebuddy.description.field.FieldList;
11+
import net.bytebuddy.description.method.MethodList;
12+
import net.bytebuddy.description.type.TypeDescription;
13+
import net.bytebuddy.implementation.Implementation;
14+
import net.bytebuddy.jar.asm.ClassWriter;
15+
import net.bytebuddy.pool.TypePool;
16+
import org.objectweb.asm.ClassVisitor;
17+
18+
public class ComputeFramesAsmVisitorWrapper extends AsmVisitorWrapper.AbstractBase {
19+
@Override
20+
public int mergeWriter(int flags) {
21+
return super.mergeWriter(flags | ClassWriter.COMPUTE_FRAMES);
22+
}
23+
24+
@Override
25+
public ClassVisitor wrap(
26+
TypeDescription instrumentedType,
27+
ClassVisitor classVisitor,
28+
Implementation.Context implementationContext,
29+
TypePool typePool,
30+
FieldList<FieldDescription.InDefinedShape> fields,
31+
MethodList<?> methods,
32+
int writerFlags,
33+
int readerFlags) {
34+
return classVisitor;
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.tooling.instrumentation.indy;
7+
8+
import java.lang.reflect.Modifier;
9+
import net.bytebuddy.ByteBuddy;
10+
import net.bytebuddy.ClassFileVersion;
11+
import net.bytebuddy.asm.AsmVisitorWrapper;
12+
import net.bytebuddy.description.method.MethodDescription;
13+
import net.bytebuddy.dynamic.DynamicType;
14+
import net.bytebuddy.dynamic.scaffold.InstrumentedType;
15+
import net.bytebuddy.implementation.Implementation;
16+
import net.bytebuddy.implementation.bytecode.ByteCodeAppender;
17+
import org.objectweb.asm.Label;
18+
import org.objectweb.asm.MethodVisitor;
19+
import org.objectweb.asm.Opcodes;
20+
import org.objectweb.asm.Type;
21+
22+
public class OldBytecode {
23+
24+
private OldBytecode() {}
25+
26+
/**
27+
* Generates and run a simple class with a {@link #toString()} implementation as if it had been
28+
* compiled on an older java compiler
29+
*
30+
* @param className class name
31+
* @param version bytecode version
32+
* @return "toString"
33+
*/
34+
public static String generateAndRun(String className, ClassFileVersion version) {
35+
try (DynamicType.Unloaded<Object> unloadedClass = makeClass(className, version)) {
36+
Class<?> generatedClass = unloadedClass.load(OldBytecode.class.getClassLoader()).getLoaded();
37+
38+
return generatedClass.getConstructor().newInstance().toString();
39+
40+
} catch (Throwable e) {
41+
throw new RuntimeException(e);
42+
}
43+
}
44+
45+
private static DynamicType.Unloaded<Object> makeClass(
46+
String className, ClassFileVersion version) {
47+
return new ByteBuddy(version)
48+
.subclass(Object.class)
49+
// required otherwise stack frames aren't computed when needed
50+
.visit(
51+
version.isAtLeast(ClassFileVersion.JAVA_V7)
52+
? new ComputeFramesAsmVisitorWrapper()
53+
: AsmVisitorWrapper.NoOp.INSTANCE)
54+
.name(className)
55+
.defineMethod("toString", String.class, Modifier.PUBLIC)
56+
.intercept(new ToStringMethod())
57+
.make();
58+
}
59+
60+
private static class ToStringMethod implements Implementation, ByteCodeAppender {
61+
62+
@Override
63+
public ByteCodeAppender appender(Target implementationTarget) {
64+
return this;
65+
}
66+
67+
@Override
68+
public InstrumentedType prepare(InstrumentedType instrumentedType) {
69+
return instrumentedType;
70+
}
71+
72+
@Override
73+
public Size apply(
74+
MethodVisitor methodVisitor,
75+
Context implementationContext,
76+
MethodDescription instrumentedMethod) {
77+
78+
// Bytecode archeology:
79+
//
80+
// JSR and RET bytecode instructions were used to create "subroutines". Those were used
81+
// in try/catch blocks as an attempt to avoid some bytecode duplication, this was later
82+
// replaced with inlining.
83+
// Starting from Java 5, no java compiler is expected to issue bytecode containing them and
84+
// the JVM bytecode validation will reject it.
85+
//
86+
// Java 7 bytecode introduced the concept of "stack map frames", which describe the types of
87+
// the objects that are stored on the stack during method body execution.
88+
//
89+
// As a consequence, the code below allows to test the following combinations:
90+
// - java 1 to java 4 bytecode with JSR/RET opcodes
91+
// - java 5 and java 6 bytecode without stack map frames
92+
// - java 7 and later bytecode with stack map frames, those are automatically added by the
93+
// ComputeFramesAsmVisitorWrapper.
94+
//
95+
boolean useJsrRet =
96+
implementationContext.getClassFileVersion().isLessThan(ClassFileVersion.JAVA_V5);
97+
98+
if (useJsrRet) {
99+
// return "toString";
100+
//
101+
// using obsolete JSR/RET instructions
102+
Label target = new Label();
103+
methodVisitor.visitJumpInsn(Opcodes.JSR, target);
104+
105+
methodVisitor.visitVarInsn(Opcodes.ALOAD, 1);
106+
methodVisitor.visitInsn(Opcodes.ARETURN);
107+
methodVisitor.visitLabel(target);
108+
methodVisitor.visitVarInsn(Opcodes.ASTORE, 2);
109+
methodVisitor.visitLdcInsn("toString");
110+
methodVisitor.visitVarInsn(Opcodes.ASTORE, 1);
111+
methodVisitor.visitVarInsn(Opcodes.RET, 2);
112+
return new Size(1, 3);
113+
} else {
114+
// try {
115+
// return "toString";
116+
// } catch (Throwable e) {
117+
// return e.getMessage();
118+
// }
119+
//
120+
// the Throwable exception is added to stack map frames with java7+, and needs to be
121+
// added when upgrading the bytecode
122+
Label start = new Label();
123+
Label end = new Label();
124+
Label handler = new Label();
125+
126+
methodVisitor.visitTryCatchBlock(
127+
start, end, handler, Type.getInternalName(Throwable.class));
128+
methodVisitor.visitLabel(start);
129+
methodVisitor.visitLdcInsn("toString");
130+
methodVisitor.visitLabel(end);
131+
132+
methodVisitor.visitInsn(Opcodes.ARETURN);
133+
134+
methodVisitor.visitLabel(handler);
135+
methodVisitor.visitVarInsn(Opcodes.ASTORE, 1);
136+
methodVisitor.visitVarInsn(Opcodes.ALOAD, 1);
137+
138+
methodVisitor.visitMethodInsn(
139+
Opcodes.INVOKEVIRTUAL,
140+
Type.getInternalName(Throwable.class),
141+
"getMessage",
142+
Type.getMethodDescriptor(Type.getType(String.class)),
143+
false);
144+
methodVisitor.visitInsn(Opcodes.ARETURN);
145+
146+
return new Size(1, 2);
147+
}
148+
}
149+
}
150+
}

0 commit comments

Comments
 (0)