为什么使用StackTraceElement时GetlineNumber返回-1

发布于 2025-02-03 05:04:52 字数 4001 浏览 3 评论 0 原文

我想在仪器使用Java字节码时获取当前的代码行号。仪器是通过ASM实现的。在访问码之后插入与getlineNumber相对应的字节码,返回值为-1,但在其他位置通过仪器获得的返回值是正常的。

例如,根据ASM的逻辑,源代码如下

public static int add(int a, int b){
        int sum = a + b;
        return sum;
    }

,获取行号信息的字节码应在添加方法之后插入。 但是,当我在主方法中调用该函数时,获得的行号

同时为-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。

此外,只有在访问码位置之后插入才能使行号为-1,并且在其他位置插入相同的字节码将没有此问题。

这是我的仪器代码(例如Holger的代码)的一部分

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();
        }

,而是使用Visecode插入代码。

I want to get the current code line number when instrumenting the java bytecode. Instrumentation is achieved through ASM. Insert the bytecode corresponding to getLineNumber after the visitcode, the return value is -1, but the return value obtained by instrumentation in other locations is normal.

for example,the source code is as follows

public static int add(int a, int b){
        int sum = a + b;
        return sum;
    }

According to the logic of ASM, the bytecode to obtain the line number information should be inserted after the add method.
But when I call the function in the main method, the line number obtained is -1

At the same time, I also analyzed the assembly code before and after instrumentation, as follows

//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

As you can see, I get not only the line number, but also the class name and method name. Among them, the class name and method name are obtained normally, and the line number is obtained as -1.

Additionally, Only inserting after the visitcode position will let the line number be -1, and inserting the same bytecode at other positions will not have this problem.

And this is one part of my instrumentation code

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();
        }

Like Holger's code,instead I insert code by using visitcode.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

喜爱纠缠 2025-02-10 05:04:52

行号由 linenumbertable 属性哪个将字节码位置映射到源代码行。当您使用ASM库转换代码时,将需要注意调整代码位置以反映更改。

这意味着,当您在任何原始代码之前注入代码时,与行号相关的第一个代码的位置也会被调整,因此您的新代码未涵盖行号。

您可以在通过 vistcode /methodvisitor.html#visitlinenumber(Int,Objectweb.asm.label)“ rel =” nofollow noreferrer“> visitlineNumber 。在最好的情况下,这仍然是在任何可执行代码之前(如果已经通过其他方式注入合成代码)之前。

这样,新代码与第一个记录的行号相关联。但是,您无需处理堆栈跟踪即可重新构造此信息,因为它在代码注入的这一点已经知道。由于类和方法名称也是已知的,因此甚至不需要生成字符串串联代码。您可以事先组装字符串。

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会按照代码位置的顺序调用访问者方法,但在打电话给班级作者时,我们不需要那么严格。

因此,我们可以通过调用 visitlabel(newStart); instrance(); 之前将标签与方法的开头关联起来,而不知道行数字。到时, visitlineNumber 首次调用,我们替换标签 start> start ,该代表该方法的原始开始,并使用我们的新标签代表新的开始。 ASM不介意我们在 instrument(); 之前没有调用 visitlineNumber ,因为仅与 label 重要的是代码位置。

The line numbers are given by the LineNumberTable Attribute which maps bytecode locations to source code lines. When you transform code with the ASM library, it will take care to adapt code locations to reflect changes.

This implies that when you inject code before any original code, the location of the first code associated with a line number gets adapted too, so your new code is not covered by the line numbers.

Instead of injecting the code on visitCode, you may inject it after the first line number has been reported through visitLineNumber. In the best case, this still is before any executable code (it may not, if synthetic code has been injected by other means already).

This way, the new code gets associated with the first recorded line number. However, you don’t need to deal with stack traces to reconstitute this information, as it is already known at this point of code injection. Since class and method name are known too, there is not even a need to generate string concatenation code. You can assemble the string beforehand.

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

I used a simple print statement instead of your logger, but the example should be easy to adapt.


As an alternative, if you want to stay with your original logic as much as possible, you may just alter the bytecode location of the first reported line number association, to cover your injected code:

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);
    }

    …

Keep in mind that a line number reported for a code location is associated with all following instructions, until the next line number is reported. While ASM will invoke the visitor methods in the order of the code locations, we don’t need to be as strict when calling into the class writer.

So we can associate a Label with the beginning of the method by calling visitLabel(newStart); before instrument();, without knowing the line number. By the time, visitLineNumber is called for the first time, we replace the label start, which represents the original start of the method, with our new label, representing the new start. ASM doesn’t mind that we didn’t call visitLineNumber before instrument();, as only the code location associated with the Label matters.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文