How to generate class file with java ASM? - java

I have tried
ClassWriter t = new ClassWriter(0);
t.visitSource("testing.java", null);
t.visitEnd();
byte d[] = t.toByteArray();
FileOutputStream p = null;
try
{
p = new FileOutputStream("testing.class");
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
try
{
p.write(d);
}
catch (IOException e)
{
e.printStackTrace();
}
And the text within the testing.java is:
public class testing
{
public static void main(String args[])
{
System.out.println("Works!");
}
}
However, When I try to run the class file, it gives me this error:
Exception in thread "main" java.lang.UnsupportedClassVersionError: testing : Unsupported major.minor version 0.0
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:631)
at java.lang.ClassLoader.defineClass(ClassLoader.java:615)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:141)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:283)
at java.net.URLClassLoader.access$000(URLClassLoader.java:58)
at java.net.URLClassLoader$1.run(URLClassLoader.java:197)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
How would I fix it so that the class file would output "Works!" to the console?
Edit:
I don't want it to have to access the jdk files! (<- Ex. javax.tools) That's why I was trying to get ASM to work.

Seems that you are trying to compile Java source file into a class file. That can be done with the Java compiler - the javac command line program or the tools in the javax.tools package.
ASM is for a different purpose. ASM can be used to create class files on-the-fly, without any source code. Read ASM's documentation to learn about Java bytecode and how to produce and read it with ASM.
Here is how a file is compiled using javax.tools package. Or then you could invoke the command line tools using Process. Check the documentation for additional arguments - what classpath to use, where to write the files etc.
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
public class TestingCompile {
public static void main(String[] args) {
JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
int result = javac.run(null, null, null, "C:\\path\\to\\Testing.java");
if (result != 0) {
throw new RuntimeException("compile failed: exit " + result);
}
}
}
Here is how to create the same class file using ASM, without using the source file. I'm quite sure this is not what you want to be doing - otherwise you wouldn't have had to ask the question. ;)
This is just the output of ASMifierClassVisitor, so the bytes would still need to be written to a file or loaded dynamically into the class loader. I used the -debug argument so that ASMifier would show also the source file name and line numbers (the visitSource, visitLineNumber and visitLocalVariable calls are optional, so you could omit them and the related labels if the debug information is not needed).
import org.objectweb.asm.*;
public class TestingDump implements Opcodes {
public static byte[] dump() throws Exception {
ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;
cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER, "Testing", null, "java/lang/Object", null);
cw.visitSource("Testing.java", null);
{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(1, l0);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
mv.visitInsn(RETURN);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLocalVariable("this", "LTesting;", null, l0, l1, 0);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(3, l0);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Works!");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(4, l1);
mv.visitInsn(RETURN);
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLocalVariable("args", "[Ljava/lang/String;", null, l0, l2, 0);
mv.visitMaxs(2, 1);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
}

I believe the problem is that the library expects a minimum set of certain methods will be called and I believe you don't have enough methods to have it generate a full class.
I suggest you use ASMifier to generate some templates.
You have corrupted the file in some way. There was no version 0.0

You can invoke the Java compiler from your source code to compile any Java source code to the class files. The compiler is written in Java itself and part of the standard jdk release. Try to look for a class called javac.

Related

Why would retransformClasses fail silently?

I'm trying to retransform classes with java instrumentation. The code below runs just fine, it prints Hello World!!.
public class Main {
public static void main(String[] args) throws Throwable {
var inst = MyAgent.getInstrumentation();
inst.addTransformer(new ClassFileTransformer() {
#Override
public byte[] transform(ClassLoader loader, String name, Class<?> c, ProtectionDomain pd, byte[] b) {
if (c != Main.class) return null;
var cr = new ClassReader(b);
var cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
cr.accept(new ClassVisitor(ASM9, cw) {
#Override
public MethodVisitor visitMethod(int acc, String name, String desc, String sig, String[] exc) {
var mv = super.visitMethod(acc, name, desc, sig, exc);
if ("foo".equals(name)) {
return new MethodVisitor(ASM9, mv) {
#Override
public void visitCode() {
super.visitCode();
visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
visitLdcInsn("Hello ");
visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "print", "(Ljava/lang/String;)V", false);
}
};
}
return mv;
}
}, 0);
return cw.toByteArray();
}
}, true);
inst.retransformClasses(Main.class);
foo();
}
public static void foo() {
System.out.println("World!!");
}
}
However, when the ClassFileTranformer returns invalid bytecode, or when it just returns new byte[0], the retransformation fails silently. No exceptions are thrown and no changes are done. According to the official documentation, if invalid bytes are provided, the method should throw a exception. The doc says:
The class file bytes are not checked, verified and installed until after the transformations have been applied, if the resultant bytes are in error this method will throw an exception.
Using the instrumentation object outside the java agent doesn't seem to be a problem, I tried to put the retransformation code in the agent and the behaviours are exactly the same. I'm using GraalVM EE (the version is shown below). I tried OpenJDK as well and it still fails silently.
java version "17.0.6" 2023-01-17 LTS
Java(TM) SE Runtime Environment GraalVM EE 22.3.1 (build 17.0.6+9-LTS-jvmci-22.3-b11)
Java HotSpot(TM) 64-Bit Server VM GraalVM EE 22.3.1 (build 17.0.6+9-LTS-jvmci-22.3-b11, mixed mode, sharing)
Why it fails silently? Is it because I'm using instrumentation in a wrong way? How can I get a exception when the bytecode is malformed? Thanks in advance.
(Thanks to Johannes Kuhn, corrected the unsupported use of ASM Api)

