What am I doing
I am using ASM and javaagent to instrument classes to report their coverage (why am I not using jacoco? Well it is nothing to do with this question), the basic logic is that, every time visitLineNumber is called, I instrument some method invocation (right before visiting the next instruction) to record the hit line number.
Problem description
With such a simple logic, a class got ClassFormatError:
java.lang.ClassFormatError: StackMapTable format error: bad offset for Uninitialized in method org.apache.commons.math.ode.ContinuousOutputModelTest.buildInterpolator(D[DD)Lorg/apache/commons/math/ode/sampling/StepInterpolator;
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
...
The bytecode before the instrumentation is shown below. The instructions where the stack map frames are located are @16(offset_delta = 16) and @17(offset_delta = 0).
private org.apache.commons.math.ode.sampling.StepInterpolator buildInterpolator(double, double[], double);
descriptor: (D[DD)Lorg/apache/commons/math/ode/sampling/StepInterpolator;
flags: ACC_PRIVATE
Code:
stack=7, locals=7, args_size=4
0: new #66 // class org/apache/commons/math/ode/sampling/DummyStepInterpolator
3: dup
4: aload_3
5: dload 4
7: dload_1
8: dcmpl
9: iflt 16
12: iconst_1
13: goto 17
16: iconst_0
17: invokespecial #67 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator."<init>":([DZ)V
20: astore 6
22: aload 6
24: dload_1
25: invokevirtual #68 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator.storeTime:(D)V
28: aload 6
30: invokevirtual #69 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator.shift:()V
33: aload 6
35: dload 4
37: invokevirtual #68 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator.storeTime:(D)V
40: aload 6
42: areturn
...
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 16
locals = [ class org/apache/commons/math/ode/ContinuousOutputModelTest, double, class "[D", double ]
stack = [ uninitialized 0, uninitialized 0, class "[D" ]
frame_type = 255 /* full_frame */
offset_delta = 0
locals = [ class org/apache/commons/math/ode/ContinuousOutputModelTest, double, class "[D", double ]
stack = [ uninitialized 0, uninitialized 0, class "[D", int ]
After the instrumentation, the bytecode becomes:
private org.apache.commons.math.ode.sampling.StepInterpolator buildInterpolator(double, double[], double);
descriptor: (D[DD)Lorg/apache/commons/math/ode/sampling/StepInterpolator;
flags: ACC_PRIVATE
Code:
stack=10, locals=7, args_size=4
0: ldc_w #264 // String org/apache/commons/math/ode/ContinuousOutputModelTest
3: ldc_w #344 // String buildInterpolator
6: ldc_w #345 // int 169
9: invokestatic #272 // Method org/test/cov/CoverageCollector.reportCoverage:(Ljava/lang/String;Ljava/lang/String;I)V
12: new #66 // class org/apache/commons/math/ode/sampling/DummyStepInterpolator
15: dup
16: aload_3
17: dload 4
19: dload_1
20: dcmpl
21: iflt 28
24: iconst_1
25: goto 29
28: iconst_0
29: invokespecial #67 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator."<init>":([DZ)V
32: astore 6
34: ldc_w #264 // String org/apache/commons/math/ode/ContinuousOutputModelTest
37: ldc_w #344 // String buildInterpolator
40: ldc_w #346 // int 170
43: invokestatic #272 // Method org/test/cov/CoverageCollector.reportCoverage:(Ljava/lang/String;Ljava/lang/String;I)V
46: aload 6
48: dload_1
49: invokevirtual #68 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator.storeTime:(D)V
52: ldc_w #264 // String org/apache/commons/math/ode/ContinuousOutputModelTest
55: ldc_w #344 // String buildInterpolator
58: ldc_w #347 // int 171
61: invokestatic #272 // Method org/test/cov/CoverageCollector.reportCoverage:(Ljava/lang/String;Ljava/lang/String;I)V
64: aload 6
66: invokevirtual #69 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator.shift:()V
69: ldc_w #264 // String org/apache/commons/math/ode/ContinuousOutputModelTest
72: ldc_w #344 // String buildInterpolator
75: ldc_w #348 // int 172
78: invokestatic #272 // Method org/test/cov/CoverageCollector.reportCoverage:(Ljava/lang/String;Ljava/lang/String;I)V
81: aload 6
83: dload 4
85: invokevirtual #68 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator.storeTime:(D)V
88: ldc_w #264 // String org/apache/commons/math/ode/ContinuousOutputModelTest
91: ldc_w #344 // String buildInterpolator
94: ldc_w #349 // int 173
97: invokestatic #272 // Method org/test/cov/CoverageCollector.reportCoverage:(Ljava/lang/String;Ljava/lang/String;I)V
100: aload 6
102: areturn
...
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 28
locals = [ class org/apache/commons/math/ode/ContinuousOutputModelTest, double, class "[D", double ]
stack = [ uninitialized 0, uninitialized 0, class "[D" ]
frame_type = 255 /* full_frame */
offset_delta = 0
locals = [ class org/apache/commons/math/ode/ContinuousOutputModelTest, double, class "[D", double ]
stack = [ uninitialized 0, uninitialized 0, class "[D", int ]
I didn't see any problem with the StackMapTable. Any ideas about why this StackMapTable format is invalid?
The following is my instrumentation code:
class CoverageMethodVisitor extends MethodVisitor {
private String slashClassName;
private String methodName;
private int currentLine;
private boolean isJUnit3TestClass;
private boolean hasTestAnnotation;
private boolean isTestMethod;
private int classVersion;
private boolean isRightAfterLabel;
protected CoverageMethodVisitor(MethodVisitor methodVisitor, String className, String methodName, boolean isJUnit3TestClass, int classVersion) {
super(ASM_VERSION, methodVisitor);
this.slashClassName = className;
this.methodName = methodName;
this.isJUnit3TestClass = isJUnit3TestClass;
this.classVersion = classVersion;
}
private void instrumentReportCoverageInvocation() {
super.visitLdcInsn(slashClassName);
super.visitLdcInsn(methodName);
super.visitLdcInsn(currentLine);
super.visitMethodInsn(INVOKESTATIC, "org/test/cov/CoverageCollector",
"reportCoverage", "(Ljava/lang/String;Ljava/lang/String;I)V", false);
}
@Override
public void visitInsn(int opcode) {
if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
super.visitInsn(opcode);
}
@Override
public void visitIntInsn(int opcode, int operand) {
if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
super.visitIntInsn(opcode, operand);
}
@Override
public void visitVarInsn(int opcode, int varIndex) {
if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
super.visitVarInsn(opcode, varIndex);
}
@Override
public void visitTypeInsn(int opcode, String type) {
if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
super.visitTypeInsn(opcode, type);
}
@Override
public void visitFieldInsn(int opcode, String owner, String name, String descriptor) {
if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
super.visitFieldInsn(opcode, owner, name, descriptor);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
@Override
public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) {
if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments);
}
@Override
public void visitJumpInsn(int opcode, Label label) {
if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
super.visitJumpInsn(opcode, label);
}
@Override
public void visitLdcInsn(Object value) {
if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
super.visitLdcInsn(value);
}
@Override
public void visitIincInsn(int varIndex, int increment) {
if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
super.visitIincInsn(varIndex, increment);
}
@Override
public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) {
if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
super.visitTableSwitchInsn(min, max, dflt, labels);
}
@Override
public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
super.visitLookupSwitchInsn(dflt, keys, labels);
}
@Override
public void visitMultiANewArrayInsn(String descriptor, int numDimensions) {
if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
super.visitMultiANewArrayInsn(descriptor, numDimensions);
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack+3, maxLocals);
}
/**
* Should not report line coverage immediately after the visitLineNumber. visitLineNumber is called right after
* visitLabel, but it is very possible that a stack map frame is after the label, if insert instructions right
* after the label, the original stack map frame will be messed up. So instead, insert instructions before the
* first instruction after the label. */
@Override
public void visitLineNumber(int line, Label start) {
super.visitLineNumber(line, start);
currentLine = line;
isRightAfterLabel = true;
}
}
The comment in /** */ is actually my guess, not sure if it is correct.
EDIT:
I'm sorry I misunderstood the meaning of offset_delta = 0, as the java spec mentions:
The bytecode offset at which a frame applies is calculated by adding offset_delta + 1 to the bytecode offset of the previous frame, unless the previous frame is the initial frame of the method, ...
Summary
Reason of the error
The Uninitialized in the bad offset for Uninitialized refers to the uninitialized object produced by the NEW instruction (the first instruction in the original bytecode). Since the original stack map frame needs to use the label (let's say L0) before the NEW instruction to represent the uninitialized object in its locals and stack, and the L0 in the instrumented code no longer represents the NEW instruction, such error is thrown.
Solution
Since the L0 in the instrumented code no longer represents the NEW instruction, we need to create a new label for that NEW instruction, and replace the old label L0 with the new label in both stack map frames. If COMPUTE_FRAME is specified, such stack map frame (re)computation will be done automatically, but here we need to do it manually since COMPUTE_FRAME is not used to avoid other latent problems.