使用JDB调试ASM生成的字节码(或类似程序) [英] Debugging ASM-generated bytecode with JDB (or similar)

查看:111
本文介绍了使用JDB调试ASM生成的字节码(或类似程序)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

所以我有一些恶意代码来调试SOMEthing在哪里抛出NPE,我想逐步介绍一些生成的方法,以尝试找出原因.

除了盲目地步是没有用的.

  Thread-4 [1]列表找不到源文件:Foo.java线程4 [1]当地人局部变量信息不可用.用-g编译以生成变量信息 

代码已生成,因此,当然没有用于JDB的 .java 文件.

而且由于我不使用javac进行编译,因此也没有指定任何 -g 标志.

我能告诉JDB给我看一下字节码吗(他显然有字节码,因为否则Java将没有任何执行力)?

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

或者那里有一个有用的调试器可以满足我的需求?

解决方案

生成局部变量信息非常容易.发出正确的

So I have some malfuctioning code to debug where SOMEthing throws an NPE and I'd like to step through some generated methods in order to try and find out why.

Except stepping blindly is not really useful.

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

The code was generated, so of course there is no .java file available for JDB.

And since I don't compile it with javac, there's no specifying any -g flags either.

Can I tell JDB to show me the bytecode, instead (which he obviously has, because otherwise java would have had nothing to execute)?

Can I tell ASM to generate locals information as if it were compiled with javac -g?

Or is there a useful debugger out there that can do what I am looking for?

解决方案

Generating local variable information is rather easy. Emit the right visitLocalVariable invocations on the target method visitor, declaring name, type and scope of local variables. This will generate the LocalVariableTable attribute in the class file.

When it comes to source level debugging, the tools will simply look for the 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 on the target method visitor. For ordinary source code, you only have to invoke it when the associated line changes. But for a byte code representation, it would change for every instruction, which may result in a rather large class file so you should definitely make the generation of these debug information optional.

Now, you only need to produce the text file. You may wrap the target ClassWriter in a TraceClassVisitor before passing it to your code generator, to produce a human readable form while generating the code. But we have to extend the Textifier provided by ASM, as we need to track the line number of the buffered text and also want to suppress the generation of output for our line number information itself, which would clutter the source with two additional lines per instruction.

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

Then, you may generate the class file and the source file together like this:

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

The "source" file it produces looks like

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

and javap reports

  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"

The example generator placed both files into the same directory, which is already sufficient for jdb to use it. It should also work with IDE debuggers when you place the files into the class path resp. source path of a project.

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

As said, this also works with IDEs when you put the two files into the class and source paths of a project. I just verified this with Eclipse:

这篇关于使用JDB调试ASM生成的字节码(或类似程序)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