我想在检测java字节码时获得当前代码行号。检测是通过ASM实现的。在visitcode后面插入getLineNumber对应的字节码,返回值为-1,但在其他位置检测得到的返回值正常
以为例,源代码如下
public static int add(int a, int b){
int sum = a + b;
return sum;
}
根据ASM的逻辑,获取行号信息的字节码应该在add方法之后插入。但是当我在main方法中调用这个函数时,得到的行号是-1
同时,我还分析了插装前后的汇编代码,如下
//this is before instrumentation
public static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: istore_2
4: iload_2
5: ireturn
//this is after instrumentation
public static int add(int, int);
Code:
0: new #33 // class java/lang/StringBuilder
3: dup
4: invokespecial #34 // Method java/lang/StringBuilder."<init>":()V
7: ldc #36 // String _
9: invokevirtual #40 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
12: invokestatic #46 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
15: invokevirtual #50 // Method java/lang/Thread.getStackTrace:()[Ljava/lang/StackTraceElement;
18: iconst_1
19: aaload
20: invokevirtual #56 // Method java/lang/StackTraceElement.getLineNumber:()I
23: invokevirtual #59 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
26: invokevirtual #63 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
29: invokestatic #69 // Method afljava/logger/Logger.writeToLogger:(Ljava/lang/String;)V
32: iload_0
33: iload_1
34: iadd
35: istore_2
36: iload_2
37: ireturn
如您所见,我不仅得到行号,还得到类名和方法名。其中,类名和方法名正常获取,行号为-1。
另外,只有在visitcode位置之后插入字节码才会让行号为-1,在其他位置插入相同的字节码不会有这个问题。
这是我的仪器代码的一部分
private void instrument(){
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Thread", "currentThread", "()Ljava/lang/Thread;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Thread", "getName", "()Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitLdcInsn("_" + classAndMethodName + "_");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Thread", "currentThread", "()Ljava/lang/Thread;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Thread", "getStackTrace", "()[Ljava/lang/StackTraceElement;", false);
mv.visitInsn(Opcodes.ICONST_1);
mv.visitInsn(Opcodes.AALOAD);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StackTraceElement", "getLineNumber", "()I", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(I)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "afljava/logger/Logger", "writeToLogger", "(Ljava/lang/String;)V", false);
}
@Override
public void visitCode() {
super.visitCode();
instrument();
}
与Holger的代码一样,我使用visitcode插入代码。
行号由LineNumberTable
属性给出,该属性将字节码位置映射到源代码行。当您使用ASM库转换代码时,它会注意调整代码位置以反映更改。
这意味着当您在任何原始代码之前注入代码时,与行号相关的第一个代码的位置也会被调整,因此您的新代码不会被行号覆盖。
您可以在visitLineNumber
报告了第一个行号之后注入代码,而不是在visitCode
上注入。在最好的情况下,这仍然是在任何可执行代码之前(如果合成代码已经通过其他方式注入,则可能不是)。
这样,新代码与第一个记录的行号相关联。但是,您不需要处理堆栈跟踪来重新构造该信息,因为在代码注入的这一点上已经知道了。由于类和方法名也是已知的,因此甚至不需要生成字符串连接代码。您可以预先组装字符串。
package com.example;
import java.lang.invoke.MethodHandles;
import org.objectweb.asm.*;
public class AsmExample {
static class Test {
public static int add(int a, int b){
int sum = a + b;
return sum;
}
}
public static void main(String[] args) throws Exception {
ClassReader cr = new ClassReader(AsmExample.class.getName()+"$Test");
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
cr.accept(new ClassVisitor(Opcodes.ASM9, cw) {
String className;
@Override
public void visit(int ver,
int acc, String name, String sig, String superName, String[] ifs) {
super.visit(ver, acc, name, sig, superName, ifs);
className = name.replace('/', '.');
}
@Override
public MethodVisitor visitMethod(
int acc, String name, String desc, String sig, String[] ex) {
MethodVisitor mv = super.visitMethod(acc, name, desc, sig, ex);
if(name.equals("add")) mv = new Injector(mv, className + '_' + name);
return mv;
}
}, 0);
MethodHandles.lookup().defineClass(cw.toByteArray());
System.out.println("return value: " + Test.add(30, 12));
}
static class Injector extends MethodVisitor {
private final String classAndMethodName;
private boolean logStatementAdded;
public Injector(MethodVisitor methodVisitor, String classAndMethod) {
super(Opcodes.ASM9, methodVisitor);
classAndMethodName = classAndMethod;
}
@Override
public void visitLineNumber(int line, Label start) {
super.visitLineNumber(line, start);
if(!logStatementAdded) {
logStatementAdded = true;
visitFieldInsn(Opcodes.GETSTATIC,
"java/lang/System", "out", "Ljava/io/PrintStream;");
visitLdcInsn(classAndMethodName + "_" + line);
visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}
}
}
com.example.AsmExample$Test_add_10
return value: 42
我使用了一个简单的print语句,而不是您的日志记录器,但是这个示例应该很容易适应。
作为一种选择,如果您希望尽可能地保持原始逻辑,您可以只更改第一个报告的行号关联的字节码位置,以覆盖您的注入代码:
static class Injector extends MethodVisitor {
private final String classAndMethodName;
Label newStart = new Label();
public Injector(MethodVisitor methodVisitor, String classAndMethod) {
super(Opcodes.ASM9, methodVisitor);
classAndMethodName = classAndMethod;
}
@Override
public void visitCode() {
super.visitCode();
visitLabel(newStart);
instrument();
}
@Override
public void visitLineNumber(int line, Label start) {
if(newStart != null) {
start = newStart;
newStart = null;
}
super.visitLineNumber(line, start);
}
…
请记住,为代码位置报告的行号与以下所有指令相关联,直到报告下一个行号。虽然ASM将按照代码位置的顺序调用访问者方法,但在调用类编写器时不需要如此严格。
因此,我们可以通过在instrument();
之前调用visitLabel(newStart);
来将Label
与方法的开头联系起来,而不知道行号。当visitLineNumber
第一次被调用时,我们用新的标签替换了代表方法起始点的标签start
。ASM并不介意我们在instrument();
之前不调用visitLineNumber
,因为只有与Label
相关的代码位置才重要。