【问题标题】:Debugging ASM-generated bytecode with JDB (or similar)使用 JDB(或类似工具)调试 ASM 生成的字节码
【发布时间】:2018-03-20 13:38:21
【问题描述】:

所以我有一些故障代码要调试 SOMEthing 引发 NPE 的地方,我想逐步检查一些生成的方法以尝试找出原因。

除了一味地踩真的没什么用。

Thread-4[1] list
Source file not found: Foo.java
Thread-4[1] locals
Local variable information not available.  Compile with -g to generate variable information

代码已生成,所以当然没有可用于 JDB 的.java 文件。

由于我不使用 javac 编译它,因此也没有指定任何 -g 标志。

我可以告诉 JDB 向我展示字节码吗(他显然有,因为否则 java 将没有什么可以执行)?

我可以告诉 ASM 生成本地信息,就好像它是 javac -g 编译的一样?

或者是否有一个有用的调试器可以做我正在寻找的?

【问题讨论】:

    标签: java debugging bytecode java-bytecode-asm jdb


    【解决方案1】:

    生成局部变量信息相当容易。在目标方法访问者上发出正确的visitLocalVariable 调用,声明局部变量的名称、类型和范围。这将在类文件中生成LocalVariableTable attribute

    当涉及到源代码级调试时,这些工具将简单地在类中查找SourceFile attribute 以获取要加载和显示的文本文件的名称。您可以通过在目标类访问者 (ClassWriter) 上调用 visitSource(fileName, null) 来生成它。指定文本文件和字节码指令之间的关系可以通过在目标方法访问者上调用visitLineNumber 来声明。对于普通的源代码,你只需要在相关行发生变化时调用它。但是对于字节码表示,它会随着每条指令而改变,这可能会导致类文件相当大,因此您绝对应该将这些调试信息的生成设为可选。

    现在,您只需要生成文本文件。您可以将目标 ClassWriter 包装在 TraceClassVisitor 中,然后将其传递给代码生成器,以便在生成代码时生成人类可读的形式。但是我们必须扩展 ASM 提供的Textifier,因为我们需要跟踪缓冲文本的行号,并且还希望抑制为我们的行号信息本身生成输出,这会使源代码变得混乱,增加两行每条指令。

    public class LineNumberTextifier extends Textifier {
        private final LineNumberTextifier root;
        private boolean selfCall;
        public LineNumberTextifier() { super(ASM5); root = this; }
        private LineNumberTextifier(LineNumberTextifier root) { super(ASM5); this.root = root; }
        int currentLineNumber() { return count(super.text)+1; }
        private static int count(List<?> text) {
            int no = 0;
            for(Object o: text)
                if(o instanceof List) no+=count((List<?>)o);
                else {
                    String s = (String)o;
                    for(int ix=s.indexOf('\n'); ix>=0; ix=s.indexOf('\n', ix+1)) no++;
                }
            return no;
        }
        void updateLineInfo(MethodVisitor target) {
            selfCall = true;
            Label l = new Label();
            target.visitLabel(l);
            target.visitLineNumber(currentLineNumber(), l);
            selfCall = false;
        }
        // do not generate source for our own artifacts
        @Override public void visitLabel(Label label) {
            if(!root.selfCall) super.visitLabel(label);
        }
        @Override public void visitLineNumber(int line, Label start) {}
        @Override public void visitSource(String file, String debug) {}
        @Override protected Textifier createTextifier() {
            return new LineNumberTextifier(root);
        }
    }
    

    然后,你可以像这样一起生成类文件和源文件:

    Path targetPath = …
    String clName = "TestClass", srcName = clName+".jasm", binName = clName+".class";
    Path srcFile = targetPath.resolve(srcName), binFile = targetPath.resolve(binName);
    ClassWriter actualCW = new ClassWriter(0);
    try(PrintWriter sourceWriter = new PrintWriter(Files.newBufferedWriter(srcFile))) {
        LineNumberTextifier lno = new LineNumberTextifier();
        TraceClassVisitor classWriter = new TraceClassVisitor(actualCW, lno, sourceWriter);
        classWriter.visit(V1_8, ACC_PUBLIC, clName, null, "java/lang/Object", null);
        MethodVisitor constructor
            = classWriter.visitMethod(ACC_PRIVATE, "<init>", "()V", null, null);
        constructor.visitVarInsn(ALOAD, 0);
        constructor.visitMethodInsn(
            INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        constructor.visitInsn(RETURN);
        constructor.visitMaxs(1, 1);
        constructor.visitEnd();
        MethodVisitor main = classWriter.visitMethod(
            ACC_PUBLIC|ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
        Label start = new Label(), end = new Label();
        main.visitLabel(start);
        lno.updateLineInfo(main);
        main.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        lno.updateLineInfo(main);
        main.visitLdcInsn("hello world");
        lno.updateLineInfo(main);
        main.visitMethodInsn(
            INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        lno.updateLineInfo(main);
        main.visitInsn(RETURN);
        main.visitLabel(end);
        main.visitLocalVariable("arg", "[Ljava/lang/String;", null, start, end, 0);
        main.visitMaxs(2, 1);
        main.visitEnd();
        classWriter.visitSource(srcName, null);
        classWriter.visitEnd(); // writes the buffered text
    }
    Files.write(binFile, actualCW.toByteArray());
    

    它生成的“源”文件看起来像

    // class version 52.0 (52)
    // access flags 0x1
    public class TestClass {
    
    
      // access flags 0x2
      private <init>()V
        ALOAD 0
        INVOKESPECIAL java/lang/Object.<init> ()V
        RETURN
        MAXSTACK = 1
        MAXLOCALS = 1
    
      // access flags 0x9
      public static main([Ljava/lang/String;)V
       L0
        GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
        LDC "hello world"
        INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
        RETURN
       L1
        LOCALVARIABLE arg [Ljava/lang/String; L0 L1 0
        MAXSTACK = 2
        MAXLOCALS = 1
    }
    

    javap 报告

      Compiled from "TestClass.jasm"
    public class TestClass
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC
    {
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=1, args_size=1
             0: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #18                 // String hello world
             5: invokevirtual #24                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: return
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       9     0   arg   [Ljava/lang/String;
          LineNumberTable:
            line 17: 0
            line 18: 3
            line 19: 5
            line 20: 8
    }
    SourceFile: "TestClass.jasm"
    

    示例生成器将两个文件放在同一个目录中,这已经足够jdb 使用它了。当您将文件分别放入类路径时,它也应该与 IDE 调试器一起使用。项目的源路径。

    Initializing jdb ...
    > stop in TestClass.main
    Deferring breakpoint TestClass.main.
    It will be set after the class is loaded.
    > run TestClass
    run  TestClass
    Set uncaught java.lang.Throwable
    Set deferred uncaught java.lang.Throwable
    >
    VM Started: Set deferred breakpoint TestClass.main
    
    Breakpoint hit: "thread=main", TestClass.main(), line=17 bci=0
    17        GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    
    main[1] locals
    Method arguments:
    arg = instance of java.lang.String[0] (id=433)
    Local variables:
    main[1] step
    >
    Step completed: "thread=main", TestClass.main(), line=18 bci=3
    18        LDC "hello world"
    
    main[1] step
    >
    Step completed: "thread=main", TestClass.main(), line=19 bci=5
    19        INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
    
    main[1] step
    > hello world
    
    Step completed: "thread=main", TestClass.main(), line=20 bci=8
    20        RETURN
    
    main[1] step
    >
    The application exited
    

    如上所述,当您将这两个文件放入项目的类和源路径时,这也适用于 IDE。我刚刚用 Eclipse 验证了这一点:

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-03-16
      • 2020-10-09
      • 2012-11-21
      • 2012-06-02
      • 1970-01-01
      • 2014-11-30
      • 2010-09-10
      • 1970-01-01
      相关资源
      最近更新 更多