为什么在使用 StackTraceElement 时 getLineNumber return -1
Why does getLineNumber return -1 when using StackTraceElement
我想在检测 java 字节码时获取当前代码行号。 Instrumentation是通过ASM实现的。在visitcode后面插入getLineNumber对应的字节码,return值为-1,但在其他位置插桩得到的return值正常
例如源码如下
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
如您所见,我不仅获得了行号,还获得了 class 名称和方法名称。其中正常获取class名称和方法名,获取行号为-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
Attribute 给出,它将字节码位置映射到源代码行。当您使用 ASM 库转换代码时,它会注意调整代码位置以反映更改。
这意味着当您在任何原始代码之前注入代码时,与行号关联的第一个代码的位置也会进行调整,因此您的新代码不会被行号覆盖。
您可以在通过 visitLineNumber
报告第一行号后注入代码,而不是在 visitCode
上注入代码。在最好的情况下,这仍然在任何可执行代码之前(如果合成代码已经通过其他方式注入,它可能不会)。
这样,新代码就会与第一个记录的行号相关联。但是,您不需要处理堆栈跟踪来重构此信息,因为在代码注入的这一点上已经知道了。由于 class 和方法名称也是已知的,因此甚至不需要生成字符串连接代码。您可以预先 assemble 字符串。
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
我使用了一个简单的打印语句而不是你的记录器,但是这个例子应该很容易适应。
作为替代方案,如果您想尽可能保持原始逻辑,您可以只更改第一个报告的行号关联的字节码位置,以覆盖您的注入代码:
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 将按照代码位置的顺序调用访问者方法,但我们在调用 class 编写器时不需要那么严格。
所以我们可以通过在 instrument();
之前调用 visitLabel(newStart);
来将 Label
与方法的开头关联起来,而无需知道行号。到第一次调用 visitLineNumber
时,我们将表示方法原始开始的标签 start
替换为表示新开始的新标签。 ASM 不介意我们在 instrument();
之前没有调用 visitLineNumber
,因为只有与 Label
关联的代码位置才重要。
我想在检测 java 字节码时获取当前代码行号。 Instrumentation是通过ASM实现的。在visitcode后面插入getLineNumber对应的字节码,return值为-1,但在其他位置插桩得到的return值正常
例如源码如下
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
如您所见,我不仅获得了行号,还获得了 class 名称和方法名称。其中正常获取class名称和方法名,获取行号为-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
Attribute 给出,它将字节码位置映射到源代码行。当您使用 ASM 库转换代码时,它会注意调整代码位置以反映更改。
这意味着当您在任何原始代码之前注入代码时,与行号关联的第一个代码的位置也会进行调整,因此您的新代码不会被行号覆盖。
您可以在通过 visitLineNumber
报告第一行号后注入代码,而不是在 visitCode
上注入代码。在最好的情况下,这仍然在任何可执行代码之前(如果合成代码已经通过其他方式注入,它可能不会)。
这样,新代码就会与第一个记录的行号相关联。但是,您不需要处理堆栈跟踪来重构此信息,因为在代码注入的这一点上已经知道了。由于 class 和方法名称也是已知的,因此甚至不需要生成字符串连接代码。您可以预先 assemble 字符串。
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
我使用了一个简单的打印语句而不是你的记录器,但是这个例子应该很容易适应。
作为替代方案,如果您想尽可能保持原始逻辑,您可以只更改第一个报告的行号关联的字节码位置,以覆盖您的注入代码:
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 将按照代码位置的顺序调用访问者方法,但我们在调用 class 编写器时不需要那么严格。
所以我们可以通过在 instrument();
之前调用 visitLabel(newStart);
来将 Label
与方法的开头关联起来,而无需知道行号。到第一次调用 visitLineNumber
时,我们将表示方法原始开始的标签 start
替换为表示新开始的新标签。 ASM 不介意我们在 instrument();
之前没有调用 visitLineNumber
,因为只有与 Label
关联的代码位置才重要。