使用 JDB(或类似工具)调试 ASM 生成的字节码

Debugging ASM-generated bytecode with JDB (or similar)

所以我有一些故障代码可以调试某些东西抛出 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

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

而且因为我没有用 javac 编译它,所以也没有指定任何 -g 标志。

我可以告诉 JDB 给我看字节码吗(他显然有,否则 java 就没有什么可执行的了)?

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

或者是否有有用的调试器可以满足我的需求?

生成局部变量信息相当容易。在 class 文件中发出正确的 visitLocalVariable invocations on the target method visitor, declaring name, type and scope of local variables. This will generate the LocalVariableTable attribute

当涉及到源代码级调试时,工具将简单地在目标方法访问者上查找 SourceFile attribute on the class to get the name of a text file to load and display. You can generate it by calling visitSource(fileName, null) on the target class visitor (ClassWriter). The relation between the specified text file and the byte code instructions can be declared via invocations of visitLineNumber。对于普通的源代码,你只需要在关联的行发生变化时调用它。但是对于字节码表示,每条指令都会发生变化,这可能会导致文件相当大 class,因此您绝对应该将这些调试信息的生成设为可选。

现在,您只需要生成文本文件即可。在将目标 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);
    }
}

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

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 使用它了。当您将文件放入 class 路径时,它也应该与 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

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