latte
is a CLI utility which parses the bytecode of a stripped Java class file and attempts to re-construct a reasonble fascimile of each missing Local Variable Table.
javac
can optionally include debugging info - including Local Variables Tables - in the compiled Java .class
file.
Consider the following - extremely contrived - sample Java program to print the contents of a text file.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class PrintFile {
public static void main(String args[]) {
Path filePath = null;
if (args.length != 1) {
System.err.println("Must specify a file");
}
else {
filePath = Paths.get(args[0]);
try {
System.out.println(Files.readString(filePath));
}
catch (IOException e) {
String errMsg = String.format("Failed to read file %s: %s", args[0], e.toString());
System.err.println(errMsg);
}
}
}
}
When compiling, the -g
option can be specified to javac
to output debugging info, like so:
$ javac -g PrintFile.java
$ javap -l PrintFile.class
Compiled from "PrintFile.java"
public class PrintFile {
public PrintFile();
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LPrintFile;
public static void main(java.lang.String[]);
LineNumberTable:
line 8: 0
line 9: 2
line 10: 8
line 13: 19
line 15: 30
line 20: 40
line 17: 43
line 18: 44
line 19: 67
line 23: 74
LocalVariableTable:
Start Length Slot Name Signature
67 7 3 errMsg Ljava/lang/String;
44 30 2 e Ljava/io/IOException;
0 75 0 args [Ljava/lang/String;
2 73 1 filePath Ljava/nio/file/Path;
The Local Variable Tables facilitate debugging; specifically, they allow inspection of the methods arguments and local variables at each step of the method:
$ jdb PrintFile /etc/issue
Initializing jdb ...
> stop in PrintFile.main
Deferring breakpoint PrintFile.main.
It will be set after the class is loaded.
> run
run PrintFile /etc/issue
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
>
VM Started: Set deferred breakpoint PrintFile.main
Breakpoint hit: "thread=main", PrintFile.main(), line=8 bci=0
8 Path filePath = null;
main[1] locals
Method arguments:
args = instance of java.lang.String[1] (id=436)
Local variables:
main[1] dump args
args = {
"/etc/issue"
}
However, if the debugging information is omitted - without access to the original Java source code - debugging becomes much trickier:
$ javac -g:none PrintFile.java
$ javap -l PrintFile.class
public class PrintFile {
public PrintFile();
public static void main(java.lang.String[]);
}
$ jdb PrintFile /etc/issue
Initializing jdb ...
> stop in PrintFile.main
Deferring breakpoint PrintFile.main.
It will be set after the class is loaded.
> run
run PrintFile /etc/issue
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
>
VM Started: Set deferred breakpoint PrintFile.main
Breakpoint hit: "thread=main", PrintFile.main(), line=-1 bci=0
main[1] locals
Local variable information not available. Compile with -g to generate variable information
latte
will attempt to build an approximation of the Local Variables Tables by analyzing the bytecode of a Java .class
- or .jar
- file.
$ java -jar app/build/libs/latte-0.1.0.jar PrintFile.class
Examining class PrintFile
Examining method <init>
Examining method main
Overwrite the existing file PrintFile.class? (Y/n)
Y
Overwriting existing file PrintFile.class
$ javap -l PrintFile.class
public class PrintFile {
public PrintFile();
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LPrintFile;
public static void main(java.lang.String[]);
LocalVariableTable:
Start Length Slot Name Signature
0 75 0 arg1 [Ljava/lang/String;
2 73 1 local1 Ljava/nio/file/Path;
44 31 2 local2 Ljava/io/IOException;
67 8 3 local3 Ljava/lang/String;
}
This allows for inspection of the local variables during debugging.
$ jdb PrintFile /etc/issue
Initializing jdb ...
> stop in PrintFile.main
Deferring breakpoint PrintFile.main.
It will be set after the class is loaded.
> run
run PrintFile /etc/issue
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
>
VM Started: Set deferred breakpoint PrintFile.main
Breakpoint hit: "thread=main", PrintFile.main(), line=-1 bci=0
main[1] locals
Method arguments:
arg1 = instance of java.lang.String[1] (id=437)
Local variables:
main[1] dump arg1
arg1 = {
"/etc/issue"
}
$ git clone https://github.com/mathewmarcus/latte.git
$ cd latte
$ ./gradlew build
usage: java -jar latte.jar INPUT_CLASS_OR_JAR [-c <arg>] [-f] [-h] [-i <arg>] [-j] [-o
<arg>]
Rebuild the local variable tables in a class file
-c,--class-path <arg> A : separated list of directories, JAR
archives,and ZIP archives to search for class
files.
-f,--force do not prompt before overwriting an existing
output file
-h,--help
-i,--include <arg> list of classes to modify (default: all)
-j,--is-jar whether the input file is a JAR
-o,--output <arg> output file
The target file; either a .class
file (default) or a .jar
file (see --jar
)
Specifies that the target file is a .jar
Write the modified class(es)/JAR to a different file; by default the input file will be overwritten
Don't prompt before overwriting an existing file.
Explicitly limit the classes which latte
should analyze/modify within a JAR file.
$ java -jar latte.jar -j -i com.example.A com.example.B input.jar
In order to handle local variable typing, latte
will attempt to resolve and load classes. If the target file has any external dependencies, the location to these can be specified via a class path string.
$ java -jar latte.jar --class-path dependency.jar:/path/to/more/jar/dependencies/* input.class
- investigate whether ASM event-based API would be better than object-based API
- code cleanup
- more unit tests