Java Compiler keeps saying "error while writing className: className.class (Permission denied)"

I'm working on a Tomcat WebApp for my university which enables students to compile their Java codes and see the trace. I'm installing it on a RHEL7 VM. But when I test the compilation function (this one is not implemented by me), the method I'm providing returns this:
error while writing className: className.class (Permission denied)
Error on line 1 in className.java
I'll show you the method I think is generating this:
public String compileJavaCode(String javaCode, String javaFileName, File workingDir) throws IOException, TimeoutException{
javax.tools.JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);
this.createJavaFile(javaCode, javaFileName, workingDir);
JavaFileObject file = new JavaSourceFromString(javaFileName, javaCode);
Iterable<? extends JavaFileObject> compilationUnits = Arrays.asList(file);
compiler.getTask(null, fileManager, diagnostics, null, null, compilationUnits).call();
String diagn = "";
for ( Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()){
diagn+=diagnostic.getMessage(null)+"\n";//E.g. cannot find symbol symbol: variable variablename
diagn+="Error on line "+Long.toString(diagnostic.getLineNumber())+" in "+diagnostic.getSource().toUri();//E.g. Error on line 22 in ClassName.java
}
fileManager.close();
compiler.run(null, null, null, workingDir.getAbsolutePath()+File.separator+javaFileName);
return diagn;
}
Students will see the content of that diagn variable as a result for their code submission.
Fun fact is that I manage to get the className.class in the workingDir directory but I keep getting that error from the for cycle above. Could the problem be compiler.getTask(...).call()? I mean maybe compiler.run is able to generate the .class correctly but the compiler.getTask(...).call() is trying to write the .class somewhere else I don't have permission to write in.
P.S. This is a pretty legacy code so please be merciful with it. :)
As asked by #Alexander, this is the content of the Java file:
public class Sommatore {
public int somma(int i, int j) {
return i+j;
}
public int differenza(int i, int j) {
return i-j;
}
}
Seems like the user you are using doesn't have the permissions to write to the destination folder. What are the permissions of the workingDir?
Fun fact is that i manage to get the className.class in the workingDir directory but i keep getting that error from the for cycle above. Could the problem be the compiler.getTask(...).call()? I mean maybe compiler.run is able to generate the .class correctly but the compiler.getTask(...).call() is trying to write the .class somewhere else i don't have permission to write in.
In order to verify if this is true, you could create a folder with open permissions and try.
For example, you could try using as workingDir = /tmp and check what happens.
EDIT
I tried to replicate your code:
public class JavaCompiler {
public static void main(String args[]) throws IOException, TimeoutException {
File dir = new File(System.getProperty("user.dir") + "/src/main/java/");
System.out.println(compileJavaCode(dir));
}
public static String compileJavaCode(File workingDir) throws IOException, TimeoutException {
javax.tools.JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);
// this.createJavaFile(javaCode, javaFileName, workingDir);
// JavaFileObject file = new JavaSourceFromString(javaFileName, javaCode);
Iterable<? extends JavaFileObject> compilationUnits = fileManager
.getJavaFileObjectsFromStrings(Arrays.asList(System.getProperty("user.dir") + "/src/main/java/Foo.java"));
// Iterable<? extends JavaFileObject> compilationUnits = Arrays.asList(file);
compiler.getTask(null, fileManager, diagnostics, null, null, compilationUnits).call();
String diagn = "";
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
diagn += diagnostic.getMessage(null) + "\n";//E.g. cannot find symbol symbol: variable variablename
diagn += "Error on line " + Long.toString(diagnostic.getLineNumber()) + " in " + diagnostic.getSource().toUri();//E.g. Error on line 22 in ClassName.java
}
fileManager.close();
compiler.run(null, null, null, workingDir.getAbsolutePath() + File.separator + "Foo.java");
return diagn;
}
}
with Foo.java
public class Foo {
public int somma(int i, int j) {
return i+j;
}
public int differenza(int i, int j) {
return i-j;
}
}
There are some changes, but the result should be the same.
I noticed that the "path" is specified in
File workingDir
that will be use in
compiler.run(null, null, null, workingDir.getAbsolutePath() + File.separator + "Foo.java");
and in
JavaFileObject file = new JavaSourceFromString(javaFileName, javaCode);
Iterable<? extends JavaFileObject> compilationUnits = Arrays.asList(file);
that in my example:
Iterable<? extends JavaFileObject> compilationUnits = fileManager
.getJavaFileObjectsFromStrings(Arrays.asList(System.getProperty("user.dir") + "/src/main/java/Foo.java"));
What contains yours
workingDir
and "file"?
JavaFileObject file = new JavaSourceFromString(javaFileName, javaCode);
In my case, are the same.
I tried to execute the code with different users, and if I use an user that isn't able to write in this folder I obtain
/tmp/testSO/src/main/java/Foo.java:5: error: error while writing Foo: /tmp/testSO/src/main/java/Foo.class (Permission denied)
public class Foo {
^
1 error
error while writing Foo: /tmp/testSO/src/main/java/Foo.class (Permission denied)
Error on line 5 in file:/tmp/testSO/src/main/java/Foo.java

Get dependencies Using ASM5.0.2

I have a Java program based on ASM 5.0.2 to extract dependency between classes. The program works fine with an ordinary Java application. However, when I run the program as a plugin then it crashes with the bug: java.lang.ClassNotFoundException.
As an example if the example class uses junit.Assert, then when I run the project as an ordinary java application, it find this dependency, but when as plugin the below error:
java.lang.ClassNotFoundException: org.junit.Assert
at java.net.URLClassLoader$1.run(URLClassLoader.java:372)
at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.net.FactoryURLClassLoader.loadClass(URLClassLoader.java:798)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:340)
Part of code that I think error is because of that is as below, and the whole code can be find in this link:
class ClassCollector extends Remapper {
static Set<Class<?>> getClassesUsedBy(final String name, final String prefix, File root) throws IOException {
final ClassReader reader = new ClassReader(name);
final Set<Class<?>> classes = new TreeSet<Class<?>> (new Comparator<Class<?>>() {
#Override
public int compare (final Class<?> o1, final Class<?> o2) {
return o1.getName().compareTo (o2.getName());
}
});
final Remapper remapper = new ClassCollector(classes, prefix, root);
final ClassWriter inner = new ClassWriter(ClassWriter.COMPUTE_MAXS);
final RemappingClassAdapter visitor = new RemappingClassAdapter(inner, remapper);
try {
reader.accept(visitor, ClassReader.EXPAND_FRAMES);
}
catch (Exception ex) {
ex.toString();
}
return classes;
}
Important: when I initialized inner (as below) with null, then the program does not crash, but cannot detect all dependencies, and for example cannot detect assert dependency in the above example.
final ClassVisitor inner = null; //new ClassWriter(ClassWriter.COMPUTE_MAXS);
Please let me know if any one knows why the program is correct as an ordinary java application, but crash as plugin.
ClassReader(String name) uses the ClassLoader.loadSystemResourceAsStream() method to access the bytes for a requested class. If the classes you want to analyze are not in the class path, this won't work, since the class path is what loadSystemResourceAsStream searches.

JavaCompiler with dependent class

I'm using JavaCompiler to compile a class. I have jar dependency, where I used to give it in class path, I have a class (class1) file in the same directly, which is a dependent for another class (class2).
Simply
Class1.class
Class2.java
I want to compile Class2.java, in Class2 have a code like
Class1.sayHi();
When I compile it its saying
error: cannot find symbol
How can I include Class1.class while compiling Class2
My compiler code
String fileToCompile = classFile;
System.setProperty("java.home", RuntimeCompiler.getJDKPath());
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
Iterable<? extends JavaFileObject> compilationSource =
fileManager.getJavaFileObjects(fileToCompile);
List<String> optionList = new ArrayList<String>();
optionList.addAll(Arrays.asList("-classpath",dynamicClassPath));
try{
compiler.getTask(null, null, null, optionList, null, compilationSource).call();
return true;
}catch (Exception e) {
return false;
}
You can specify multiple files/classes on the class path. Just separate them with either a colon or semicolon, depending on your platform.
Have you verified that it will compile using just the javac command? If this works then it must be something in your procedure and not the class path.
The class path for a particular compiled class can be given as
-classpath "full_folder_Path_Till_Package"
Ex:
dynamicClassPath = "C:/work/sample1/core"
in core directory you will have package folder "com" inside that dependant class.

Compiled a class, but where is it?

I'm not an expert in Java and I'm pretty new to the whole concept of compiling and running dynamic generated code, which is so simple in other languages, expecially script languages like Javascript and PHP.
I'm following this snippet of code:
http://www.java2s.com/Code/Java/JDK-6/CompilingfromMemory.htm
and I made something like this:
private final String = "GeneratedClass_" + Long.toHexString(random.nextLong());
private Method compileCode(String code) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) return null;
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
JavaFileObject source = new JavaSource(className, code);
Iterable<? extends JavaFileObject> compilationUnits = Arrays.asList(source);
CompilationTask task = compiler.getTask(null, null, diagnostics, null, null, compilationUnits);
if (!task.call()) return null;
try {
return Class.forName(className).getDeclaredMethods()[0];
} catch (ClassNotFoundException e) {}
return null;
}
private class JavaSource extends SimpleJavaFileObject {
final String code;
JavaSource(String name, String code) {
super(URI.create("string:///" + name.replace('.','/') + Kind.SOURCE.extension),Kind.SOURCE);
this.code = code;
}
#Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {return code;}
}
Just imagine that the string code is something like
"public class GeneratedClass_65ce701239c32ce0 {
public String hello() {
return "Hello, world!";
}
}"
It works well until that Class.forName which throws a ClassNotFoundException. I'm puzzled since it doesn't seem I cut something important from the snippet: so, the class was compiled but where has it gone?
I read something about using a different class loader, but since, like I said, I'm pretty new to all this stuff I don't know where to head and how to use it, and how should I define my own extension of ClassLoader.
The only thing I know is that everything seems quite complicated to me...
Using Eclipse Indigo in Windows 7 and JDK 1.7.
One important thing you cut was all the error output and diagnostic information. You'd never know if something went wrong. However, everything looks correct. Your problem is most likely just that you didn't send any options to the compiler, so it'll write the class file out to wherever it feels like (current working directory is the default, I believe), and that's probably not on your classpath, especially in an IDE. Try running it from the command line to prove to yourself it works. This should work:
mkdir tmp
javac -d tmp <path your main class .java file>
java -cp .;tmp <your main class name>
If you're not familiar with the command-line tools, the argument to javac has to be a file system path to the .java file, and the argument to java needs to be the .-separated, fully-qualifed class name, like com.foo.Main. Doing that should:
Compile your class to the tmp directory.
Write your dynamically-generated class to the current directory.
Successfully load the newly compiled class from the current directory because it's on the classpath.

Categories

Resources