我在做什么
我正在使用 ASM 和 javaagent 来检测类以报告它们的覆盖率(为什么我不使用 jacoco?嗯,这与这个问题无关),基本逻辑是,每次 visitLineNumber
被调用时,我检测了一些方法调用(就在访问下一条指令之前)以记录命中行号。
问题描述
这么简单的逻辑,一个类得到了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)
...
检测前的字节码如下所示。堆栈映射帧所在的指令是@16(offset_delta = 16)和@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 ]
检测后,字节码变为:
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 ]
我没有发现 StackMapTable 有任何问题。关于此 StackMapTable 格式为何无效的任何想法?
以下是我的检测代码:
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;
}
}
/** */
中的注释其实是我的猜测,不确定是否正确。
编辑:
很抱歉,我误解了 offset_delta = 0
的含义,因为 java spec提及:
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, ...
总结
错误原因
bad offset for Uninitialized
中的Uninitialized
指的是由NEW
指令(原始字节码中的第一条指令)产生的未初始化对象).由于原始堆栈映射框架需要在 NEW
指令之前使用标签(比如 L0
)来表示其 locals
中的未初始化对象,并且stack
,检测代码中的L0
不再代表NEW
指令,抛出此类错误。
解决方案
由于检测代码中的 L0
不再代表 NEW
指令,我们需要为该 NEW
指令创建一个新标签,并在两个堆栈映射框架中用新标签替换旧标签 L0
。如果指定了COMPUTE_FRAME
,这样的堆栈映射帧(重新)计算将自动完成,但这里我们需要手动完成,因为COMPUTE_FRAME
没有被使用以避免其他潜在问题.
最佳答案
作为Rafael Winterhalter said ,标签用于引用 NEW
指令,因此将指令放在标签位置和指令之间会破坏此引用。当指令创建的未初始化实例位于堆栈或局部变量中时,堆栈映射框架需要此类引用。
由于在 NEW
指令之前插入代码时要保留原始位置作为分支目标或行号开始,因此必须为注入(inject)代码和插入代码之间的代码位置创建一个新标签old NEW
指令,然后替换堆栈映射框架使用的标签(并且仅替换堆栈映射框架使用的标签)。
请注意,当使用 COMPUTE_FRAMES
选项时,这不是必需的,该选项将从头开始重新计算帧并且不使用旧标签,但是,不使用此选项实际上是一个合理的策略,因为它很昂贵且容易出错。为了注入(inject)没有分支的简单日志记录语句,我们可以保留原始帧,即使检查 NEW
指令的特殊情况会使它稍微困难一些。
以下方法访问者实现上述策略。对于我的测试,我只是注入(inject)了一个普通的 System.out.println(...);
语句。
class CoverageMethodVisitor extends MethodVisitor {
private final String slashClassName;
private final String methodName;
private final Map<Label,Label> translateForUninitialized = new HashMap<>();
private Label lastLabel;
private int newLineNumber = -1;
protected CoverageMethodVisitor(MethodVisitor mv,String clName,String methodName){
super(Opcodes.ASM9, mv);
this.slashClassName = clName;
this.methodName = methodName;
}
@Override
public void visitLineNumber(int line, Label start) {
newLineNumber = line;
super.visitLineNumber(line, start);
}
@Override
public void visitLabel(Label label) {
lastLabel = label;
super.visitLabel(label);
}
@Override
public void visitTypeInsn(int opcode, String type) {
if(instrumentReportCoverageInvocation() && opcode == Opcodes.NEW) {
if(lastLabel != null) {
Label newLabel = new Label();
super.visitLabel(newLabel);
translateForUninitialized.put(lastLabel, newLabel);
}
}
super.visitTypeInsn(opcode, type);
}
@Override
public void visitFrame(int type,
int numLocal, Object[] local, int numStack, Object[] stack) {
switch(type) {
case Opcodes.F_NEW, Opcodes.F_FULL -> {
local = replaceLabels(numLocal, local);
stack = replaceLabels(numStack, stack);
}
case Opcodes.F_APPEND -> local = replaceLabels(numLocal, local);
case Opcodes.F_CHOP, Opcodes.F_SAME -> {}
case Opcodes.F_SAME1 -> stack = replaceLabels(1, stack);
default -> throw new AssertionError();
}
super.visitFrame(type, numLocal, local, numStack, stack);
}
private Object[] replaceLabels(int num, Object[] array) {
Object[] result = array;
for(int ix = 0; ix < num; ix++) {
Label repl = translateForUninitialized.get(result[ix]);
if(repl == null) continue;
if(result == array) result = array.clone();
result[ix] = repl;
}
return result;
}
private boolean instrumentReportCoverageInvocation() {
int lineNumber = newLineNumber;
if(lineNumber < 0) return false;
newLineNumber = -1;
super.visitFieldInsn(Opcodes.GETSTATIC,
"java/lang/System", "out", "Ljava/io/PrintStream;");
super.visitLdcInsn(slashClassName + "." + methodName + " line " + lineNumber);
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
return true;
}
@Override
public void visitLdcInsn(Object value) {
instrumentReportCoverageInvocation();
super.visitLdcInsn(value);
}
@Override
public void visitInsn(int opcode) {
instrumentReportCoverageInvocation();
super.visitInsn(opcode);
}
@Override
public void visitIntInsn(int opcode, int operand) {
instrumentReportCoverageInvocation();
super.visitIntInsn(opcode, operand);
}
@Override
public void visitVarInsn(int opcode, int varIndex) {
instrumentReportCoverageInvocation();
super.visitVarInsn(opcode, varIndex);
}
@Override
public void visitFieldInsn(int opcode,
String owner, String name, String descriptor) {
instrumentReportCoverageInvocation();
super.visitFieldInsn(opcode, owner, name, descriptor);
}
@Override
public void visitMethodInsn(int opcode,
String owner, String name, String descriptor, boolean isInterface) {
instrumentReportCoverageInvocation();
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
@Override
public void visitInvokeDynamicInsn(
String name, String descriptor, Handle bootstrapMethodHandle,
Object... bootstrapMethodArguments) {
instrumentReportCoverageInvocation();
super.visitInvokeDynamicInsn(
name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments);
}
@Override
public void visitJumpInsn(int opcode, Label label) {
instrumentReportCoverageInvocation();
super.visitJumpInsn(opcode, label);
}
@Override
public void visitIincInsn(int varIndex, int increment) {
instrumentReportCoverageInvocation();
super.visitIincInsn(varIndex, increment);
}
@Override
public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) {
instrumentReportCoverageInvocation();
super.visitTableSwitchInsn(min, max, dflt, labels);
}
@Override
public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
instrumentReportCoverageInvocation();
super.visitLookupSwitchInsn(dflt, keys, labels);
}
@Override
public void visitMultiANewArrayInsn(String descriptor, int numDimensions) {
instrumentReportCoverageInvocation();
super.visitMultiANewArrayInsn(descriptor, numDimensions);
}
@Override
public AnnotationVisitor visitInsnAnnotation(
int typeRef, TypePath tp, String desc, boolean visible) {
instrumentReportCoverageInvocation();
return super.visitInsnAnnotation(typeRef, tp, desc, visible);
}
}
我使用了一个直截了当的类访问者
class CoverageClassVisitor extends ClassVisitor {
private String className;
CoverageClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM9, cv);
}
@Override
public void visit(
int version, int acc, String name, String sig, String superName, String[] ifs) {
className = name;
super.visit(version, acc, name, sig, superName, ifs);
}
@Override
public MethodVisitor visitMethod(int access,
String name, String descriptor, String signature, String[] exceptions) {
return new CoverageMethodVisitor(
super.visitMethod(access, name, descriptor, signature, exceptions),
className,
name);
}
}
……以及下面的测试代码
public class ReportLineNumbers {
public static void main(String[] arg) throws IOException, IllegalAccessException {
String className = ReportLineNumbers.class.getName() + "$Example";
// ToolProvider.findFirst("javap")
// .ifPresent(p -> p.run(System.out, System.err, "-c", className));
ClassReader cr = new ClassReader(className);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
CoverageClassVisitor trans = new CoverageClassVisitor(cw);
cr.accept(trans, 0);
MethodHandles.lookup().defineClass(cw.toByteArray());
Example.method();
}
static class Example {
static void method() {
for(int i = 0; i < 3; i++) {
System.out.println(
i < 2?
new String(switch(i) {
case 0 -> {
// try {
yield "0";
// } catch(Throwable t) {
// yield "e";
// }
}
default -> {
for(int j = 1; j < 3; j++) {
System.out.println(j);
}
yield "3";
}
}):
new String(i == 2?
"4".toCharArray():
"5".toCharArray())
);
}
}
}
}
Example
类的设计尽可能具有挑战性,涵盖NEW
指令前后的不同分支类型(向前、向后、切换)。事实上,事实证明它是如此具有挑战性,以至于我不得不注释掉一个结构,就像 Eclipse 一样,即使没有转换器,它也会产生无效的字节码。但是当使用 javac
时,您可以启用这个内联的 try … catch …
构造,以查看转换器是否会正确处理它。
关于java - ASM 和 Javaagent 字节码检测 : ClassFormatError: StackMapTable format error: bad offset for Uninitialized,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/72877465/