我已经为方法创建了一个小型记录器,并且使用了ASM。我需要通过描述符方法参数确定并打印出来。但是我有一个错误
Exception in thread "main" java.lang.VerifyError: Bad type on operand stack
Exception Details:
Location:
ru/otus/TestLogging.calc(IFD)V @6: invokedynamic
Reason:
Type 'java/io/PrintStream' (current frame, stack[4]) is not assignable to double_2nd
Current Frame:
bci: @6
flags: { }
locals: { 'ru/otus/TestLogging', integer, float, double, double_2nd }
stack: { 'ru/otus/TestLogging', float, double, double_2nd, 'java/io/PrintStream' }
Bytecode:
0000000: 2a24 29b2 0007 ba00 3e00 00b6 0011 b200
0000010: 071b 2429 ba00 0d00 00b6 0011 b1
这是我的代理代码
public class Agent {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
if(className.contains("ru/otus/")) {
return changeMethod(classfileBuffer, className);
}
return classfileBuffer;
}
});
}
private static byte[] changeMethod(byte[] originalClass, String className) {
ClassReader reader = new ClassReader(originalClass);
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
ArrayList<String> list = new ArrayList<>();
ClassVisitor visitor = new ClassVisitor(Opcodes.ASM5, writer) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions){
System.out.println("visitMethod: access="+access+" name="+name+" desc="+descriptor+" signature="+signature+" exceptions="+exceptions);
Method thisMethod = new Method(name, descriptor);
MethodVisitor mv = new MethodAnnotationScanner(Opcodes.ASM5, super.visitMethod(access, name, descriptor, signature, exceptions), thisMethod, className);
return mv;
}
};
reader.accept(visitor, Opcodes.ASM5);
for(String methodName : list) {
System.out.println(methodName);
}
byte[] finalClass = writer.toByteArray();
if(className.contains("Test")) {
try (OutputStream fos = new FileOutputStream("TestLogging.class")) {
fos.write(finalClass);
} catch (Exception e) {
e.printStackTrace();
}
}
return writer.toByteArray();
}
static class MethodAnnotationScanner extends MethodVisitor {
private Method thisMethod;
private boolean isChangeMethod = false;
private String className = null;
private StringBuilder descriptor = new StringBuilder("(");
public MethodAnnotationScanner(int api, MethodVisitor methodVisitor, Method thisMethod, String className) {
super(api, methodVisitor);
this.thisMethod = thisMethod;
this.className = className;
}
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
System.out.println("visitAnnotation: desc="+desc+" visible="+visible);
if(desc.contains("ru/otus/annotations/Log")) {
this.isChangeMethod = true;
return super.visitAnnotation(desc, visible);
}
this.isChangeMethod = false;
return super.visitAnnotation(desc, visible);
}
@Override
public void visitCode() {
if(this.isChangeMethod) {
super.visitVarInsn(Opcodes.ALOAD, 0);
int i = 1;
for(Type arg : thisMethod.getArgumentTypes()) {
this.descriptor.append(arg.getDescriptor());
if (arg.getDescriptor().equals("J")) {
super.visitVarInsn(Opcodes.LLOAD, i);
++i;
} else if (arg.getDescriptor().equals("D")) {
super.visitVarInsn(Opcodes.DLOAD, i);
++i;
} else if (arg.getDescriptor().equals("F")) {
super.visitVarInsn(Opcodes.FLOAD, i);
} else if(arg.getDescriptor().equals("I")) {
super.visitVarInsn(Opcodes.ILOAD, i);
}
i++;
}
Handle handle = new Handle(
H_INVOKESTATIC,
Type.getInternalName(java.lang.invoke.StringConcatFactory.class),
"makeConcatWithConstants",
MethodType.methodType(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, String.class, Object[].class).toMethodDescriptorString(),
false);
this.descriptor.append(")Ljava/lang/String;");
super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
super.visitInvokeDynamicInsn("makeConcatWithConstants", this.descriptor.toString(), handle, "executed method: " + this.thisMethod.getName() + ", param: \u0001".repeat(i));
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
super.visitMaxs(0, 0);
}
if (mv != null) {
super.visitCode();
}
super.visitEnd();
}
}
}
我有两个类来重现此功能。首先-TestLogging次-AutoLogger
在第一堂课中,我有一个需要记录的方法,第二秒,它的开始类包含方法main。
您在读取字段System.out
之前先推动字符串连接的参数,然后尝试执行字符串连接。因此,用于执行字符串连接的invokedynamic
指令在操作数堆栈上发现不匹配的PrintStream
。
一个简单的解决方法是更改说明
super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
super.visitInvokeDynamicInsn("makeConcatWithConstants", this.descriptor.toString(), handle, "executed method: " + this.thisMethod.getName() + ", param: \u0001".repeat(i));
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
to
super.visitInvokeDynamicInsn("makeConcatWithConstants", this.descriptor.toString(), handle, "executed method: " + this.thisMethod.getName() + ", param: \u0001".repeat(i));
super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
super.visitInsn(Opcodes.SWAP);
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
或将GETSTATIC
移到将执行concat参数的代码之前,移至执行过时的super.visitVarInsn(Opcodes.ALOAD, 0);
的位置。然后,您不需要SWAP
。
但是代码有更多问题。您在推入值时正在计算局部变量,正确地考虑了long
和double
取两个变量,但是随后,您在", param: \u0001".repeat(i)
表达式中使用了相同的数字,这将告诉StringConcatFactory
在long
和double
的情况下是两个值。您需要分开计数器。另外,您不是要推送引用类型参数,而是由于要对它们进行计数并将其包含在concat调用的签名中,因此还必须将它们推送到操作数堆栈。
[此外,visitMaxs(0, 0)
调用和visitEnd()
调用虽然在这里没有效果,但不合适。您正在注入代码的开头,随后将进行其他不会被拦截的访问,包括自动执行的visitMaxs
和visitEnd
。
使用所有修复程序,代码看起来像
public void visitCode() {
if(this.isChangeMethod) {
super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
int varIndex = 1, numArgs = 0;
for(Type arg : thisMethod.getArgumentTypes()) {
this.descriptor.append(arg.getDescriptor());
if (arg.getDescriptor().equals("J")) {
super.visitVarInsn(Opcodes.LLOAD, varIndex);
++varIndex;
} else if (arg.getDescriptor().equals("D")) {
super.visitVarInsn(Opcodes.DLOAD, varIndex);
++varIndex;
} else if (arg.getDescriptor().equals("F")) {
super.visitVarInsn(Opcodes.FLOAD, varIndex);
} else if(arg.getDescriptor().equals("I")) {
super.visitVarInsn(Opcodes.ILOAD, varIndex);
} else {
super.visitVarInsn(Opcodes.ALOAD, varIndex);
}
varIndex++;
numArgs++;
}
Handle handle = new Handle(
H_INVOKESTATIC,
Type.getInternalName(java.lang.invoke.StringConcatFactory.class),
"makeConcatWithConstants",
MethodType.methodType(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, String.class, Object[].class).toMethodDescriptorString(),
false);
this.descriptor.append(")Ljava/lang/String;");
super.visitInvokeDynamicInsn("makeConcatWithConstants", this.descriptor.toString(), handle, "executed method: " + this.thisMethod.getName() + ", param: \u0001".repeat(numArgs));
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
super.visitCode();
}
作为旁注,当为串联推送所有参数时,可以简化描述符的构造,因为concat描述符与方法的描述符几乎相同;您只需将返回类型替换为Ljava/lang/String;
。
仅使用已经传递给Method
的两个字符串,就可以完成整个操作而无需处理Type
和visitMethod
对象。
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
System.out.println("visitMethod: access="+access+" name="+name
+" desc="+descriptor+" signature="+signature+" exceptions="+exceptions);
MethodVisitor mv = new MethodAnnotationScanner(Opcodes.ASM5, name, descriptor,
super.visitMethod(access, name, descriptor, signature, exceptions));
return mv;
}
static class MethodAnnotationScanner extends MethodVisitor {
private boolean isChangeMethod;
private final String name, descriptor;
public MethodAnnotationScanner(int api, String name,
String methodDesciptor, MethodVisitor methodVisitor){
super(api, methodVisitor);
this.name = name;
this.descriptor = methodDesciptor;
}
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
System.out.println("visitAnnotation: desc="+desc+" visible="+visible);
if(desc.contains("ru/otus/annotations/Log")) {
this.isChangeMethod = true;
return super.visitAnnotation(desc, visible);
}
this.isChangeMethod = false;
return super.visitAnnotation(desc, visible);
}
@Override
public void visitCode() {
if(this.isChangeMethod) {
super.visitFieldInsn(Opcodes.GETSTATIC,
"java/lang/System", "out", "Ljava/io/PrintStream;");
int varIndex = 1, numArgs = 0, p;
for(p = 1; descriptor.charAt(p) != ')'; p++) {
switch(descriptor.charAt(p)) {
case 'J':
super.visitVarInsn(Opcodes.LLOAD, varIndex); ++varIndex; break;
case 'D':
super.visitVarInsn(Opcodes.DLOAD, varIndex); ++varIndex; break;
case 'F': super.visitVarInsn(Opcodes.FLOAD, varIndex); break;
case 'I': super.visitVarInsn(Opcodes.ILOAD, varIndex); break;
case 'L': super.visitVarInsn(Opcodes.ALOAD, varIndex);
p = descriptor.indexOf(';', p);
break;
case '[': super.visitVarInsn(Opcodes.ALOAD, varIndex);
do {} while(descriptor.charAt(++p)=='[');
if(descriptor.charAt(p) == 'L') p = descriptor.indexOf(';', p);
break;
default: throw new IllegalStateException(descriptor);
}
varIndex++;
numArgs++;
}
String ret = "Ljava/lang/String;";
String concatSig = new StringBuilder(++p + ret.length())
.append(descriptor, 0, p).append(ret).toString();
Handle handle = new Handle(
H_INVOKESTATIC,
"java/lang/invoke/StringConcatFactory",
"makeConcatWithConstants",
MethodType.methodType(CallSite.class, MethodHandles.Lookup.class,
String.class, MethodType.class, String.class, Object[].class)
.toMethodDescriptorString(),
false);
super.visitInvokeDynamicInsn("makeConcatWithConstants", concatSig, handle,
"executed method: " + name + ", param: \u0001".repeat(numArgs));
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
super.visitCode();
}
}