使用JDB(或类似)

时间:2018-03-20 13:38:22

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

所以我有一些故障代码来调试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进行编译一样?

或者是否有一个有用的调试器可以做我想要的?

1 个答案:

答案 0 :(得分:3)

生成局部变量信息非常简单。在目标方法访问者上发出正确的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使用它。当您将文件放入类路径resp时,它也应该与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验证了这一点:

Eclipse debugging ASM code