最近在做java的插桩,希望获取到源代码中对应基本块位置的一些信息。比如说获取到基本块所在的类,它所对应的方法,以及它处在源代码中的哪个位置。
在网上发现可以通过stacktrace的信息去获取
1.使用方法
查阅了官方的API,发现Thread类有一个getStackTrace的方法,如下所示,发现它跟踪线程并获取堆栈帧,以数组形式返回。
获取到stackTraceElement信息之后,可以根据它自定义的方法获取到自己想要的信息。比如说获取类名,方法名,行号等等。
2.示例
总共就是写了两个方法:add方法和subs方法,在main方法中调用add方法,同时在add方法中调用subs方法。
主要目的在于去测试它的堆栈帧数组的大小。
public static void main(String[] args) {
System.out.println("in method main ---->");
System.out.println("main method stacktrace length is : "+Thread.currentThread().getStackTrace().length);
add(1,2);
}
public static int add(int a , int b){
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
System.out.println("in method add ---->");
System.out.println(stackTrace.length);
for(StackTraceElement trace:stackTrace){
System.out.println("method name is :"+trace.getMethodName());
System.out.println("line number is : "+trace.getLineNumber());
}
subs(a,b);
return a+b;
}
public static int subs(int a ,int b){
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
System.out.println("in method subs ---->");
System.out.println(stackTrace.length);
for(StackTraceElement trace:stackTrace){
System.out.println("method name is :"+trace.getMethodName());
System.out.println("line number is : "+trace.getLineNumber());
}
return a-b;
}
实验结果如下所示:
可以看到在main方法中堆栈帧的大小是2,add方法堆栈帧大小为3,而subs方法堆栈帧大小为4。
同时可以看到返回的堆栈帧数组的第一个元素都是getStrackTrace,表示堆栈帧的顶部,每个方法的堆栈帧顶部都是一样的。
从add和subs方法的堆栈帧信息看出,根据方法的调用关系,堆栈信息也是对应的。比如subs方法,调用顺序为:main->add->subs,所以可以看到subs的堆栈帧的元素中method name从subs->add->main。所以堆栈的最底部信息是最开始调用的堆栈信息,顶部的信息是最近调用堆栈的信息。对应到StackTraceElement数组中,第一个为getStrackTrace堆栈帧,后面一个为最近的调用堆栈帧,最后一个为最开始的调用堆栈帧。
3.问题
根据前面的分析,可以看到,StackTraceElement数组下标为1的时候就是我们需要的信息,比如method name,line number等等。这个就是最近调用堆栈的信息。
但是我发现在插桩的时候,在对应位置插入获取第二个堆栈帧信息的代码,在获取到行号的时候,就出现了-1的情况。
根据前面官方提到的,如下所示。包含此堆栈跟踪元素表示的执行点的源行的行号,如果此信息不可用,则为负数。
从官方文档看到:在某些情况下,某些虚拟机可能会从堆栈跟踪中省略一个或多个堆栈帧。 在极端情况下,允许没有关于此线程的堆栈跟踪信息的虚拟机从此方法返回零长度数组。
但是在实际运行的过程中,并没有报数组越界的错误,所以应该不是没有堆栈帧的问题。
最终目前还没有找到解决方法,后续再看一下。
最近发现了前面的行号出现-1的问题,是StackOverflow上的一位大佬帮忙解答的,非常感谢,下面是StackOverflow的链接,感兴趣的可以点击看一下。
https://stackoverflow.com/questions/72430477/why-does-getlinenumber-return-1-when-using-stacktraceelement
问题分析
请记住,为代码位置报告的行号与紧接着的所有指令相关联,直到报告下一个行号。当使用asm去调用方法访问者获取行号的时候,不必要严格遵循asm的类访问者的顺序。
首先,在visitcode的时候,如果插桩获取行号信息,此时的行号信息是不可用的,也就是会返回-1。
当第一次访问visitLineNumber是在第一条指令之前,从visitLineNumber的参数就可以看到,它是需要行号和标签的,所以在visitcode的位置后面插桩,此时并不知道行号信息,所以获取也是无效的操作。
通常,第一个 visitLineNumber 将发生在第一条指令之前,因此仍处于方法的开头。所以无法获取信息在知道信息之前,因此尝试在较早的时间注入引用行号的代码不适用于 ASM 的单次访问者概念
所以只有在visitLineNumber之后才会知道行号的信息,同时根据asm的访问顺序,visitcode是在visitLineNumber之前的。但是asm并不介意在插桩之前没有调用visitLineNumber
所以最终解决方法,就是在visitcode之后调用visitLabel,把一个标签和方法开头联系起来。然后在visitLineNumber的时候把对应的标签换成我们新创建的标签,所以当第一次调用visitLineNumber的时候,把开始的标签位置换成了visitcode位置处的标签,那么就可以获取到行号信息了。
// 这是那位大佬写的demo
// 每一个方法访问者都创建一个标签,用于在方法开始位置。
// 同时在visitLineNumber的时候更改开始的标签,并注意修改条件即可。
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;
}
// 这样做把第一次visitlinenumber强行转换到方法开始位置
super.visitLineNumber(line, start);
}
…
1.更新
后面再次去看ASM的用户手册的时候发现了visitLineNumber的描述,如下所示。才发现,在asm中这个函数可以去获取行号,它的第二个参数就是一个标签,指的就是此行号对应的第一条指令。同时也可以注意到,如果说该标签没有被visitLabel访问的话,就会抛出一个异常。
同时在ASM的逻辑中,visitCode也确实是在visitLineNumber之前访问的。所以如果在visitCode之后插入获取行号信息的代码,此时行号信息是不可用得。这是因为插入的代码在visitLineNumber之前尝试去获取行号信息,但是这时候行号信息都还不知道,所以才会出现-1的情况。
所以可以在visitCode之后新创建一个标签,然后使用visitLabel访问标签,再进行插桩。同时把第一个访问visitLineNumber的标签替换为我们新创建的标签,此时行号信息已经知道,所以在获取行号的时候就不会出错。
2.demo
下面是我写的demo
上面的ASMcode是我通过IDEA的插件生成的,有兴趣的可以自己下载一个,名字叫ASM Bytecode Outline
可以看到上面正常获取到行号信息,获取到行号信息的代码都是在visitLineNumber之后的,这也说明了如果在visitCode之后插桩并不能获取信息。
按照生成的asm code 的逻辑,就可以在visitCode后创建一个标签并且使用visitLabel访问,然后再使用visitLineNumber把第一次调用的开始标签替换为自己创建的标签。
可能有人会问,那visitLineNumber原来的行号是我们想要的吗
实际上第一次访问visitLineNumber的行号就是我们需要的行号,它就是方法开始位置处的行号。
可能又有人问,把开始的标签替换为新的标签之后,原来的标签没有访问到怎么办
我个人猜测这个标签没有被visitLineNumber访问,也无所谓。