8

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?

1 Answer 1

14

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:

Eclipse debugging ASM code

Sign up to request clarification or add additional context in comments.

4 Comments

what if you have bytrecode but not the source?
@Rainb the whole point of the Q&A is debugging bytecode without source code.
nevermind, I just read the tag description, move along
The first bit/para is slightly more involved if you start from a class file. But if the typing info is available as you generate bytecode (to begin with, as in the OP's Q) that part surely easier, but I'm not 100% on the valid address range for each var.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.